Quick Introduction to Rhombus with Pictures
1 Ready...
2 Set...
3 Go!
4 Definitions
5 Annotations
6 Local Binding
7 Functions are Values
8 Lists
9 Patterns and Repetitions
10 Code as Data
11 Modules
12 Where to Go From Here
0.45+9.0.0.11

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.

#lang rhombus

import:

  pict open

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".titlecase()

"Art Gallery"

> circle()

image

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")

image

> rectangle(~width: 10, ~height: 20, ~line: "blue")

image

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:

> beside(circle(~size: 20),

         rectangle(~width: 10, ~height: 20),

         ~sep: 5)

image

If you wonder what other functions exist—perhaps a way to stack pictures vertically and left-aligned?—move the text caret to the name beside and press the F1 key in DrRacket. A browser window will open, and it will give you a link to the documentation for beside. Click the link, and you’ll see lots of other functions.

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:

#lang rhombus

import:

  pict open

 

def c = circle(~size: 10)

def r = rectangle(~width: 10, ~height: 20)

Then click Run again. Now, you can just type c or r:

> r

image

> beside(c, r)

image

> beside(~sep: 20, c, r, c)

image

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—it’s the file that you save—while the interaction area is for transient explorations and debugging tasks.

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)

image

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")

image

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:

fun checker(p1 :: Pict, p2 :: Pict) :: Pict:

  def top = beside(p1, p2)

  def bottom = beside(p2, p1)

  stack(top, bottom)

> checker(cell(10).colorize("black"),

          cell(10).colorize("red"))

image

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.

fun checkerboard(p :: Pict) :: Pict:

  let result = checker(p.colorize("black"),

                       p.colorize("red"))

  let result = checker(result, result)

  let result = checker(result, result)

  result

> checkerboard(cell(10))

image

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.

fun series(mk :: Int -> Pict) :: Pict:

  beside(~sep: 4, mk(5), mk(10), mk(20))

> series(cell)

image

> 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:

> series(fun (n): circle(~size: n))

image

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

def series = (fun (mk :: Int -> Pict) :: Pict:

                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:

  fun (mk :: Int -> Pict) :: Pict:

    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"]

> [circle(~size: 10), rectangle(~width: 10, ~height: 20)]

[image, image]

Rhombus lists are immutable. If you add or remove a list item, then you get a new list, and the old one remains unmodified.

def colors = ["red", "orange", "yellow", "green", "blue", "purple"]

def favorite_colors = colors.remove("green").add("pink")

> 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 Neven operations like List.append or List.sublistso Rhombus programmers usually don’t need to worry about the efficiency of lists.

> colors[0]

"red"

> favorite_colors ++ ["black", "white"]

["red", "orange", "yellow", "blue", "purple", "pink", "black", "white"]

> colors.take(3)

["red", "orange", "yellow"]

The List.map method takes a function to apply to each element of the list, and it creates a new list.

fun rainbow(p :: Pict) :: List.of(Pict):

  colors.map(fun (c): p.colorize(c))

> rainbow(cell(10))

[image, image, image, image, image, image]

When calling a function, you can use & to splice a list of arguments into the function call.

> stack(& rainbow(cell(10)))

image

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.

fun grid([[a :: Pict, b :: Pict],

          [c :: Pict, d :: Pict]]) :: Pict:

  stack(beside(a, b),

        beside(c, d))

> grid([[cell(10).colorize("red"), cell(10).colorize("orange")],

        [cell(10).colorize("yellow"), cell(10).colorize("green")]])

image

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.

fun rainbow(p :: Pict) :: List.of(Pict):

  let [c, ...] = colors

  [p.colorize(c), ...]

> rainbow(cell(10))

[image, image, image, image, image, image]

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.

> def [a, b, c, d, _, ...] = rainbow(cell(10))

> grid([[a, b],

        [c, d]])

image

10 Code as Data🔗ℹ

The text function from pict creates a picture of text.

> text("Hello").colorize("blue")

image

> text("Bye").scale(2)

image

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:

import:

  pict/rhombus open

> rhombus(rainbow(cell(10))).scale(2)

image

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—for documentation rendering instead of pictures, but it’s the same idea. Metaprogramming is pervasive in software development, and Rhombus supports it directly.

11 Modules🔗ℹ

Since your program in the definitions window starts with

#lang rhombus

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:

import:

  pict/radial

> radial.flower(~fill: "pink")

image

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

    export:

      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":

    #lang rhombus

    import:

      "quick.rhm"

    quick.rainbow(quick.cell(5))

    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—enough that you’re willing to wing it—and you are particularly interested in Rhombus’s metaprogramming facilities, then you might instead try Rhombus Metaprogramming Tutorial.