Functions

Functions are also terms in Unison, so they're governed by the same conventions. A type signature is followed by a corresponding function implementation like this:

addOne : Nat -> Nat
addOne n1 =
  use Nat +
  n1 + 1

We know addOne is a function by the right arrow ->. When we see the arrow in a type signature, we might read the left side in human language as "takes in a" and the right side of the arrow as "returns a", as in, "addOne is a function which takes in a Nat and returns a Nat."

Great! But what if we need more than one argument to our function. addNums is a function which adds two numbers together to get a total.

addNums : Nat -> Nat -> Nat
addNums : Nat -> Nat -> Nat
addNums n1 n2 =
  use Nat +
  n1 + n2

In Unison type signatures, your function arguments are indicated to the left of each arrow ->, and the return type of the signature is the last type on the right of the arrow.

Unison function arguments are curried, so if we wanted, we could express addOne in terms of addNums by providing one argument to the function at a time.

addOneCurried : Nat -> Nat addOneCurried count = plusOne : Nat -> Nat plusOne = addNums 1 plusOne count addOneCurried 100
101

Function arguments are separated by spaces so calling a two argument function in Unison looks like:

addNums 4 5
9

When calling a multi-argument function, we can draw the implied parentheses for the order in which the function arguments are applied like this:

add3 : Nat -> Nat -> Nat -> Nat
add3 a b c = a + b + c

((add3 1) 2) 3

Function application starts with the leftmost argument--you can think of it as each argument being fed to successive functions from left to right. This might seem obvious, but later when signatures and their implementations get more complicated, an intuition for this becomes more important. You can always use parentheses to regroup expressions as needed.

📌

While we as developers give our functions and variables human readable names, to the Unison codebase, terms are not identified by their text based names. Unison terms are unique by their content. So if you add a term definition abc text = "123" ++ text to the ucm, and later write another term definition with a different name doReMi text = "123" ++ text you'll see that the terms can be referred to by either name.

⍟ These new definitions are ok to `add`:

  doReMi : Text -> Text
    (also named abc)

Block syntax

A block is a section of Unison code which is grouped together to organize smaller units of code. One of the many places you'll start a code block is after a term definition. We can introduce a code block via indentation or via a let block.

repeatNum : Nat -> Text
repeatNum num =
  text = Nat.toText num
  Text.repeat num text

In the example above, we've opted to use indentation to delimit the code block. Everything at the same indentation level is within the same block and therefore shares the same lexical scope. The exact same code with a let block looks like:

repeatNum : Nat -> Text
repeatNum num = let
  text = toText num
  Text.repeat num text

🪆 Blocks can be nested within other blocks, and terms in inner blocks can reference terms from their enclosing blocks.

nesting : [Text]
nesting =
  parent = "outer"
  inner1 = let
    child1 = "child1"
    inner2 = let
      child2 = "child2"
      [parent, child1, child2]
    inner2
  inner1

The last thing evaluated in a code block is the return value for that entire code block. Below we have a series of expressions whose values are ultimately discarded even though each line of the code block is actually executed.

myFunction : Text myFunction = use Nat + x = 1 + 1 y = "I am unreachable!" z = ?a "I am what is returned." myFunction
"I am what is returned."

The UCM will fail to compile a block which contains a sub-expression that is not used or bound to a variable and does not return Unit. That means the following function will fail to typecheck and cannot be added to the codebase because both 1 Nat.+ 1 and the doc fragment are not used elsewhere in the function.

invalidFunction : Text
invalidFunction =
  Debug.trace "I am ok because I return unit!" ()
  1 + 1
  {{I am an unused expression}}
  "returned value"

Check out the full language reference for block syntax for more details.

Lambda syntax

Lambdas, or anonymous functions are unnamed functions which are defined without going through the entire term declaration process. They're typically used when the function doesn't need to be reused elsewhere in the program or when the function itself is very simple. They're often found as the arguments to higher order functions.

While we could define the function below as a separate term to pass into List.map, it's much simpler to define in-line.

use Nat + a = [1, 2, 3, 4, 5] jsonschema.lib.base.data.List.map (elem -> elem + 1) a

The section elem -> elem + 1 is the lambda. Anything to the left of the arrow represents the parameters to the function and the expression to the right is the function body.

Anonymous functions can be simple, like the elem -> elem + 1 example above, or they can be more complicated, multi-line expressions when combined with the block syntax.

jsonschema.lib.base.data.List.map (i -> let use Nat + x = i + 1 y = x + 1 z = y + 1 z) [1, 2, 3]

You can do simple pattern decomposition in lambdas with the cases syntax.

This can be useful when you want to destructure a tuple or other data types.

A lambda with two arguments is represented by separating the function arguments with spaces.

You can also choose to ignore the argument to the lambda altogether with the underscore symbol, as in the following function:

a = [1, 2, 3, 4, 5]
List.map (_ -> 10) a