Remix.run Logo
paldepind2 7 hours 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).

riwsky 4 hours ago | parent | next [-]

The 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 3 hours ago | parent [-]

Let 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 3 hours ago | parent [-]

> 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.

raincole 3 hours ago | parent | next [-]

> Yes, but the exact FP idea here is that this distinction is meaningless; that curried functions are "actual results".

Everyone knows that. At least everyone who would click a post titled "A case against currying." The article's author clearly knows that too.

That's not the point. The point is that this distinction is very meaningful in practice, as many functions are only meant to be used in one way. It's extremely rare that you need to (printf "%d %d" foo). The extra freedom provided by currying is useful, but it should be opt-in.

Just because two things are fundamentally equivalent, it doesn't mean it's useless to distinguish them. Mathematics is the art of giving the same name to different things; and engineering is the art of giving different names to the same thing depending on the context.

momentoftop a few seconds ago | parent | next [-]

> 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.

kccqzy 2 hours ago | parent | prev [-]

> It's extremely rare that

Not when a language embraces currying fully and then you find that it’s used all the fucking time.

It’s really simple as that: a language makes the currying syntax easy, and programmers use it all the time; a language disallows currying or makes the currying syntax unwieldy, and programmers avoid it.

jstanley 2 hours ago | parent | prev | next [-]

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.

fn-mote 2 hours ago | parent [-]

Another way to make the point: when you write 0, which do you mean?

In a pure language like Haskell, 0-ary functions <==> constants

AnimalMuppet 3 hours ago | parent | prev | next [-]

Fine, 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 2 hours ago | parent [-]

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.

AnimalMuppet 2 hours ago | parent [-]

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 more common case, for two reasons. First, code is read more often than it's written. Second, when you're the author, you probably already have it in your head how many parameters foobinade takes when you call it, but when you're the reader, you have to go consult the definition to find out.

But if I was willing to do it, I could go through and annotate the variables like that, and have the compiler tell me everything I got wrong. It would be tedious, but I could do it.

skywhopper 2 hours ago | parent | prev [-]

It’s not at all clear or the same to the new reader of the code.

munchler 7 hours ago | parent | prev [-]

Well, 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 7 hours ago | parent | next [-]

You 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.
twic 6 hours ago | parent | next [-]

Even 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 6 hours ago | parent [-]

Yeah, 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 4 hours ago | parent | prev | next [-]

This 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.

raincole 6 hours ago | parent | prev [-]

Wow, this convinced me. It's so obviously the right approach when you put it this way.

skybrian 6 hours ago | parent | prev | next [-]

For 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.
RHSeeger 3 hours ago | parent [-]

> Naming the variables can help readability

It can, or it can't; depending on the situation. Sometimes it just adds weight to the mental model (because now there's another variable in scope).

7 hours ago | parent | prev [-]
[deleted]