On this page:
2.1.11.1 Lambda Expressions
2.1.11.2 Curly-Brace Lambda Shorthand
2.1.11.3 Anonymous Method Expressions
2.1.11.4 Application Expressions
2.1.11.5 Curried Application Expressions
2.1.11.6 Chaining Application
2.1.11.7 Instantiation Expressions
2.1.11.8 Binary Operators
2.1.11.9 Tuple Expressions
2.1.11.10 Tuple Access Expressions
2.1.11.11 Object Expressions
2.1.11.12 Dot Expressions
2.1.11.13 Extend Expressions
2.1.11.14 If Expressions
2.1.11.15 Ask Expressions
2.1.11.16 Cases Expressions
2.1.11.17 For Expressions
2.1.11.18 Template (...) Expressions
2.1.11.19 Tables
2.1.11.19.1 Sorting Table Rows
2.1.11.19.2 Transforming Table Rows
2.1.11.19.3 Extracting Table Columns
2.1.11.19.4 Adding Table Columns
2.1.11.20 Table Loading Expressions
2.1.11.21 Reactor Expressions
2.1.11.22 Mutable fields
2.1.11.23 Construction expressions
2.1.11.24 Expression forms of bindings
2.1.11 Expressions

The following are all the expression forms of Pyret:

‹expr›: ‹paren-expr› | ‹id-expr› | ‹prim-expr› | ‹lam-expr› | ‹method-expr› | ‹app-expr› | ‹obj-expr› | ‹dot-expr› | ‹extend-expr› | ‹tuple-expr› | ‹tuple-get› | ‹template-expr› | ‹get-bang-expr› | ‹update-expr› | ‹if-expr› | ‹ask-expr› | ‹cases-expr› | ‹for-expr› | ‹user-block-expr› | ‹inst-expr› | ‹construct-expr› | ‹multi-let-expr› | ‹letrec-expr› | ‹type-let-expr› | ‹construct-expr› | ‹table-expr› | ‹table-select› | ‹table-sieve› | ‹table-order› | ‹table-extract› | ‹table-transform› | ‹table-extend› | ‹load-table-expr› | ‹reactor-expr› ‹paren-expr›: ( ‹binop-expr› ) ‹id-expr›: NAME ‹prim-expr›: NUMBER | RATIONAL | BOOLEAN | STRING

2.1.11.1 Lambda Expressions

The grammar for a lambda expression is:

‹lam-expr›: lam ‹fun-header› [block] : ‹doc-string› ‹block› ‹where-clause› end

A lambda expression creates a function value that can be applied with application expressions. The arguments in args are bound to their arguments as immutable identifiers as in a let expression.

Examples:

check: f = lam(x, y): x - y end f(5, 3) is 2 end check: f = lam({x;y}): x - y end f({5;3}) is 2 end

These identifiers follow the same rules of no shadowing and no assignment.

Examples:

x = 12 f = lam(x): x end # ERROR: x shadows a previous definition g = lam(y): y := 10 # ERROR: y is not a variable and cannot be assigned y + 1 end

If the arguments have annotations associated with them, they are checked before the body of the function starts evaluating, in order from left to right. If an annotation fails, an exception is thrown.

add1 = lam(x :: Number): x + 1 end add1("not-a-number") # Error: expected a Number and got "not-a-number"

A lambda expression can have a return annotation as well, which is checked before evaluating to the final value:

Examples:

add1 = lam(x) -> Number: tostring(x) + "1" end add1(5) # Error: expected a Number and got "51"

Lambda expressions remember, or close over, the values of other identifiers that are in scope when they are defined. So, for example:

Examples:

check: x = 10 f = lam(y): y + x end f(5) is 15 end

2.1.11.2 Curly-Brace Lambda Shorthand

Lambda expressions can also be written with a curly-brace shorthand:

‹curly-lam-expr›: { ‹fun-header› [block] : ‹doc-string› ‹block› ‹where-clause› }

Examples:

check: x = 10 f = {(y :: Number) -> Number: x + y} f(5) is 15 end

2.1.11.3 Anonymous Method Expressions

An anonymous method expression looks much like an anonymous function (defined with lam):

