Quick Introduction to Rhombus with Pictures
This tutorial provides a brief introduction to the Rhombus programming language by using one of its picture-drawing libraries. Even if you don’t intend to use Rhombus for your artistic endeavors, the picture library supports interesting and enlightening examples. After all, a picture is worth five hundred “hello world”s.
Along the same lines, we assume that you will run the examples using DrRacket. (Rhombus is built on Racket and uses many racket tools.) Using DrRacket is the fastest way to get a sense of what the language and system feels like, even if you eventually use Rhombus with Emacs, vi, VS Code, or some other editor.
1 Ready...
Download Rhombus, install, and then start DrRacket.
2 Set...
To draw pictures, we must first import some picture functions. Copy the following into the definitions area, which is the top text area that you see in DrRacket: See the DrRacket documentation for a brief overview of the DrRacket IDE.
Then click the Run button. You’ll see the text caret move to the bottom text area, which is the interactions area.
3 Go!
When you type an expression after the > in the interactions window and hit Enter, DrRacket evaluates the expression and prints its result. An expression can be just a value, such as the number 5 or the string "art gallery":
> 5
5
> "art gallery"
"art gallery"
Arithmetic, simple function calls, and method calls look the same as in many other languages:
> 1 + 2
3
> math.max(1, 3)
3
"Art Gallery"
> circle()
A result from the circle function is a picture value, which prints as an expression result in much the same way that numbers or strings print.
The circle function accepts optional keyword arguments. A keyword is written starting with ~, and a keyword argument is written as a keyword followed by : and the argument.
> circle(~size: 20, ~fill: "red")
> rectangle(~width: 10, ~height: 20, ~line: "blue")
Try giving circle wrong arguments, just to see what happens:
> circle(20, "red")
circle: arity mismatch;
the expected number of arguments does not match the given number
expected: 0 plus optional arguments with keywords ~arc, ~around,
~duration, ~end, ~epoch, ~fill, ~line, ~line_width, ~order,
~refocus, ~size, and ~start
given: 2
arguments...:
20
"red"
Note that DrRacket highlights in pink the expression that triggered the error (but pink highlighting is not shown in this documentation).
In addition to basic picture constructors like circle and rectangle, there’s a beside function that combines pictures:
rectangle(~width: 10, ~height: 20),
~sep: 5)
If you wonder what other functions exist—
If you’re reading this in a browser, you can also just click on beside or any other imported identifier that is used in this tutorial.
4 Definitions
To use a particular circle and rectangle picture many times, it’s simpler to give them names. Move back to the definitions area (the top area) and add two definitions, so that the complete content of the definitions area looks like this:
pict open
Then click Run again. Now, you can just type c or r:
> r
> beside(c, r)
> beside(~sep: 20, c, r, c)
As you can see, the beside function accepts any number of picture arguments, while an optional ~sep argument specifies the amount of space to add between pictures.
We could have evaluated the def forms for c and
r in the interactions area instead of the definitions area. In
practice, though, the definitions area is where your program
lives—
Let’s add a function definition to the program. A function definition uses fun, then a function name, a parenthesized sequence of ,-separated names for the function arguments, a :, and then the body indented under the :.
fun cell(n):
// Two slashes start start a line comment.
// The expression below is the function body.
rectangle(~width: n, ~height: n, ~fill: ColorMode.inherit)
The use of ColorMode.inherit will let us apply a fill color externally. Meanwhile, it defaults to black:
> cell(10)
In the same way that definitions can be evaluated in the interactions area, expressions can be included in the definitions area. When a program is run, expression results from the definition area are shown in the interaction area. From now on, we’ll write our example definitions and expressions together, and you can put them in whichever area you prefer. The examples will build on each other, however, so it’s best to put at least the definitions in the definition area.
5 Annotations
Our first try at cell is a little sloppy, because it takes any argument and passes it on to rectangle, which can trigger an error from rectangle if that argument is bad.
> cell("red")
rectangle: argument does not satisfy annotation
argument: "red"
annotation: AutoReal
A better definition of cell annotates its arguments using :: to impose a check that the argument is valid, and it declares an annotation for the function’s result using ::. The result annotation is written after the parentheses for arguments and before : for the function body.
fun cell(n :: NonnegReal) :: Pict:
rectangle(~width: n, ~height: n, ~fill: ColorMode.inherit)
> cell("red")
cell: argument does not satisfy annotation
argument: "red"
annotation: NonnegReal
> cell(10).colorize("blue")
If cell accidentally returned a value that is not a picture, then the :: Pict result annotation would catch the error before returning that value. More importantly, the result annotation for cell makes the call to the colorize method in cell(10).colorize("blue") resolve statically to the Pict.colorize method, instead of calling just any method on the target object that happens to be named colorize. Although calling a dynamically discovered colorize is sometimes useful, static dispatch is normally better because it’s faster and safer.Static here does not mean static in the sense of static methods in Java, but in the sense of static typing. If Pict has subclasses that override colorize, then a call to Pict.colorize dispatches to an overriding implementation, if any.
A :: result annotation does incur the cost of a run-time check (unless the check is proven unneecssary by the optimizer). Instead of ::, use :~ to declare static information without an accompanying run-time check. In that case, the static information is just assumed to be correct. Meanwhile, the declaration use_static or the rhombus/static language ensures that operators like . are only ever used in a way that can be statically resolved.
6 Local Binding
The def form can be used in some places to create local bindings. For example, it can be used inside a function body:
stack(top, bottom)
Within a local block, Rhombus programmers will more often use let than def. The difference is that let allows a later definition with let to use the same name. The later definition does not modify the earlier definition’s variable; it just makes the new definition’s variable the one that is seen afterward in the block.
result
> checkerboard(cell(10))
7 Functions are Values
Instead of calling circle as a function, try evaluating just circle as an expression:
> circle
#<function:circle>
The identifier circle is bound to a function just like c is bound to a circle. Unlike a circle picture, there’s not a simple way of completely printing the function, so DrRacket just prints #<function:circle>.
This example shows that functions are values, just like numbers and pictures (even if they don’t print as nicely). Since functions are values, you can define functions that accept other functions as arguments. The -> annotation constructor lets you describe a function in terms of its argument and result annotations.
> series(cell)
> series(circle)
series: argument does not satisfy annotation
argument: #<function:circle>
annotation: Int -> Pict
Passing circle to series doesn’t work, because circle expects a keyword argument instead of a single by-position argument. We could define a new function just for this purpose, but if if we only need to adapt circle for just one use, then writing an extra definition is a hassle. The alternative is to use the expression form fun, which creates an anonymous function:
The expression form of fun is just like the definition form, but without the function name. An annotation could have been written on the argument n or for the result of the function, but annotations don’t particularly help in this case.
A fun definition form is really a shorthand for def plus a fun expression. For example, the series definition could be written as
beside(~sep: 4, mk(5), mk(10), mk(20)))
As a small syntactic adjustment, : can be used with def instead of =, which usually makes more sense when the right-hand side of the definition spans multiple lines.
def series:
beside(~sep: 4, mk(5), mk(10), mk(20))
In any case, Rhombus programmers generally prefer to use the shorthand fun definition form instead of this def plus fun combination.
8 Lists
Lists in Rhombus are written with […] square brackets.
> ["red", "green", "blue"]
["red", "green", "blue"]
[
,
]
Rhombus lists are immutable. If you add or remove a list item, then you get a new list, and the old one remains unmodified.
> favorite_colors
["red", "orange", "yellow", "blue", "purple", "pink"]
> colors
["red", "orange", "yellow", "green", "blue", "purple"]
Use […] square brackets as a postfix operation to
extract an element from a list by position (counting from 0). The
++ infix operator appends lists. Lists support many other
typical methods, such as List.remove and List.add
shown above. Despite the immutable nature of lists, they take advantage
of sharing internally so that most operations take O(log N) time
for a list of size N—
> colors[0]
"red"
> favorite_colors ++ ["black", "white"]
["red", "orange", "yellow", "blue", "purple", "pink", "black", "white"]
["red", "orange", "yellow"]
The List.map method takes a function to apply to each element of the list, and it creates a new list.
> rainbow(cell(10))
[
,
,
,
,
,
]
When calling a function, you can use & to splice a list of arguments into the function call.
Note that stack(rainbow(cell(10))) would not work, because stack does not want a list as an argument; it wants individual arguments that are Picts, but it is willing to accept any number of arguments.
9 Patterns and Repetitions
In most places within a Rhombus program where a variable is bound, the binding can be a pattern instead of just a plain identifier. Patterns imitate value-construction forms, so a list pattern is written with […] square brackets.
beside(c, d))
Most uses of lists involve any number of elements, instead of a fixed number. To support matching those kinds of lists, a list pattern can use ... after a subpattern to bind a repetition of the subpattern. Identifiers bound by the subpattern can be used later in a constructor that recognizes .... Here’s another way to define rainbow by letting c stand for each color in the colors list.
> rainbow(cell(10))
[
,
,
,
,
,
]
Concrete shapes and ... can be mixed. The next example uses the “don’t care” pattern _ to match any number of additional list elements in the result of rainbow, as long as there are at least four elements.
> grid([[a, b],
[c, d]])
10 Code as Data
The text function from pict creates a picture of text.
Suppose, though that we are creating a tutorial about the pict library for Rhombus, and we want to show literally the code that is shown in the interaction that calls rainbow. In that case, the literal 10 should use the color for literals and the parentheses () should use the color for parentheses. Building up the expression with individually colorized text calls would be tedious.
The pict/rhombus module provides a rhombus form that typesets its “argument” instead of evaluating it:
The rhombus form is possible because it is implemented as a macro instead of a function. Metaprogramming is often used to define or extend a programming language more generally, and that’s the subject of another tutorial (that’s much longer).
This example may seem frivolous at first glance, but consider that you
are currently reading a tutorial about Rhombus that is filled with
example code. Here’s the source:
rhombus-quick.scrbl.
You’ll see many uses of forms that quote and typeset code—
11 Modules
Since your program in the definitions window starts with
all of the code that you put in the definitions window is inside a module. Furthermore, the module initially imports everything from the rhombus language.
We have imported additional libraries using the import form. For convenience, we have opened each import, but if open is omitted, then imported bindings are available through a dotted name that starts with the last component of the module path. For example, the pict/radial library provides a flower function:
> radial.flower(~fill: "pink")
Modules are named and distributed in various ways:
Some modules are packaged in the Rhombus or Racket distribution or otherwise installed into a hierarchy of collections. For example, the module name pict/radial means “the module implemented in the file "radial.rhm" that is located in the "pict" collection.” When a module name includes no slash, then it refers to a "main.rhm" file.
Some collections of modules are distributed as packages. Packages can be installed using the Install Package... menu item in DrRacket’s File menu, or they can be installed using the raco pkg command-line tool. For example, installing the "avl" package makes the avl module available.
Packages can be registered at https://pkgs.racket-lang.org/, or they can be installed directly from a Git repository, web site, file, or directory. See Package Management in Racket for more information about packages.
Some modules live relative to other modules, without necessarily belonging to any particular collection or package. For example, in DrRacket, if you save your definitions so far in a file "quick.rhm" and add the line
cell
rainbow
then you can open a new tab or window in DrRacket, type the new program "use.rhm" in the same directory as "quick.rhm":
"quick.rhm"
and when you run "use.rhm", a rainbow list of squares is the output.
Rhombus programmers typically write new programs and libraries as modules that import each other through relative paths and collection-based paths. When a program or library developed this way seems useful to others, it can be registered as a package, especially if the implementation is hosted in a Git repository.
12 Where to Go From Here
To start learning about the full Rhombus language, move on to Rhombus Guide.
If you are familiar with Rhombus-style languages—




