Skip to main content

Let declarations and functions

Let declarations bind identifiers to values or functions.

For instance:

let
a = 1,
f(v: int) = v * 2
in
a + f(1) // Result is 3

This let declaration binds the identifier a to the value 1, and the identifier f to a function that receives a int parameter and returns its double.

The in part of the declaration contains an expression. The final value of the let declaration is the value of the expression in the in (in the example the value is 3). The in has access to all the identifiers defined in the declaration. In the example above, the in has access to a and f. The identifier f, however, only has access to the identifier a, since in the example that is the only identifier defined before itself.

There are three types of declarations in the let part:

  • assign an identifier to a value, e.g. a = 1. The identifier can optionally be typed as in a: int = 1.
  • assign an identifier to a function, e.g. f(v: int) = v * 2. The return type of the function can optionally be typed as in f(v: int): int = v * 2
  • assign an identifier to a recursive function, e.g. rec fact(v: int): int = if (v <= 1) then 1 else v * fact(v - 1). Recursive functions must be prefixed by the keyword rec. In addition, recursive functions require the user to specify the return type.

Let declarations create a new scope. This means that within a let declaration you can assign an identifier with the same name as an existing identifier to a new value. This does not overwrite the old identifier: instead it defines a new identifier that only exists in the scope of the new let declaration. For instance:

let
a = 1, // defines an identifier called 'a' that is bound to the value 1
b = // defines an identifier called 'b' whose value is the value of
// the 'in' of its inner let below.
let // a new let declaration, which creates a new scope.
a = a + 1 // a new identifier called 'a' is bound to the value
// of the old identifier called 'a' (value of 1) plus 1
// with the resulting value of 2.
in
a // refers to the new identifier 'a' with value 2.
// therefore, the value of 'b' is 2.
in
a + b // 'a' is an identifier with value 1, 'b' is an identifier with value 2.
// therefore, the result is 3.

Functions

Functions can be defined in let declarations as shown above.

However, it is also possible to define lambda functions, i.e. anonymous functions. These are particularly useful when calling built-in libraries.

For instance, the following expression creates a lambda function that receives an int.

(v: int) -> v + 1

Lamda functions are particularly useful to create functions with more powerful behaviours. For instance, here is a function that says "Hi {name}>!" but first calls a user-defined cleaning function that cleans the name.

let say(name: string, cleaner: (string) -> string) = "Hi " + cleaner(name) + "!"

This could be used as e.g.:

let
say(name: string, cleaner: (string) -> string) = "Hi " + cleaner(name) + "!"
in
say("John", s -> String.Upper(s)) // Result is "Hi JOHN!"

Optional parameters

Functions can have optional parameters, e.g.:

let f(x: int, y: int = 2) = x > y
in f(1) // Same as f(1, 2)

Difference between program declarations and let declarations

Program declarations and let declarations serve different purposes and have different capabilities.

Program declarations are used to defined the entrypoint of a program. Program declarations can only be declared at the top-level of a program, i.e. cannot be nested inside other declarations. Program declarations cannot do recursive calls.

Let declarations can be used anywhere, including nested inside other let declarations. Let declarations can also be used to build recursive functions.

Program declarations are normally only used to define the entrypoints for a program, e.g. the declaration main that a data API calls, or the public functions available in a user-defined library. Let declarations are the normal construct to create identifiers and define functions as to reuse code in a Snapi script.