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 aboutability requirements.

Basic type signatures

We've seen basic type signatures in Unison before, when learning aboutdefining functions.As a brief review, the signature formultiplybelow takes twoNat'sas parameters and returns aNat.

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

Because functions arecurriedin 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->isright-associative.That meansmultiply3is a function, which when given aNat,will return afunctionof(Nat -> (Nat -> Nat)).

Remember when we said thatfunction applicationstarts 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.

multiplyF : (Nat -> Nat) -> Nat -> Nat -> Nat multiplyF f a b = use Nat * 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 typeNat.

Type parameters

Thus far, we've been defining functions that have concrete types likeTextorBytesas 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 withlower caseletters.

This is the implementation ofbase.id:

base.id : a -> a
base.id : a -> a
base.id a = a

base.idcontains a type parameterabecauseany other typecan be assigned to that type variable and it will return a value of that specific type.acan be a bound to aTextwhen called with a text argument, as inbase.id "hi",oracan be aListofNatwhen called with a value likebase.id [5, 4, 3],etc, etc.

Ability requirements

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

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

The{e}is a generic ability requirement, we'll be talking aboutabilities 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 forList.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.zipWithis 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 typeaand the third is a list of typeb
  • The "zipping" function provided as an argument has two parameters itself of typeaandb
  • The "zipping" function is permitted to perform effects, represented by the{e}
  • The return type ofList.zipWithis a list of typec--inthe 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