Reading Unison type signatures

This doc is for folks who come from languages with non-Haskell like ways of expressing type signatures. The Haskell folks might want to skip to the section where we talk about ability requirements.

Basic type signatures

We've seen basic type signatures in Unison before, when learning about defining functions. As a brief review, the signature for multiply below takes two Nat's as parameters and returns a Nat.

multiply3 : Nat -> Nat -> Nat -> Nat
multiply3 a b c = a * b * c

Because functions are curried in Unison, the implied parentheses in the type signatures of a multi-parameter function are drawn like this:

multiply3 : Nat -> (Nat -> (Nat -> Nat))
multiply3 a b c = a * b * c

Notice how they all nest towards the right? The function arrow -> is right-associative. That means multiply3 is a function, which when given a Nat, will return a function of (Nat -> (Nat -> Nat)).

Remember when we said that function application starts at the leftmost element? This is the corollary to that in type signatures.

When you want to define a function with a different implied argument order than this, like when an argument is itself a function, use parentheses in your type signature.

use Nat * multiplyF : (Nat -> Nat) -> Nat -> Nat -> Nat multiplyF f a b = f (a * b) multiplyF Nat.increment 0 1
โงจ
1

Without the parentheses to designate the first argument as a function, this function would have four arguments of type Nat.

Type parameters

Thus far, we've been defining functions that have concrete types like Text or Bytes as their arguments. Unison functions can also use type parameters to represent functions that operate over many different types. We call these functions polymorphic. Type variables are introduced in a type signature with lower case letters.

This is the implementation of Function.id:

Function.id : a -> a
Function.id : a -> a
Function.id a = a

Function.id contains a type parameter a because any other type can be assigned to that type variable and it will return a value of that specific type. a can be a bound to a Text when called with a text argument, as in Function.id "hi", or a can be a List of Nat when called with a value like Function.id [5, 4, 3], etc, etc.

Ability requirements

Let's take a closer look at the signature for distributed.lib.base.data.List.map. In addition to the usual arguments, (a -> b) and [a], you'll see {๐•–} to the right side of the function arrows.

distributed.lib.base.data.List.map : (a ->{๐•–} b) -> [a] ->{๐•–} [b]

The {e} is a generic ability requirement, we'll be talking about abilities in depth later, but for the purpose of reading Unison type signatures, the {e} is a type variable which stands in for the set of possible effects a function can perform.

All together now!

Take a look at the signature for List.zipWith, we'll parse it together.

List.zipWith : (a ->{๐•–} b ->{๐•–} c) -> [a] -> [b] ->{๐•–} [c]

Here's some information we can glean from just the type signature:

  • List.zipWith is polymorphic, we know this from the lowercase type parameters
  • It takes 3 arguments, the parentheses indicate the first is a function, the second is a list of type a and the third is a list of type b
  • The "zipping" function provided as an argument has two parameters itself of type a and b
  • The "zipping" function is permitted to perform effects, represented by the {e}
  • The return type of List.zipWith is a list of type c--in the process of returning that value, the overall function can perform effects.

Where to next:

๐ŸŒŸ Special operators for function application

๐Ÿ“š Scoping rules for type variables

๐Ÿ“š Abilities in functions signatures