Prior to this article, I didn't think of currying as being something a person could be "for" or "against." It just is. The fact that a function of multiple inputs can be equivalently thought of as a function of a tuple can be equivalently thought of as a composite of single-input functions that return functions is about cognition, and understanding structure, not code syntax.
A case against currying
121–130 of 135 posts
Re: A case against currying
#122I'd got a step further and say that in business software, named parameters are preferable for all but the smallest functions. Using curried OR tuple arg lists requires remembering the name of an argument by its position. This saves room on the screen but is mental overhead. The fact is that arguments do always have names anyway and you always have to know what they are.
I want to agree, but there is the tension that in business code, what you pass as arguments is very often already named like the parameter, so having to indicate the parameter name in the call leads to a lot of redundancy. And if you’re using domain types judiciously, the types are typically also different, hence (in a statically-typed language) there is already a reduced risk of passing the wrong parameter. Maybe th…
fn compute_thing(cost: whatever, num_widgets: whatever) -> Whatever;
let cost = …;
let num_widgets = …;
let result = compute_thing(num_widgets, cost);
(This can by most any language including Haskell or Lean, with slightly different syntax.)One can prevent this very verbosely with the Builder pattern. Or one can use named parameters in languages that support them.
An interesting analogue is tensor math. In Einstein’s work, there were generally four dimensions and you probably wouldn’t lose track of which letter was which. In linear algebra, at least at the high school or early undergrad level, there are usually vectors and tensors and, well, that’s it. But in data crunching or modern ML, tensors have all kinds of cool axes, and for some reason we usually just identify them by which slot they are in the order that they happen to be in in the input tensor. Some people try to creatively make this “type safe” by specializing on the length of the dimension, which is an incomplete solution at best. I would love to see adoption of some solution that gives these things explicit names and does not ever guess which axis is being referenced.
(I find 95% of ML code and a respectable fraction of papers and descriptions to be locally incomprehensible because you need to look somewhere else to figure out what on Earth A • B' actually means.
Re: A case against currying
#123 length = foldr (+) 0 . map (const 1)
length2d = foldr (+) 0 . map length
-- and the proposed syntax the author calls more readable:
length = foldr((+), 0, $) . map(const(1), $)
length2d = foldr((+), 0, $) . map(length, $)
But I don't understand. Why does the author think it's confusing to partially apply foldr and map but not (+), (.), const, and length? Applied consistently: length = (.)(foldr((+)($, $), 0, $), map(const(1, $), $), $)
length2d = (.)(foldr((+)($, $), 0, $), map(length($), $), $)
And clearly no-one thinks this strawman is clearer. Further, it's impossible to make it 100% consistent: any function you write with polymorphic result could instantiate as a function needing more arguments. I think currying is the better default.Re: A case against currying
#124Earlier quoted context omitted.
> Unless you have the signatures of foobinade and foobinadd memorized, you have no way to tell that f is a curried function and g is an actual result. Yes, but the exact FP idea here is that this distinction is meaningless; that curried functions are "actual results". Or rather, you never have a result that isn't a function; `0` and `lambda: 0` (in Python syntax) are the same thing. It does, of course, turn out that…
It’s not at all clear or the same to the new reader of the code.
Re: A case against currying
#125Earlier quoted context omitted.
> Unless you have the signatures of foobinade and foobinadd memorized, you have no way to tell that f is a curried function and g is an actual result. Yes, but the exact FP idea here is that this distinction is meaningless; that curried functions are "actual results". Or rather, you never have a result that isn't a function; `0` and `lambda: 0` (in Python syntax) are the same thing. It does, of course, turn out that…
If 0 and a function that always returns 0 are the same thing, does that make `lambda: lambda: 0` also the same? I suppose it must do, otherwise `0` and `lambda: 0` were not truly the same.
Re: A case against currying
#126Earlier quoted context omitted.
> It's extremely rare that you need to (printf "%d %d" foo) I write stuff like `map (printf "%d %d" m) ns` all the time. I daresay I even do the map as a partial application, so double currying.
But arguably your intent would be much more clear with something like `map (printf "%d %d" m _) ns` or a lambda. I don't think parent is saying that partial application is bad, far from it. But to a reader it is valuable information whether it's partial or full application.
- `iter`: this is a side-effect on a collection
- `(printf`: ok, this is just printing, I don't care about what is printed, let's skip to the `)`
- ns: ok, this is the collection being printed
Notice that having a lambda or a partial application `_` will only add noise here.> But to a reader it is valuable information whether it's partial or full application.
This can be a valuable information in some context, but in a functional language, functions are values. Thus a "partial application" (in term of closure construction) might be better read as a full application because the main type of concern in the current context is a functional type.
Re: A case against currying
#127A benefit to using the currying style is that you can do work in the intermediate steps and use that later. It is not simply a 'cool' way to define functions. Imagine a logging framework: (log configuration identifier level format-string arg0 arg1 ... argN) After each partial application step you can do more and more work narrowing the scope of what you return from subsequent functions. ;; Preprocessing the configura…
Re: A case against currying
#128Earlier quoted context omitted.
Come on, just write let f :: Int = foobinade a b And the compiler immediately tells you that you are wrong: your type annotation does not unify with compiler’s inferred type. And if you think this is verbose, well many traditional imperative languages like C have no type deduction and you will need to provide a type for every variable anyways.
I spent the last three years on the receiving end of mass quantities of code written by people who knew what they were writing but didn't do an adequate job of communicate it to readers who didn't already know everything. What you say is true. And it works, if you're the author and are having trouble keeping it all straight. It doesn't work if the author didn't do it and you are the reader, though. And that's the mor…
Re: A case against currying
#129Re: A case against currying
#130From the article: length = foldr (+) 0 . map (const 1) length2d = foldr (+) 0 . map length -- and the proposed syntax the author calls more readable: length = foldr((+), 0, $) . map(const(1), $) length2d = foldr((+), 0, $) . map(length, $) But I don't understand. Why does the author think it's confusing to partially apply foldr and map but not (+), (.), const, and length? Applied consistently: length = (.)(foldr((+)(…
Neither of these functions is being partially applied in the code you cited. While the placeholder $ can be used to indicate all "free slots" of a function, you would only have to use it in partial applications of that function. (Analogous to how in mathematics the abstract index notation for tensors[0] is only really useful when you start contracting tensors, etc. Otherwise, the plain objects/tensors without indices are much easier to write & read.)
Specifically:
- (+) was already a function of two arguments, so usage of $ is unnecessary since (+)($, $) == (+). Similarly with the length function (a function of 1 argument): length($) == length.
- Function composition was being used as a binary operator between two functions. You just replaced the infix notation with prefix notation.
- I read const(1) as the function that maps everything to 1. I.e. `const` is a function of one argument x, which returns a function that always returns x. Once again, no need to indicate slots there.
[0]: See https://en.wikipedia.org/wiki/Abstract_index_notation and https://math.stackexchange.com/questions/455478/what-is-the-...