‹method-expr›: method ‹fun-header› [block] : ‹doc-string› ‹block› ‹where-clause› end

All the same rules for bindings, including annotations and shadowing, apply the same to ‹method-expr›s as they do to ‹lam-expr›s.

It is a well-formedness error for a method to have no arguments.

At runtime, a ‹method-expr› evaluates to a method value. Method values cannot be applied directly:

Examples:

check: m = method(self): self end m(5) raises "non-function" end

Instead, methods must be included as object fields, where they can then be bound and invoked. A method value can be used in multiple objects:

Examples:

check: m = method(self): self.x end o = { a-method-name: m, x: 20 } o2 = { a-method-name: m, x: 30 } o.a-method-name() is 20 o2.a-method-name() is 30 end

2.1.11.4 Application Expressions

Function application expressions have the following grammar:

‹app-expr›: ‹expr› ‹app-args› ‹app-args›: ( [(‹app-arg-elt›)* ‹binop-expr›] ) ‹app-arg-elt›: ‹binop-expr› ,

An application expression is an expression followed by a comma-separated list of arguments enclosed in parentheses. It first evaluates the arguments in left-to-right order, then evaluates the function position. If the function position is a function value, the number of provided arguments is checked against the number of arguments that the function expects. If they match, the arguments names are bound to the provided values. If they don’t, an exception is thrown.

Note that there is no space allowed before the opening parenthesis of the application. If you make a mistake, Pyret will complain:

f(1) # This is the function application expression f(1) f (1) # This is the id-expr f, followed by the paren-expr (1) # The second form yields a well-formedness error that there # are two expressions on the same line

2.1.11.5 Curried Application Expressions

Suppose a function is defined with multiple arguments:

fun f(v, w, x, y, z): ... end

Sometimes, it is particularly convenient to define a new function that calls f with some arguments pre-specified:

call-f-with-123 = lam(y, z): f(1, 2, 3, y, z) end

Pyret provides syntactic sugar to make writing such helper functions easier:

call-f-with-123 = f(1, 2, 3, _, _) # same as the fun expression above

Specifically, when Pyret code contains a function application some of whose arguments are underscores, it constructs an lambda expression with the same number of arguments as there were underscores in the original expression, whose body is simply the original function application, with the underscores replaced by the names of the arguments to the anonymous function.

This syntactic sugar also works with operators. For example, the following are two ways to sum a list of numbers:

[list: 1, 2, 3, 4].foldl(lam(a, b): a + b end, 0) [list: 1, 2, 3, 4].foldl(_ + _, 0)

Likewise, the following are two ways to compare two lists for equality:

list.map_2(lam(x, y): x == y end, first-list, second-list) list.map_2(_ == _, first-list, second-list)

Note that there are some limitations to this syntactic sugar. You cannot use it with the is or raises expressions in check blocks, since both test expressions and expected outcomes are known when writing tests. Also, note that the sugar is applied only to one function application at a time. As a result, the following code:

_ + _ + _

desugars to

lam(z): (lam(x, y): x + y end) + z end

which is probably not what was intended. You can still write the intended expression manually:

lam(x, y, z): x + y + z end

Pyret just does not provide syntactic sugar to help in this case (or other more complicated ones).

2.1.11.6 Chaining Application

‹chain-app-expr›: ‹binop-expr› ^ ‹binop-expr›

The expression e1 ^ e2 is equivalent to e2(e1). It’s just another way of writing a function application to a single argument.

Sometimes, composing functions doesn’t produce readable code. For example, if say we have a Tree datatype, and we have an add operation on it, defined via a function. To build up a tree with a series of adds, we’d write something like:

t = add(add(add(add(empty-tree, 1), 2), 3), 4)

Or maybe

t1 = add(empty-tree, 1) t2 = add(t1, 2) t3 = add(t2, 3) t = add(t3, 4)

If add were a method, we could write:

t = empty-tree.add(1).add(2).add(3).add(4)

which would be more readable, but since add is a function, this doesn’t work.

In this case, we can write instead:

t = empty-tree ^ add(_, 1) ^ add(_, 2) ^ add(_, 3)

