7 ms·
I completely agree with the points in this article and have come to the same conclusion after using languages that default to unary curried functions. > I'd al
by paldepind2 6mo ago
I completely agree with the points in this article and have come to the same conclusion after using languages that default to unary curried functions.
> I'd also love to hear if you know any (dis)advantages of curried functions other than the ones mentioned.
I think it fundamentally boils down to the curried style being _implicit_ partial application, whereas a syntax for partial application is _explicit_. And as if often the case, being explicit is clearer. If you see something like
let f = foobinade a b
in a curried language then you don't immediately know if `f` is the result of foobinading `a` and `b` or if `f` is `foobinade` partially applied to some of its arguments. Without currying you'd either write
let f = foobinade(a, b)
or
let f = foobinade(a, b, $) // (using the syntax in the blog post)
and now it's immediately explicitly clear which of the two cases we're in.
This clarity not only helps humans, it also help compilers give better error messages. In a curried languages, if a function is mistakenly applied to too few arguments then the compiler can't always immediately detect the error. For instance, if `foobinate` takes 3 arguments, then `let f = foobinade a b` doesn't give rise to any errors, whereas a compiler can immediately detect the error in `let f = foobinade(a, b)`.
A syntax for partial application offers the same practical benefits of currying without the downsides (albeit loosing some of the theoretical simplicity).
- munchler 6mo agoWell, I totally disagree with this. One of the main benefits of currying is the ability to chain function calls together. For example, in F# this is typically done with the |> operator: let result = input |> foobinade a b |> barbalyze c d Or, if we really want to name our partial function before applying it, we can use the >> operator instead: let f = foobinade a b >> barbalyze c d let result = f input Requiring an explicit "hole" for this defeats the purpose: let f = barbalyze(c, d, foobinade(a, b, $)) let result = f(input) Or, just as bad, you could give up on partial function application entirely and go with: let result = barbalyze(c, d, foobinade(a, b, input)) Either way, I hope that gives everyone the same "ick" it gives me.
- emih 6mo agoYou can still do this though: let result = (barbalyze(c, d, $) . foobinade(a, b, $)) input Or if you prefer left-to-right: let result = input |> foobinade(a, b, $) |> barbalyze(c, d, $) Maybe what isn't clear is that this hole operator would bind to the innermost function call, not the whole statement.
- raincole 6mo agoWow, this convinced me. It's so obviously the right approach when you put it this way.
- twic 6mo agoEven better, this method lets you pipeline into a parameter which isn't the last one: let result = input |> add_prefix_and_suffix("They said '", $, "'!")
- raincole 6mo agoYeah, especially in F#, a language that means to interpolate with .Net libraries (most not written with "data input at last" mindset.) now I'm quite surprised that F# doesn't have this feature.
- Smaug123 6mo agoThis is essentially how Mathematica does it: the sugar `Foo[x,#,z]&` is semantically the same as `Function[{y}, Foo[x,y,z]]`. The `&` syntax essentially controls what hole belongs where.
- deleted 6mo ago[deleted]
- skybrian 6mo agoFor pipelines in any language, putting one function call per line often works well. Naming the variables can help readability. It also makes using a debugger easier: let foos = foobinate(a, b, input) let bars = barbakize(c, d, foos) Other languages have method call syntax, which allows some chaining in a way that works well with autocomplete.
- riwsky 6mo agoThe functional programming take is that “the result of foobinade-ing an and b” IS “foobinade applied to two of its arguments”. The application is not some syntactic pun or homonym that can refer to two different meanings—those are the same meaning.
- AnimalMuppet 6mo agoLet us postulate two functions. One is named foobinade, and it takes three arguments. The other is named foobinadd, and it only takes two arguments. (Yes, I know, shoot anybody who actually names things that way.) When someone writes f = foobinade a b g = foobinadd c d there is no confusion to the compiler. The problem is the reader. 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. Whereas with explicit syntax, the parentheses say what the author thinks they're doing, and the compiler will yell at them if they get it wrong.
- zahlman 6mo ago> 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 for many people this isn't a natural way of thinking about things.
- AnimalMuppet 6mo agoFine, it's a regular type. It's still not the type I think it is. If it's an Int -> Int when I think it's an Int, that's still a problem, no matter how much Int -> Int is an "actual result".
- kccqzy 6mo agoCome 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.