On this page:
2.1.7.1 Let Declarations
2.1.7.2 Function Declaration Expressions
2.1.7.3 Data Declarations
2.1.7.4 Variable Declarations
2.1.7.5 Type Declarations
2.1.7.6 Newtype Declarations
2.1.7 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› is an exception, and can appear only at the top level.

‹stmt›: ‹let-decl› | ‹fun-decl› | ‹data-decl› | ‹var-decl› | ‹type-stmt› | ‹newtype-stmt›

2.1.7.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.7.2 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›]

A function expression is syntactic sugar for a let and an anonymous function expression for non-recursive case. The statement:

"fun" NAME ty-params args return-ann ":"

  doc-string

  block

  where-clause

"end"

is equivalent to

NAME "=" "lam" ty-params args return-ann ":"

  doc-string

  block

"end"

With the where-clause registered in check mode. Concretely:

fun f(x, y): x + y end

is equivalent to

f = lam(x, y): x + y end

See the documentation for lam-exprs for an explanation of arguments’ and annotations’ behavior, as well as doc-strings.

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

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 BTree is a detector for values created from this data definition, and can be used as an annotation to check for values created by the constructors of BTree. BTree returns true when provided values created by node or leaf, but no others.

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 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))) BTree(a-btree) is true BTree("not-a-tree") is false 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 4 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 3 leaf(0).size() is 1 leaf(1).increment() is leaf(2) a-btree.increment() # raises error: field increment not found. end

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

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

Examples:

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

Examples:

newtype MytypeBrander as MyType

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