Erik Explores

Erik Explores

Languages

Higher-Order Functions in Unison

Why you must use map, reduce and filter instead of loops in the Unison programming language

Erik Engheim's avatar
Erik Engheim
Jan 20, 2023
∙ Paid
Share

Because Unison is a functional language, and you don't have regular loops, you will have to get used to higher-order functions. Many of us are accustomed to functions such as map, reduce and filter. Unison has the same style of higher-order functions, but with a minor twist: Instead of reduce we have fold, leftFold and rightFold depending on how you want to reduce the values in a collection.

Because arguments in Unison, like in Haskell, are separated by space rather than coma, you occasionally have to assist the parser by clarifying using parenthesis what constitutes as a single argument or function. For instance, we can add to numbers on infix form 3 + 4 or prefix form (+) 3 4. Notice the need to put parenthesis around the + operator to indicate that we are treating it as a function rather than writing a positive number.

Partial function application is also possible: Instead of writing x -> x + 1 you could write (+)1 to produce a single argument function which adds 1 to the input.

Using the Map Function

Below are two code examples of using the map higher-order function to add a 1 to every element in the input collection. In the first example, we use an anonymous function x -> x + 1 to perform the addition. In the second example, we use partial application to create a function which add a one to its input argument.

> map (x -> x + 1) [1,2,3]
  ⧩
  [2, 3, 4]
> map ((+)1) [1,2,3]
  ⧩
  [2, 3, 4]

Furthermore, it is useful to know that you can use ranges as input instead of lists.

> List.map ((+)10) (range 1 5)
  ⧩
  [11, 12, 13, 14]

In this case, we need to qualify the map function and point out that we want the map function from the List namespace. The reason is that Unison in this case cannot guess which map function we want to call. Why could Unison figure it out in the other examples, though? Because [1, 2, 3] always evaluates to a List object, while there are many rangefunctions, which each give different output.

The Unison type-checker ends up in a catch-22 situation not knowing which map function to pick because it doesn't know the type produced by (range 1 5) because it doesn't know which range function is called. It cannot figure which range function to call because it doesn't know which map function was called. You need to tell Unison about one of them, so it can deduce the other. Hence, it would have worked equally well to write:

map ((+)10) (List.range 1 5)

Using the Filter Function

The filter function is very similar to map. In the example below, we pick the values larger than four.

> filter (x -> x > 4) (List.range 1 10)
  ⧩
  [5, 6, 7, 8, 9]

With the Julia programming language, for instance, this would be written as:

filter(x -> x > 4, 1:9)

It is interesting to note that if we do partial function application, Julia and Unison use different function arguments. You can to use the less than comparison in Unison.

> filter ((<)4) (List.range 1 10)
  ⧩
  [5, 6, 7, 8, 9]

While in Julia, you would use the greater than operator as in the anonymous function example:

julia> filter(>(4), 1:9)
5-element Vector{Int64}:
 5
 6
 7
 8
 9

One thing that was useful for me to be aware of is that Unison is a quite simple language. There isn't a lot of syntax sugar to know. For instance, in Julia, you have the do-end form for anonymous functions spanning multiple lines. These two Julia forms are equivalent.

filter(x -> x > 4, 1:9)

filter(1:9) do x 
   x > 4
end

In Unison, you would just need to use parenthesis. Here is an example of a multiline function. There is no special syntax to deal with it.

List.map (i ->
    let
        x = i + 1
        y = x + 1
        z = y + 1
        z) 
    [1, 2, 3]

There is, in fact, a do form in Unison, but it is used to create anonymous functions without arguments.

Using the Fold Functions

In Unison, when we take a collection of values and reduce it to a single value, we refer to this as fold. We can do both left and right fold. In the following example, we use the string concatenation operator ++ to join strings. Use foldLeft to begin combining values from the left. foldRightstarts joining string from the right-most side.

> foldLeft (++) "x" ["a", "b", "c"]
  ⧩
  "xabc"

> foldRight (++) "x" ["a", "b", "c"]
  ⧩
  "abcx"

A common usage of fold is doing things like calculating the sum, product, or factorial. For instance, we could implement a sum function using fold. Notice the second argument is 0. That is the initial value we add the first element to.

sum: [Nat] -> Nat
sum xs = foldLeft (+) 0 xs

You could also implement the factorial of a number n using fold. Notice we need to have the starting value set to 1 because if we multiply with 0 all input ranges would end up as zero.

factorial : Nat -> Nat
factorial n = foldLeft (*) 1 (Nat.rangeClosed 1 n)

Here is an example of using these two functions in Unison:

> sum [2, 3, 5]
  ⧩
  10

> factorial 4
  ⧩
  24

The factorial of 4 is 4 * 3 * 2 * 1, which results in 24.

Implementing Higher-Order Functions Yourself

I find that implementing functions often helps to develop an understanding of them. There are of course existing implementation in the standard library we can look up, but let's make a simple minimal implementation for educational purposes of some of these higher-order functions.

Keep reading with a 7-day free trial

Subscribe to Erik Explores to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Erik Engheim
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture