2.1.8 Declarations
There are a number of forms that can only appear as statements in blocks (rather than anywhere an expression can appear). Several of these are declarations, which define new names within their enclosing block. ‹data-decl› and ‹contract› are exceptions, and can appear only at the top level.
‹stmt› ‹let-decl›‹rec-decl›‹fun-decl›‹var-decl›‹type-stmt›‹newtype-stmt› ‹data-decl›‹contract›
2.1.8.1 Let Declarations
Let declarations are written with an equals sign:
‹let-decl› ‹binding› = ‹binop-expr›
A let statement causes the name in the binding to be put in scope in the current block, and upon evaluation sets the value to be the result of evaluating the binop-expr. The resulting binding cannot be changed via an ‹assign-stmt›, and cannot be shadowed by other bindings within the same or nested scopes:
x = 5 x := 10 # Error: x is not assignable
x = 5 x = 10 # Error: x defined twice
x = 5 fun f(): x = 10 x end # Error: can't use the name x in two nested scopes
fun f(): x = 10 x end fun g(): x = 22 x end # Not an error: x is used in two scopes that are not nested
A binding also has a case with tuples, where several names can be given in a binding which can then be assigned to values in a tuple.
{x;y;z} = {"he" + "llo"; true; 42} x = "hi" #Error: x defined twice
{x;y;z} = {10; 12} #Error: The number of names must match the length of the tuple
2.1.8.2 Recursive Let Declarations
‹rec-decl› rec ‹binding› = ‹binop-expr›
A recursive let-binding is just like a normal let-binding, except that the name being defined is in scope in the definition itself, rather than only after it. That is:
countdown-bad = lam(n): if n == 0: true else: countdown-bad(n - 1) # countdown-bad is not in scope end end # countdown-bad is in scope here
rec countdown-good = # countdown-good is in scope here, because of the 'rec' lam(n): if n == 0: true else: countdown-good(n - 1) # so this call is fine end end # countdown-good is in scope here
2.1.8.3 Function Declaration Expressions
Function declarations have a number of pieces:
‹fun-decl› fun NAME ‹fun-header› block : ‹doc-string› ‹block› ‹where-clause› end ‹fun-header› ‹ty-params› ‹args› ‹return-ann› ‹ty-params› < ‹list-ty-param› NAME > ‹list-ty-param› NAME , ‹args› ( ‹list-arg-elt› ‹binding› RPAREN ‹list-arg-elt› ‹binding› , ‹return-ann› -> ‹ann› ‹doc-string› doc: STRING ‹where-clause› where: ‹block›
Function declarations are statements used to define functions with a given name, parameters and signature, optional documentation, body, and optional tests. For example, the following code:
fun is-even(n): num-modulo(n, 2) == 0 end
defines a minimal function, with just its name, parameter names, and body. A more complete example:
fun fact(n :: NumNonNegative) -> Number: doc: "Returns n! = 1 * 2 * 3 ... * n" if n == 0: 1 else: n * fact(n - 1) end where: fact(1) is 1 fact(5) is 120 end
defines a recursive function with a fully-annotated signature (the types of its parameter and return value are specified), documents the purpose of the function with a doc-string, and includes a where-block definine some simple tests of the function.
Function declarations are statements that can only appear either at the top level of a file, or within a block scope. (This is commonly used for defining local helper functions within another one.)
2.1.8.3.1 Scope
Once defined, the name of the function is visible for the remainder of the scope in which it is defined. Additionall, the function is in scope within its own body, to enable recursive functions like fact above:
fun outer-function(a, b, c): ... # outer-function is in scope here # as are parameters a, b, and c ... fun inner-helper(d, e, f): ... # inner-helper is in scope here, # as are parameters d, e, and f # and also outer-helper, a, b and c ... end ... # outer-function, a, b, and c are in scope here, # and so is inner-helper, but *not* d, e or f ... end
As with all Pyret identifiers, these function and parameter names cannot be mutated, and they cannot be redefined while in scope unless they are explicitly shadowed.
2.1.8.3.2 Where blocks
If a function defines a where: block, it can incorporate unit tests directly inline with its definition. This helps to document the code in terms of executable examples. Additionally, whenever the function declaration is executed, the tests will be executed as well. This helps ensure that the code and tests don’t fall out of synch with each other. (The clarification about "whenever the declaration is executed" allows writing tests for nested functions that might rely on the parameters of their containing function: in the example above, inner-helper might have a test case that relied on the parameters a, b or c from the surrounding call to outer-function.) See the documentation for check: and where: blocks for more details.
2.1.8.3.3 Syntactic sugar
fun fact(n): if n == 1: 1 else: n * fact(n - 1) end end
rec fact = lam(n): if n == 1: 1 else n * fact(n - 1) end end
See the documentation for more information about ‹lam-expr›s, and also see ‹rec-decl›s above for more information about recursive bindings.
2.1.8.4 Data Declarations
Data declarations define a number of related functions for creating and manipulating a data type. Their grammar is:
‹data-decl› data NAME ‹ty-params› : ‹data-variant› ‹data-sharing› ‹where-clause› end ‹data-variant› | NAME ‹variant-members› ‹data-with›| NAME ‹data-with› ‹variant-members› ( ‹list-variant-member› ‹variant-member› ) ‹list-variant-member› ‹variant-member› , ‹variant-member› ref ‹binding› ‹data-with› with: ‹fields› ‹data-sharing› sharing: ‹fields›
A ‹data-decl› causes a number of new names to be bound in the scope of the block it is defined in:
The NAME of the data definition
NAME, for each variant of the data definition
is-NAME, for the data definition and each variant of the data definition
For example, in this data definition:
data BTree: | node(value :: Number, left :: BTree, right :: BTree) | leaf(value :: Number) end
These names are defined, with the given types:
is-BTree :: (Any -> Bool) node :: (Number, BTree, BTree -> BTree) is-node :: (Any -> Bool) leaf :: (Number -> BTree) is-leaf :: (Any -> Bool)
We call node and leaf the constructors of BTree, and they construct values with the named fields. They will refuse to create the value if fields that don’t match the annotations are given. As with all annotations, they are optional. The constructed values can have their fields accessed with dot expressions.
The function is-BTree is a detector for values created from this data definition. is-BTree returns true when provided values created by node or leaf, but no others. BTree can be used as an annotation to check for values created by the constructors of BTree.
The functions is-node and is-leaf are detectors for the values created by the individual constructors: is-node will only return true for values created by calling node, and is-leaf correspondingly for leaf.
Here is a longer example of the behavior of detectors, field access, and constructors:
data BTree: | node(value :: Number, left :: BTree, right :: BTree) | leaf(value :: Number) where: a-btree = node(1, leaf(2), node(3, leaf(4), leaf(5))) is-BTree(a-btree) is true is-BTree("not-a-tree") is false is-BTree(leaf(5)) is true is-leaf(leaf(5)) is true is-leaf(a-btree) is false is-leaf("not-a-tree") is false is-node(leaf(5)) is false is-node(a-btree) is true is-node("not-a-tree") is false a-btree.value is 1 a-btree.left.value is 2 a-btree.right.value is 3 a-btree.right.left.value is 4 a-btree.right.right.value is 5 end
A data definition can also define, for each instance as well as for the data definition as a whole, a set of methods. This is done with the keywords with: and sharing:. Methods defined on a variant via with: will only be defined for instances of that variant, while methods defined on the union of all the variants with sharing: are defined on all instances. For example:
data BTree: | node(value :: Number, left :: BTree, right :: BTree) with: method size(self): 1 + self.left.size() + self.right.size() end | leaf(value :: Number) with: method size(self): 1 end, method increment(self): leaf(self.value + 1) end sharing: method values-equal(self, other): self.value == other.value end where: a-btree = node(1, leaf(2), node(3, leaf(4), leaf(2))) a-btree.values-equal(leaf(1)) is true leaf(1).values-equal(a-btree) is true a-btree.size() is 5 leaf(0).size() is 1 leaf(1).increment() is leaf(2) a-btree.increment() # raises error: field increment not found. end
When you have a single kind of datum in a data definition, instead of writing:
data Point: | pt(x, y) end
You can drop the | and simply write:
data Point: pt(x, y) end
2.1.8.5 Variable Declarations
Variable declarations look like let bindings, but with an extra var keyword in the beginning:
A var expression creates a new assignable variable in the current scope, initialized to the value of the expression on the right of the =. It can be accessed simply by using the variable name, which will always evaluate to the last-assigned value of the variable. Assignment statements can be used to update the value stored in an assignable variable.
If the binding contains an annotation, the initial value is checked against the annotation, and all assignment statements to the variable check the annotation on the new value before updating.
2.1.8.6 Type Declarations
‹type-stmt› type ‹type-decl› ‹type-decl› NAME ‹ty-params› = ‹ann›
type Predicate<a> = (a -> Boolean) # Now we can use this alias to make the signatures for other functions more readable: fun filter<a>(pred :: Predicate<a>, elts :: List<a>) -> List<a>: ... end # We can specialize types, too: type NumList = List<Number> type StrPred = Predicate<String>
2.1.8.7 Newtype Declarations
‹newtype-stmt› ‹newtype-decl› ‹newtype-decl› newtype NAME as NAME
newtype MytypeBrander as MyType