34 Pyret for Racketeers and Schemers
If you’ve programmed before in a language like Scheme or the student
levels of Racket (or the WeScheme programming environment), or for
that matter even in certain parts of OCaml, Haskell, Scala, Erlang,
Clojure, or other languages, you will find many parts of Pyret very
familiar. This chapter is specifically written to help you make the
transition from (student) Racket/Scheme/WeScheme (abbreviated “RSW”)
to Pyret by showing you how to convert the syntax. Most of what we say
applies to all these languages, though in some cases we will refer
specifically to Racket (and WeScheme) features not found in Scheme.
In every example below, the two programs will produce the same results.
34.1 Numbers, Strings, and Booleans
Numbers are very similar between the two. Like Scheme, Pyret
implements arbitrary-precision numbers and rationals. Some of the more
exotic numeric systems of Scheme (such as complex numbers) aren’t in
Pyret; Pyret also treats imprecise numbers slightly differently.
Strings are also very similar, though Pyret allows you to use
single-quotes as well.
RSW | Pyret |
"Hello, world!" | "Hello, world!" |
RSW | Pyret |
"\"Hello\", he said" | "\"Hello\", he said" |
RSW | Pyret |
"\"Hello\", he said" | '"Hello", he said' |
Booleans have the same names:
34.2 Infix Expressions
Pyret uses an infix syntax, reminiscent of many other textual
programming languages:
RSW | Pyret |
(* (- 4 2) 5) | (4 - 2) * 5 |
Note that Pyret does not have rules about orders of precedence between
operators, so when you mix operators, you have to parenthesize the
expression to make your intent clear. When you chain the same
operator you don’t need to parenthesize; chaining associates to the
left in both languages:
RSW | Pyret |
(/ 1 2 3 4) | 1 / 2 / 3 / 4 |
These both evaluate to 1/24.
34.3 Function Definition and Application
Function definition and application in Pyret have an infix syntax,
more reminiscent of many other textual programming
languages. Application uses a syntax familiar from conventional
algebra books:
RSW | Pyret |
(dist 3 4) | dist(3, 4) |
Application correspondingly uses a similar syntax in function headers,
and infix in the body:
RSW | Pyret |
(define (dist x y) | (sqrt (+ (* x x) | (* y y)))) |
| fun dist(x, y):
num-sqrt((x * x) +
(y * y))
end |
34.4 Tests
There are essentially three different ways of writing the equivalent
of Racket’s check-expect tests. They can be translated into
check blocks:
RSW | Pyret |
(check-expect 1 1) | check:
1 is 1
end |
Note that multiple tests can be put into a single block:
RSW | Pyret |
(check-expect 1 1) | (check-expect 2 2) |
| check:
1 is 1
2 is 2
end |
The second way is this: as an alias for check we can also write
examples. The two are functionally identical, but they capture
the human difference between examples (which explore the
problem, and are written before attempting a solution) and
tests (which try to find bugs in the solution, and are written
to probe its design).
The third way is to write a where block to accompany a function
definition. For instance:
fun double(n):
n + n
where:
double(0) is 0
double(10) is 20
double(-1) is -2
end
These can even be written for internal functions (i.e., functions
contained inside other functions), which isn’t true for
check-expect.
In Pyret, unlike in Racket, a testing block can contain a
documentation string. This is used by Pyret when reporting test
successes and failures. For instance, try to run and see what you get:
check "squaring always produces non-negatives":
(0 * 0) is 0
(-2 * -2) is 4
(3 * 3) is 9
end
This is useful for documenting the purpose of a testing block.
Just as in Racket, there are many testing operators in Pyret (in
addition to is). See
the
documentation.
34.5 Variable Names
Both languages have a fairly permissive system for naming
variables. While you can use CamelCase and under_scores in both, it is
conventional to instead use what is known as
kebab-case.
This name is inaccurate. The word “kebab”
just means “meat”. The skewer is the “shish”. Therefore, it ought
to at least be called “shish kebab case”. Thus:
RSW | Pyret |
this-is-a-name | this-is-a-name |
Even though Pyret has infix subtraction, the language can
unambiguously tell apart this-name (a variable) from
this - name (a subtraction expression) because the - in
the latter must be surrounded by spaces.
Despite this spacing convention, Pyret does not permit some of the
more exotic names permitted by Scheme. For instance, one can write
in Scheme but that is not a valid variable name in Pyret.
34.6 Data Definitions
Pyret diverges from Racket (and even more so from Scheme) in its
handling of data definitions. First, we will see how to define a
structure:
RSW | Pyret |
(define-struct pt (x y)) | data Point:
| pt(x, y)
end |
This might seem like a fair bit of overkill, but we’ll see in a moment
why it’s useful. Meanwhile, it’s worth observing that when you have
only a single kind of datum in a data definition, it feels unwieldy to
take up so many lines. Writing it on one line is valid, but now it
feels ugly to have the | in the middle:
data Point: | pt(x, y) end
Therefore, Pyret permits you to drop the initial |, resulting
in the more readable
Now suppose we have two kinds of points. In the student languages of
Racket, we would describe this with a comment:
;; A Point is either |
;; - (pt number number), or |
;; - (pt3d number number number) |
In Pyret, we can express this directly:
data Point:
| pt(x, y)
| pt3d(x, y, z)
end
In short, Racket optimizes for the single-variant case, whereas Pyret
optimizes for the multi-variant case. As a result, it is difficult to
clearly express the multi-variant case in Racket, while it is unwieldy
to express the single-variant case in Pyret.
For structures, both Racket and Pyret expose constructors, selectors,
and predicates. Constructors are just functions:
Predicates are also functions with a particular naming scheme:
and they behave the same way (returning true if the argument
was constructed by that constructor, and false otherwise). In
contrast, selection is different in the two languages (and we will see
more about selection below, with cases):
Note that in the Racket case, pt-x checks that the parameter
was constructed by pt before extracting the value of the
x field. Thus, pt-x and pt3d-x are two different
functions and neither one can be used in place of the other. In
contast, in Pyret, .x extracts an x field of any value
that has such a field, without attention to how it was
constructed. Thus, we can use .x on a value whether it was
constructed by pt or pt3d (or indeed anything else with
that field). In contrast, cases does pay attention to this
distinction.
34.7 Conditionals
There are several kinds of conditionals in Pyret, one more than in the
Racket student languages.
General conditionals can be written using if, corresponding to
Racket’s if but with more syntax.
RSW | Pyret |
(if full-moon | "howl" | "meow") |
| if full-moon:
"howl"
else:
"meow"
end |
RSW | Pyret |
(if full-moon | "howl" | (if new-moon | "bark" | "meow")) |
| if full-moon:
"howl"
else if new-moon:
"bark"
else:
"meow"
end |
Note that if includes else if, which makes it possible
to list a collection of questions at the same level of indentation,
which if in Racket does not have. The corresponding code in
Racket would be written
(cond |
[full-moon "howl"] |
[new-moon "bark"] |
[else "meow"]) |
to restore the indentation. There is a similar construct in Pyret
called ask, designed to parallel cond:
ask:
| full-moon then: "howl"
| new-moon then: "bark"
| otherwise: "meow"
end
In Racket, we also use cond to dispatch on a datatype:
(cond |
[(pt? v) (+ (pt-x v) (pt-y v))] |
[(pt3d? v) (+ (pt-x v) (pt-z v))]) |
We could write this in close parallel in Pyret:
ask:
| is-pt(v) then: v.x + v.y
| is-pt3d(v) then: v.x + v.z
end
or even as:
if is-pt(v):
v.x + v.y
else if is-pt3d(v):
v.x + v.z
end
(As in Racket student languages, the Pyret versions will signal an
error if no branch of the conditional matched.)
However, Pyret provides a special syntax just for data
definitions:
cases (Point) v:
| pt(x, y) => x + y
| pt3d(x, y, z) => x + z
end
This checks that v is a Point, provides a clean
syntactic way of identifying the different branches, and makes
it possible to give a concise local name to each field position
instead of having to use selectors like .x. In general, in
Pyret we prefer to use cases to process data
definitions. However, there are times when, for instance, there many
variants of data but a function processes only very few of them. In
such situations, it makes more sense to explicitly use predicates and
selectors.
34.8 Lists
In Racket, depending on the language level, lists are created using
either cons or list, with empty for the empty
list. The corresponding notions in Pyret are called link,
list, and empty, respectively. link is a
two-argument function, just as in Racket:
RSW | Pyret |
(cons 1 empty) | link(1, empty) |
RSW | Pyret |
(list 1 2 3) | [list: 1, 2, 3] |
Note that the syntax [1, 2, 3], which represents lists in many
languages, is not legal in Pyret: lists are not privileged with
their own syntax. Rather, we must use an explicit constructor:
just as [list: 1, 2, 3] constructs a list, [set: 1, 2,
3] constructs a set instead of a list.In fact, we can
create our own constructors
and use them with this syntax.
Try typing [1, 2, 3] and see the error message.
This shows us how to construct lists. To take them apart, we use
cases. There are two variants, empty and link
(which we used to construct the lists):
RSW | Pyret |
(cond | [(empty? l) 0] | [(cons? l) | (+ (first l) | (g (rest l)))]) |
| cases (List) l:
| empty => 0
| link(f, r) => f + g(r)
end |
It is conventional to call the fields f and r (for
“first” and “rest”). Of course, this convention does not work if
there are other things by the same name; in particular, when writing a
nested destructuring of a list, we conventionally write fr and
rr (for “first of the rest” and “rest of the rest”).
34.9 First-Class Functions
The equivalent of Racket’s lambda is Pyret’s lam:
RSW | Pyret |
(lambda (x y) (+ x y)) | lam(x, y): x + y end |
34.10 Annotations
In student Racket languages, annotations are usually written as comments:
; square: Number -> Number |
; sort-nums: List<Number> -> List<Number> |
; sort: List<T> * (T * T -> Boolean) -> List<T> |
In Pyret, we can write the annotations directly on the parameters and
return values. Pyret will check them to a limited extent dynamically,
and can check them statically with its type checker. The corresponding
annotations to those above would be written as
fun square(n :: Number) -> Number: ...
fun sort-nums(l :: List<Number>) -> List<Number>: ...
fun sort<T>(l :: List<T>, cmp :: (T, T -> Boolean)) -> List<T>: ...
Though Pyret does have a notation for writing annotations by
themselves (analogous to the commented syntax in Racket), they aren’t
currently enforced by the language, so we don’t include it here.
34.11 What Else?
If there are other parts of Scheme or Racket syntax that you would
like to see translated, please
let us know.