This uses curried application to create a single argument function, and chaining application to apply it. This can be more readable across several lines of initialization as well, when compared to composing “inside-out” or using several intermediate names:

t = empty-tree ^ add(_, 1) ^ add(_, 2) ^ add(_, 3) # and so on

2.1.11.7 Instantiation Expressions

Functions may be defined with parametric signatures. Calling those functions does not require specifying the type parameter, but supplying it might aid in readability, or may aid the static type checker. You can supply the type arguments just between the function name and the left-paren of the function call. Spaces are not permitted before the left-angle bracket or after the right-angle bracket

‹inst-expr›: ‹expr› < ‹ann› (, ‹ann›)* >

Examples:

fun is-even(n :: Number) -> Boolean: num-modulo(n, 2) == 0 end check: map<Number, Boolean>(is-even, [list: 1, 2, 3]) is [list: false, true, false] end

2.1.11.8 Binary Operators

There are a number of binary operators in Pyret. A binary operator expression is a series of expressions joined by binary operators. An expression itself is also a binary operator expression.

‹binop-expr›: ‹expr› (BINOP ‹expr›)*

Pyret supports the following operations, shown by example:

examples: 1 + 1 is 2 1 - 1 is 0 2 * 4 is 8 6 / 3 is 2 1 < 2 is true 1 <= 1 is true 1 > 1 is false 1 >= 1 is true 1 == 1 is true true and true is true false or true is true not(false) is true end

There are additional equality operators in Pyret, which also call methods, but are somewhat more complex. They are documented in detail in equality.

The arithmetic and comparison operators examine their arguments. For primtive numbers and strings, the operation happens internally to Pyret. If the arguments are objects, however, the operators are syntactic sugar for a particular method call, as follows:

left + right

  

left._plus(right)

left - right

  

left._minus(right)

left * right

  

left._times(right)

left / right

  

left._divide(right)

left <= right

  

left._lessequal(right)

left < right

  

left._lessthan(right)

left >= right

  

left._greaterequal(right)

left > right

  

left._greaterthan(right)

Logical operators do not have a corresponding method call, since they only apply to primitive boolean values.

2.1.11.9 Tuple Expressions

Tuples are an immutable, fixed-length collection of expressions indexed by non-negative integers:

‹tuple-expr›: { ‹tuple-fields› } ‹tuple-fields›: ‹binop-expr› (; ‹binop-expr›)* [;]

A semicolon-separated sequence of fields enclosed in {} creates a tuple.

2.1.11.10 Tuple Access Expressions

‹tuple-get›: ‹expr› . { NUMBER }

A tuple-get expression evaluates the expr to a value val, and then does one of three things:

A static well-formedness error is raised if the index is negative

  • Raises an exception, if expr is not a tuple

  • Raises an exception, if NUMBER is equal to or greater than the length of the given tuple

  • Evaluates the expression, returning the val at the given index. The first index is 0

For example:

check: t = {"a";"b";true} t.{0} is "a" t.{1} is "b" t.{2} is true end

Note that the index is restricted syntactically to being a number. So this program is a parse error:

t = {"a";"b";"c"} t.{1 + 1}

This restriction ensures that tuple access is typable.

2.1.11.11 Object Expressions

Object expressions map field names to values:

‹obj-expr›: { ‹fields› } | { } ‹fields›: (‹list-field›)* ‹field› [,] ‹list-field›: ‹field› , ‹field›: ‹key› : ‹binop-expr› | method ‹key› ‹fun-header› [block] : ‹doc-string› ‹block› ‹where-clause› end ‹key›: NAME

A comma-separated sequence of fields enclosed in {} creates an object; we refer to the expression as an object literal. There are two types of fields: data fields and method fields. A data field in an object literal simply creates a field with that name on the resulting object, with its value equal to the right-hand side of the field. A method field

"method" key fun-header ":" doc-string block where-clause "end"

is syntactic sugar for:

key ":" "method" fun-header ":" doc-string block where-clause "end"

That is, it’s just special syntax for a data field that contains a method value.

The fields are evaluated in the order they appear. If the same field appears more than once, it is a compile-time error.

