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:
The NAME of the data definition
NAME, for each variant of the data definition
is-NAME, for 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:
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:
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
‹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.7.6 Newtype Declarations
‹newtype-stmt› ‹newtype-decl› ‹newtype-decl› newtype NAME as NAME
newtype MytypeBrander as MyType