I have been thinking about making Haskell easier to learn. One big hurdle seems to be currying. So, I am starting to question whether the benefits of currying outweigh the drawbacks. In this post, I present what I think the two most important advantages of currying are, and why I think they might not be worth the cost.
Please let me know what you think, especially if you are a beginning Haskeller. I would love to hear about your experience of learning how currying works. And for the more proficient Haskellers: do you know any other advantages or do you see big problems with the things proposed in this post?
Partial application
The Haskell wiki page about currying lists this advantage:
[…] the curried form is usually more convenient because it allows partial application.
But I think currying is rather something that holds back better mechanisms for partial application. Currying only allows partial application in the order of the arguments. Some other languages allow you to partially apply function arguments in any order. For example you could indicate the missing arguments with an underscore:
> map (add 1 _) [1,2,3]
[2,3,4]
Or even numbered underscores:
> map (_2, _1) [(1,2), (3,4)]
[(2,1), (4,3)]
I feel like that has a much better UX and is more intuitive than currying.
Here is one other example from a stackoverflow answer about the benefits of currying:
incElems = map (+1)
--non-curried equivalent: incElems = (\elems -> map (\i -> (+) 1 i) elems)
I think that comment is rather disingenuous: operator sections do not require currying and this assumes the language puts absolutely no effort in partial application syntax. With my proposed syntax you would write it like this:
incElems = map (_ + 1) _
Or if you also add operator sections, which are now less necessary, you could write it with only one underscore:
incElems = map (+ 1) _
One question that I have skipped over is how the scope of the underscores is determined. You could say the scope ends at the first set of enclosing parentheses, then map (_ + 1) [1,2,3]
would work, but map ((_) + 1) [1,2,3]
would not work. I think that would be enough to completely subsume currying for partial application, but you could imagine more complicated scenarios where you might want the scope to be larger than just the enclosing parentheses. For that you could introduce a second set of parentheses, perhaps ((
and ))
which don’t capture the scope of the underscores, so this would be valid: map ( (( _ )) + 1) [1,2,3]
. Then this is much more powerful than currying.
Polymorphic functions
Currying allows you to “request” additional arguments from polymorphic functions.
An example that I have some experience with is foldr
. You can add an accumulator argument to the fold because of currying. For example sum can be implemented as follows:
sum :: [Integer] -> Integer
sum xs = foldr (\y k acc -> k (y + acc)) (\acc -> acc) xs
The type of foldr
is (a -> b -> b) -> b -> [a] -> b
so you wouldn’t expect to see three arguments as I did in the first argument of foldr (and indeed I think it is not intuitive unless you have much experience with it). However, you can instantiate the type variable b
to a function type c -> d
, or in the case above Integer -> Integer
.
Without currying we could write a separate function foldrAcc :: ((a, b -> c, b) -> c), b -> c, [a], b) -> c
. And we could write the accumulating sum function as follows:
sum xs = foldrAcc (\(y, k, acc) -> k (y + acc), \acc -> acc, xs, 0)
In fact, I only notice now that I forgot to pass the initial value 0 in the example above; did you catch that mistake? Also note that in this example I am using normal Haskell syntax, but the language could be change so that you wouldn’t have to write all the tuples explicitly.
This has the advantage of making it clear that something more complicated than a “simple” foldr is happening.
Edit: How could I forget the f <$> x <*> y <*> z
pattern. I think that is only really possible with currying. Perhaps using the !
-notation from Idris is a solution for this. Then you could write: f !x !y !z
which is even nicer than the Haskell version and again this is explicit special notation which doesn’t confuse beginners with complicated type signatures (although it is something you need to learn like do
-notation).
- very easy
- easy
- difficult
- very difficult
- I don’t remember
0 voters