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› NUMBERRATIONALBOOLEANSTRING
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.
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.
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:
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:
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› }
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:
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:
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
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.
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.
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:
Raises an exception, if NAME is not a field of expr
Evaluates to the value stored in NAME, if NAME is present and not a method
If the NAME field is a method value, evaluates to a function that is the method binding of the method value to val. For a method
m = method(self, x): body end
The method binding of m to a value v is equivalent to:
(lam(self): lam(x): body end end)(v)
What this detail means is that you can look up a method and it automatically closes over the value on the left-hand side of the dot. This bound method can be freely used as a function.
For example:
o = { method m(self, x): self.y + x end, y: 22 } check: the-m-method-closed-over-o = o.m the-m-method-closed-over-o(5) is 27 end
Note that a method binding is not a itself a method value. Creating new objects from method bindings will not behave the same as using method values directly.
For example:
code = method(self, x): self.y + x end p = { y: 10, m: code } q = p.{ y: 15 } r = { y: 20, m: p.m } # m given method binding, not a method check: p.m(5) is 15 q.m(5) is 20 # self.y dynamically resolved when code runs r.m(5) is 15 # but this is not 25, because r.m is p.m end
2.1.11.13 Extend Expressions
The extend expression consists of an base expression and a list of fields to extend it with:
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:
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:
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:
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.
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 ascendingNAME 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
‹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
By analogy with how ‹dot-expr› accesses normal fields,
‹get-bang-expr› accesses mutable fields —
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:
ex1!{x: 42} check: ex1!x is 42 end
2.1.11.23 Construction expressions
link(1, link(2, link(3, link(4, empty))))
[list: 1, 2, 3, 4]
‹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.
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), }
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 in 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›.