2.1.11.12 Dot Expressions

A dot expression is any expression, followed by a dot and name:

‹dot-expr›: ‹expr› . NAME

A dot expression evaluates the expr to a value val, and then does one of three things:

2.1.11.13 Extend Expressions

The extend expression consists of an base expression and a list of fields to extend it with:

‹extend-expr›: ‹expr› . { ‹fields› }

The extend expression first evaluates expr to a value val, and then creates a new object with all the fields of val and fields. If a field is present in both, the new field is used.

Examples:

check: o = {x : "original-x", y: "original-y"} o2 = o.{x : "new-x", z : "new-z"} o2.x is "new-x" o2.y is "original-y" o2.z is "new-z" end

2.1.11.14 If Expressions

An if expression has a number of test conditions and an optional else case.

‹if-expr›: if ‹binop-expr› [block] : ‹block› (‹else-if›)* [else: ‹block›] end ‹else-if›: else if ‹binop-expr› : ‹block›

For example, this if expression has an "else:"

if x == 0: 1 else if x > 0: x else: x * -1 end

This one does not:

if x == 0: 1 else if x > 0: x end

Both are valid. The conditions are tried in order, and the block corresponding to the first one to return true is evaluated. If no condition matches, the else branch is evaluated if present. If no condition matches and no else branch is present, an error is thrown. If a condition evaluates to a value other than true or false, a runtime error is thrown.

2.1.11.15 Ask Expressions

An ask expression is a different way of writing an if expression that can be easier to read in some cases.

‹ask-expr›: ASK [block] : (‹ask-branch›)* [| otherwise: ‹block›] end ‹ask-branch›: | ‹binop-expr› then: ‹block›

This ask expression:

ask: | x == 0 then: 1 | x > 0 then: x | otherwise: x * -1 end

is equivalent to

if x == 0: 1 else if x > 0: x else: x * -1 end

Similar to if, if an otherwise: branch isn’t specified and no branch matches, a runtime error results.

2.1.11.16 Cases Expressions

A cases expression consists of a datatype (in parentheses), an expression to inspect (before the colon), and a number of branches. It is intended to be used in a structure parallel to a data definition.

‹cases-expr›: cases ( ‹ann› ) ‹expr› [block] : (‹cases-branch›)* [| else => ‹block›] end ‹cases-branch›: | NAME [‹args›] => ‹block›

The check-ann must be a type, like List. Then expr is evaluated and checked against the given annotation. If it has the right type, the cases are then checked.

Cases should use the names of the variants of the given data type as the NAMEs of each branch. In the branch that matches, the fields of the variant are bound, in order, to the provided args, and the right-hand side of the => is evaluated in that extended environment. An exception results if the wrong number of arguments are given.

An optional else clause can be provided, which is evaluated if no cases match. If no else clause is provided, a runtime error results.

For example, some cases expression on lists looks like:

check: result = cases(List) [list: 1,2,3]: | empty => "empty" | link(f, r) => "link" end result is "link" result2 = cases(List) [list: 1,2,3]: | empty => "empty" | else => "else" end result2 is else result3 = cases(List) empty: | empty => "empty" | else => "else" end result3 is "empty" end

If a field of the variant is a tuple, it can also be bound using a tuple binding.

For example, a cases expression on a list with tuples looks like:

Examples:

check: result4 = cases(List) [list: {"a"; 1}, {"b"; 2}, {"c"; 3}]: | empty => "empty" | link({x;y}, r) => x | else => "else" end result4 is "a" end

2.1.11.17 For Expressions

For expressions consist of the for keyword, followed by a list of binding from expr clauses in parentheses, followed by a block:

‹for-expr›: for ‹expr› ( [(‹for-bind-elt›)* ‹for-bind›] ) ‹return-ann› [block] : ‹block› end ‹for-bind-elt›: ‹for-bind› , ‹for-bind›: ‹binding› from ‹binop-expr›

The for expression is just syntactic sugar for a lam-expr and a app-expr. An expression

for fexpr(arg1 :: ann1 from expr1, ...) -> ann-return: block end

is equivalent to:

fexpr(lam(arg1 :: ann1, ...) -> ann-return: block end, expr1, ...)

Using a for-expr can be a more natural way to call, for example, list iteration functions because it puts the identifier of the function and the value it draws from closer to one another. Use of for-expr is a matter of style; here is an example that compares fold with and without for:

for fold(sum from 0, number from [list: 1,2,3,4]): sum + number end fold(lam(sum, number): sum + number end, 0, [list: 1,2,3,4])

2.1.11.18 Template (...) Expressions

A template expression is three dots in a row:

‹template-expr›: ...

It is useful for a placeholder for other expressions in code-in-progress. When it is evaluated, it raises a runtime exception that indicates the expression it is standing in for isn’t yet implemented:

Examples:

fun list-sum(l :: List<Number>) -> Number: cases(List<Number>) l: | empty => 0 | link(first, rest) => first + ... end end check: list-sum(empty) is 0 list-sum(link(1, empty)) raises "template-not-finished" end

This is handy for starting a function (especially one with many cases) with some tests written and others to be completed.

These other positions for ... may be included in the future.

The ... expression can only appear where expressions can appear. So it is not allowed in binding positions or annotation positions. These are not allowed:

Examples:

fun f(...): # parse error "todo" end x :: ... = 5 # parse error

Because templates are by definition unfinished, the presence of a template expression in a block exempts that block from explicit-blockiness checking.

2.1.11.19 Tables

Tables precise syntax is documented here. For helper functions and data structures, see Creating Tables.

Table expressions consist of a list of column names followed by one or more rows of data:

‹table-expr›: table: ‹table-headers› ‹table-rows› end ‹table-headers›: ([‹table-header› ,])* ‹table-header› ‹table-header›: NAME [:: ‹ann›] ‹table-rows›: (‹table-row›)* ‹table-row› ‹table-row›: row: ([‹binop-expr› ,])* ‹binop-expr›

‹table-select›: select NAME (, NAME)* from ‹expr› end

‹table-sieve›: sieve ‹expr› [using ‹binding› (, ‹binding›)*] : ‹binop-expr› end

2.1.11.19.1 Sorting Table Rows

‹table-order›: order ‹expr› : ‹column-order› end ‹column-order›: NAME ascending | descending

2.1.11.19.2 Transforming Table Rows

‹table-transform›: transform ‹expr› [using ‹binding› (, ‹binding›)*] : ‹transform-fields› end ‹transform-fields›: ‹transform-field› (, ‹transform-field›)* [,] ‹transform-field›: ‹key› : ‹binop-expr›

2.1.11.19.3 Extracting Table Columns

‹table-extract›: extract NAME from ‹expr› end

2.1.11.19.4 Adding Table Columns

‹table-extend›: extend ‹expr› [using ‹binding› (, ‹binding›)*] : ([‹table-extend-field› ,])* ‹table-extend-field› end ‹table-extend-field›: ‹key› [:: ‹ann›] : ‹binop-expr› | ‹key› [:: ‹ann›] : ‹expr› of NAME

2.1.11.20 Table Loading Expressions

A table loading expression constructs a table using a data source and zero or more data sanitizers:

‹load-table-expr›: load-table : ‹table-headers› [‹load-table-specs›] end ‹load-table-specs›: (‹load-table-spec›)* ‹load-table-spec› ‹load-table-spec›: source: ‹expr› | sanitize NAME using ‹expr›

2.1.11.21 Reactor Expressions

‹reactor-expr›: reactor : init : ‹expr› ([, ‹option-name› : ‹expr›])* end ‹option-name›: | | on-tick | on-mouse | on-key | to-draw | stop-when | title | close-when-stop | seconds-per-tick

Reactors are described in detail in Creating Reactors.

2.1.11.22 Mutable fields

Pyret allows creating data definitions whose fields are mutable. Accordingly, it provides syntax for accessing and modifying those fields.

‹get-bang-expr›: ‹expr› ! NAME ‹update-expr›: ‹expr› ! { ‹fields› }

