Polyvariadic functions can be useful to improve the user experience when writing an EDSL in Haskell. Instead of making your users write doSomething ["With", "these", "arguments"]
, you could have them write doSomething "With" "these" "arguments"
.
Your intuition will probably tell you that making a polyvariadic function will be unsafe, but Haskell allows you to make it safe. Of course it is not as easy as in Python, but then again, polyvariadic functions in Python will never be type safe.
Background: Currying
The concept of currying needs to be very clear before we can dive into polyvariadic functions.
A function with two arguments: f :: a -> b -> c
really only has one argument. The ->
operator is right-associative, so we can rewrite this type signature as f :: a -> (b -> c)
. This means that f
takes a value x :: a
and gives you a function f x :: b -> c
.
When we write down functions with 'multiple arguments', the parentheses are usually omitted, but in reality f x y z
is parsed as ((f x) y) z
.
The problem of polyvariadic functions
What we want to build is a function of type a -> (a -> (a -> (... -> b )))
where the length of this chain of ->
operators is determined by how many arguments are given to the function.
Meanwhile, we would like to implement this function as if it were of type [a] -> b
.
Keep in mind that, with what I am about to show you, it is also possible to implement more complicated polyvariadic functions (for example with different types as arguments).
A first draft of a solution
The trick that we will employ to make this happen will involve a typeclass. Let's say we would like to build the polyvariadic equivalent of a function sumOf :: [Int] -> Int
. We would then write a typeclass SumArgs
as follows:
class SumArgs a where
sumArgs :: [Int] -> a
Instances of this class, 'know how to make a value of their type from a list of integers'.
Next, we instantiate Int -> r
where SumArgs r
. (We will need {-# LANGUAGE FlexibleInstances #-}
and {-# LANGUAGE FlexibleContexts #-}
.)
instance (SumArgs r) => SumArgs (Int -> r) where sumArgs is i = sumArgs (i:is)
This may look like black magic at first. I will explain how this works right now. Why it is useful should become clear later.
Note that the sumArgs
function in the second instance is of type SumArgs r => [Int] -> (Int -> r)
. That is, given a list of integers is :: [Int]
and another integer i :: Int
, we have to build a value of type r
where r
has a sumArgs
instance. r
, having a sumArgs
instance, can be built using sumArgs :: SumArgs r => [Int] -> r
if we have a list of integers. We will build this list by adding i :: Int
to the existing list is :: [Int]
.
Now we are ready to add the functionality that will use sumOf :: [Int] -> Int
in a polyvariadic context. We will do this with another instance:
instance SumArgs Int where
= sumOf sumArgs
This instance explains how to build the result (of type Int
) from a list of Int
arguments.
We could now use sumArgs as let f = 5 :: Int in sumArgs [] f f :: Int
and we would already have a polyvariadic function, but the empty list n there is still ugly.
The final piece of the puzzle is to abbreviate this:
sumOf' :: SumArgs args => args
= sumArgs [] sumOf'
There we have it, a type safe polyvariadic function in Haskell.
*> let f = 5 :: Int
*> let g = 4 :: Int
*> sumOf' f g :: Int
9
*> sumOf' f g f f g :: Int
23
Under the hood of the black magic
If you did not already get how this works, here is an example.
sumOf' f g
is declared to be of type Int
, which means we can write it as sumArgs [] f g :: Int
.
If we add the parentheses in the right places, we get ((sumArgs []) f) g :: Int
. Now we will examine what Haskell's type inference does:
Peeling off one layer, Haskell figures out that
(sumArgs []) f
must be of some typec -> Int
whereg
is of that typec
.Peeling off the next layer, Haskell figures out that
sumArgs []
must be of some typeb -> c -> Int
wheref
is of that typeb
.It knows that
sumArgs
is of type[Int] -> a
.It identifies
a
withb -> c -> Int
to come to the conclusion thatsumArgs
is of type[Int] -> b -> c -> Int
whereb
andc
are the types off
andg
, respectively.Because it knows that
f
andg
are both of typeInt
, it specializes the type ofsumArgs
to[Int] -> Int -> Int -> Int
, which is exactly what we want.
The reason that sumArgs :: [Int] -> Int -> Int -> Int
is a valid type signature, is because Int -> Int -> Int
has an instance of SumArgs
. Int -> Int -> Int
has an instance of SumArgs because Int -> Int
does. Finally, Int -> Int
has an instance of SumArgs because Int
does.
Backward compatibility
I motivated polyvariadic functions by arguing that they look nice in EDSLs. If you are interested in improving the user experience of your EDSL, chances are that you have already implemented it. This means that there is probably already a bunch of code that uses sumOf :: [Int] -> Int
directly. Now, there is a way to change the type of sumOf
to SumArgs args => a
without breaking any of your users' code and it is by adding one more instance to the above code:
instance (SumArgs r) => SumArgs ([Int] -> r) where
= sumArgs (n ++ is) sumArgs is n
Now the old code will still work as expected:
*> sumOf' [f, g] :: Int
9
As a nice bonus, the following now also works:
:: Int
sumOf' f g [f, g]18