Higher-Order Functions in Unison
Why you must use map, reduce and filter instead of loops in the Unison programming language
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 range
functions, 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. foldRight
starts 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.