Lecture notes: Documentation, types, and organization

Reading documentation

Let’s take a look at the documentation for Pyret’s image library. Specifically, let’s check out the documentation for a function we touched on briefly last week but didn’t talk about in detail: overlay-xy. 1

The Pyret documentation gives the following signature for overlay-xy:

overlay-xy :: (
  img1 :: Image,
  dx :: Number,
  dy :: Number,
  img2 :: Image
)
-> Image

Image and Number are types, which represent categories of values. We’ve seen examples of both types--circle(50, "solid", "red") is an Image, and 1/3 is a number. String is also a type--"dog" is a String. We’ll see more types as the course continues.

The signature for overlay-xy uses these types to describe its inputs and its outputs. When we call overlay-xy, we’ll need to call it with an Image, two Number’s, and another Image–in that order. If we don’t, Pyret will raise an error. These expressions are both errors:

> overlay-xy(circle(50, "solid", "red"), 10, 10)
> overlay-xy(circle(50, "solid", "red"), 10, 10, "dog")

The signature provides descriptive names for its inputs to help you remember what each of them does. For instance, dx is the number of pixels to shift the background image on the x axis.

The signature also specifies a type (in this case, Image for the operation’s output). This determines the contexts in which you can use the operation. You can tell from the signature that

> rotate(45, overlay-xy(circle(50, "solid", "red"),
                        10, 10, 
                        circle(100, "solid", "blue")))

is a valid operation, while

3 + overlay-xy(circle(50, "solid", "red"), 
               10, 10, 
               circle(100, "solid", "blue"))

is not.

Code organization exercise

Here are two versions of a program to draw an alien face emoji.

Version 1:

include image

base = circle(100, "solid", "green")
eye = circle(18, "solid", "red")
# create pair of eyes, using another square as a spacer
eye-spacer = square(24, "solid", "green")
eye-bar = beside(eye, beside(eye-spacer, eye))
mouth = ellipse(60, 30, "solid", "blue")
# create a 3-pixel border for the base
border = circle(105, "solid", "black")
eye-face-spacer = rectangle(60, 30, "solid", "green")
eyes-and-mouth = above(eye-bar, above(eye-face-spacer, mouth))
the-face = overlay(eyes-and-mouth, base)
add-border = overlay(the-face, border)
emoji = scale(0.4, add-border)
emoji

Version 2:

  include image

  base = circle(100, "solid", "green")
  eye = circle(18, "solid", "red")

# create pair of eyes, using another square as a spacer
  eye-bar = 
    beside(eye, 
      beside(square(24, "solid", "green"), 
        eye))

  mouth = ellipse(60, 30, "solid", "blue")

  # create a 3-pixel border for the base
  # use the image-width to compute the border size
  border = circle((image-width(base) / 2) + 5, "solid", "black")

  # keep the large image so we can look at it if needed
  emoji-large =
    overlay(
      overlay(
        above(eye-bar,
          above(rectangle(60, 30, "solid", "green"),
            mouth)),
        base),
      border)

  # scale the large image down to the size we want
  emoji = scale(0.4, emoji-large)

  # automatically show the image when we run the file
  emoji

Which version do you like better, and why (class discussion)?

How about this version:

scale(0.4, 
  overlay(
    overlay(
      above(
        beside(
          circle(18, "solid", "red"), 
          beside(
            square(24, "solid", "green"),
            circle(18, "solid", "red"))),
        above(
          rectangle(60, 30, "solid", "green"),
          ellipse(60, 30, "solid", "blue"))),
      circle(100, "solid", "green")),
    circle(105, "solid", "black")))

In this course, we’ll generally encourage you to write code like Version 2. But finding the right balance between naming subcomputations and clean code structure is often challenging, and each person will do it differently! Please don’t write code like Version 3, though. Remember–programs are read by the computer, but they are also read by other people!

How Pyret evaluates expressions

Imagine that you had the following contents in the definitions window:

include image

SCOOP-SIZE = 15
cone = flip-vertical(triangle(SCOOP-SIZE * 2, "solid", "tan"))
overlay-xy(circle(SCOOP-SIZE, "solid", "pink"),
  0, 25,
  overlay-xy(circle(SCOOP-SIZE, "solid", "green"),
    0, 25,
    cone))

How will Pyret evaluate this program?

Pyret processes the expressions one at a time from top to bottom.

When Pyret encounters something of the form name = expr, it creates an entry in an internal dictionary. The dictionary maps names to values. Thus, Pyret evaluates the expression on the right of the =, then makes a dictionary entry to associate the name with that value.

When Pyret encounters an expression that has other expressions nested within it, it evaluates the sub-expressions from left-to-right and innermost to outermost. Thus, in the flip-vertical expression (that creates the cone), the first expression to evaluate is SCOOP-SIZE (which Pyret looks up in the dictionary), then Pyret computes 15 * 2, then it creates the triangle, then flips the triangle.

In the longer overlay-xy expression, the pink circle gets created before the green circle (by the left-to-right rule).

You may have noticed that SCOOP-SIZE is all in caps, while cone is in lowercase. Conceptually, SCOOP-SIZE is a key concept in the program (which we plan to use multiple times), whereas cone is just naming an intermediate computation for readability. We use this naming convention to help us remember the respective roles of the names in the program. What happens if we change SCOOP-SIZE?

What does the “Run” button do?

When you press the Run button, Pyret erases the dictionary, then re-processes the definitions window from the beginning. Any dictionary entries that you made only in the interactions (right) window disappear.

Expressions and statements

We’ve already seen that there’s a difference between

SCOOP-SIZE = 15

and

SCOOP-SIZE * 2

and their impact on the interactions window: the first adds to the dictionary, while the second displays a value. A piece of a program that changes how future expressions will evaluate is called a statement. Creating a name meets this definition, since expressions will yield errors or not depending on whether a name appears in the dictionary. Expressions perform computations without changing the information that Pyret maintains for running future expressions.

This distinction will become more meaningful later in the course, but we’ll use the terminology to get used to it now.

Footnotes:

1
You don’t need to use overlay-xy on the homework, though you can if you’d like.