By analogy with how ‹dot-expr› accesses normal fields, ‹get-bang-expr› accesses mutable fields — but more emphatically so, because mutable fields, by their nature, might change. Dot-access to mutable fields also works, but does not return the field’s value: it returns the reference itself, which is a Pyret value that’s mostly inert and difficult to work with outside the context of its host object.

Examples:

data MutX: | mut-x(ref x, y) end ex1 = mut-x(1, 2) check: ex1!x is 1 # this access the value inside the reference ex1.x is-not 1 # this does not end

To update a reference value, we use syntax similar to ‹extend-expr›, likewise made more emphatic:

Examples:

ex1!{x: 42} check: ex1!x is 42 end

2.1.11.23 Construction expressions

Individual Pyret data values are syntactically simple to construct: they look similar to function calls. But arbitrarily-sized data is not as obvious. For instance, we could write

Examples:

link(1, link(2, link(3, link(4, empty))))

to construct a 4-element list of numbers, but this gets tiresome quite quickly. Many languages provide built-in syntactic support for constructing lists, but in Pyret we want all data types to be treated equally. Accordingly, we can write the above example as

Examples:

[list: 1, 2, 3, 4]

where list is not a syntactic keyword in the language. Instead, this is one example of a construction expression, whose syntax is simply

‹construct-expr›: [ ‹binop-expr› : ‹construct-args› ] ‹construct-args›: [‹binop-expr› (, ‹binop-expr›)*]

Pyret defines several of these constructors for you: lists, sets, arrays, and string-dictionaries all have the same syntax.

The expression before the initial colon is a Pyret object that has a particular set of methods available. Users can define their own constructors as well.

type Constructor<A> = { make0 :: ( -> A), make1 :: (Any -> A), make2 :: (Any, Any -> A), make3 :: (Any, Any, Any -> A), make4 :: (Any, Any, Any, Any -> A), make5 :: (Any, Any, Any, Any, Any -> A) make :: (RawArray<Any> -> A), }

When Pyret encounters a construction expression, it will call the appropriately-numbered method on the constructor objects, depending on the number of arguments it received.

Examples:

weird :: Constructor<String> = { make0: lam(): "nothing at all" end, make1: lam(a): "just " + tostring(a) end, make2: lam(a, b): tostring(a) + " and " + tostring(b) end, make3: lam(a, b, c): "several things" end, make4: lam(a, b, c, d): "four things" end, make5: lam(a, b, c, d, e): "five things" end, make : lam(args): "too many things" end } check: [weird: ] is "nothing at all" [weird: true] is "just true" [weird: 5, 6.24] is "5 and 6.24" [weird: true, false, 5] is "several things" [weird: 1, 2, 3, 4] is "four things" [weird: 1, 1, 1, 1, 1] is "five things" [weird: "a", "b", "c", true, false, 5] is "too many things" end

2.1.11.24 Expression forms of bindings

Every definition is Pyret is visible until the end of its scope, which is usually the nearest enclosing block. To limit that scope, you can wrap definitions in explicit ‹user-block-expr›s, but this is sometimes awkward to read. Pyret allows for three additional forms that combine bindings with expression blocks in a manner that is sometimes more legible:

‹multi-let-expr›: let ‹let-or-var› (, ‹let-or-var›)* [block] : ‹block› end ‹let-or-var›: ‹let-decl› | ‹var-decl› ‹letrec-expr›: letrec ‹let-decl› (, ‹let-decl›)* [block] : ‹block› end ‹type-let-expr›: type-let ‹type-let-or-newtype› (, ‹type-let-or-newtype›)* [block] : end ‹type-let-or-newtype›: ‹type-decl› | ‹newtype-decl›

These define their bindings only for the scope of the following block. A ‹multi-let-expr› defines a sequence of either let- or variable-bindings, each of which are in scope for subsequent ones. A ‹letrec-expr› defines a set of mutually-recursive let-bindings that may refer to each other in a well-formed way (i.e., no definition may rely on other definitions before they’ve been fully evaluated). These are akin to the ‹let-decl› and ‹var-decl› forms seen earlier, but with more explicitly-visible scoping rules.

Finally, ‹type-let-expr› defines local type aliases or new types, akin to ‹type-stmt›.