In language design, there is a tradeoff to make between implicit folding and implicit currying.
Let’s take the + function as an example: it is a function which takes 2 arguments and returns 1 result. What if we passed 3 or more arguments to it instead? What if we passed less than 2? In principle, it doesn’t mean anything. So we have 3 options:
1- If the user passes more or less than 2 arguments, raise an error. We have left operand an right operand. More or less than 2 arguments has no meaning, what do you even mean by that?
This is the path that Python takes:
>>> from operator import add >>> add(2,3) 5 >>> add(1,2,3) # oops! expected 2 arguments, got 3 >>> add(1) # oops! expected 2 arguments, got 1
2- If the user passes any number of arguments, add them all. Thus, you are performing an implicit fold.
This is the path that Lisp takes:
(+ 1 2 3) ; => 6 (+ 1) ; => 1
3- If the user passes less than the required arguments (2 in this case), partially apply the function to the supplied arguments. Thus, the + is implicitly curried.
This is the path that Haskell takes:
(+ 7) -- a function of 1 argument that adds 7 to the number you pass as argument
PROS & CONS
By defining the meaning of passing fewer/more than required number of arguments to a function, you are able to squeeze more meaning into your language, potentially resulting in a more concise and small language. By choosing option number 1 (raising an error), you miss that opportunity. A pro might be that eventually there is going to be a newbie that passes a wrong number of arguments by accident, and you want to let them know.
A pro of option 2 (implicit folding) is that you can pass any number of arguments and it always has a meaning. On the other hand, option number 3 (implicit currying) only has a meaning if you pass lower than required arguments (if you pass more it is an error).
For functions like + and *, this is very useful, you will use folding with these functions a lot. In general, it is useful for functions of signature a→b→a (with + and * being a particular case, with signatures a→a→a ). As a cons, not all functions have these signatures and it is unclear how we should generalize folding to these other functions.
Option 3 is easily generalizable to any function, unlike option 2. If you think in terms of functions and function composition (instead of function application), this will be very handy, you will be creating partially applied functions all the time.
This won’t be a substitute for general partial application (what if you wanted to partially apply a function on its second argument?), but still will cover many use cases.
My personal favorite is option 3, implicit currying. What about you?
4 thoughts on “Folding vs. Currying”
Interesting. What happens when we give more than required argument in case of currying?
That’s an interesting question. It’s an error. How do you think we could generalize the currying rule to give a meaning to this case as well?
Implicit currying works better in statically typed language like Haskell, where the compiler will tell you if you ever ‘accidentally’ curried a function instead of passing all the arguments like you meant to. Whereas in Python this would be more like a potential gotcha scenario.
I’m all for currying, but I think in Python it would be better done explicitly, e.g. with something like a `curry(f, *args) ` function call.
I think you should consider the significance of position and ordering.
In your examples, you’re using operations that are commutative. It’s true: with +, you can put arguments in any order, and you can look at it as a binary, or unary (plus-k), trinary, or even n-ary operation, and you get results that are all consistent with each other.
But let’s try the next most complicated function, ÷. So say I pass Div(42). Is this a function that divides by 42, or a function that divides 42 by something else? It’s common to fill in position parameters in a natural order, so I guess it’s the latter; but let’s be honest, how often do I want that? Setting the divisor is probably the way more common operation. Either you’re saying people have to order their parameters in reverse order of how often they want to omit them (which might be backward of how they’re normally understood), or you’ve made a feature that’s only occasionally useful.
Or returning to the real world, let’s say I have some business logic: Transfer(AccountName, From, StockTicketSymbol)
So I have Transfer(“Aaron”) .. a function that transfers assets to me, I guess I can’t argue with that. But if I write Transfer(“Aaron”, “Matias”) I suspect you’re going to get a bit more nervous.. what exactly is this going to do… but now try to decode this: Transfer(“Aaron”, “Matias”, “MSFT”, “MSFT”, “MSFT)… what does this do? Are you giving me a lot of Microsoft stock? Or perhaps you’re giving me something, and then Microsoft is giving something to itself.. or perhaps you give some stock to me and then I give it all to Microsoft, as part of some big buyback program?
If we’re talking about some mathematical notation or a specialized DSL, sure, do whatever makes it the most consistent and concise. But if we’re talking about software engineering, it really can’t be like this. Computer code tends to be voluminous and relatively hard to read, way harder to read than write. The fact these alternatives (implicit currying and folding) are fundamentally attached to position is going to limit the places they’re useful to places the ordering just happens to work. Every now and then as a programmer, you’ll be like YES, I found a place I can use implicit folding! But everyone reading the code will have to stare at it to understand what it means and you really aren’t helping anyone…
But I bet you could use it to write a super-concise one-line “quicksort”…