On this page: Let Declarations Recursive Let Declarations Function Declaration Expressions Scope Where blocks Syntactic sugar Data Declarations Variable Declarations Type Declarations Newtype Declarations
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› 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 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 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.) 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. 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. Syntactic sugar

Function declarations are not a primitive concept in the language. Instead, they can be thought of as an idiomatic declaration of a recursively-scoped let binding to a lambda expression. That is, the following two definitions are equivalent:

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. 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:

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 Variable Declarations

Variable declarations look like let bindings, but with an extra var keyword in the beginning:

‹var-decl›: var ‹binding› = ‹expr›

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. Type Declarations

Pyret provides two means of defining new type names.

‹type-stmt›: type ‹type-decl› ‹type-decl›: NAME ‹ty-params› = ‹ann›

A ‹type-stmt› declares an alias to an existing type. This allows for creating convenient names for types, especially when type parameters are involved.


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> Newtype Declarations

By contrast, sometimes we need to declare brand-new types, that are not easily describable using ‹data-decl› or other existing types. (For one common example, we might want to build an object-oriented type that encapsulates details of its internals.) To do that we need to specify both a static name to use as annotations to describe our data, and a dynamic brand to mark the data and ensure that we can recognize it again when we see it.

‹newtype-stmt›: ‹newtype-decl› ‹newtype-decl›: newtype NAME as NAME

When we write


newtype MytypeBrander as MyType

we define both of these components. See Brands for more information about branders.