Live data from Hacker News

A case against currying

emi-h.com

101–110 of 135 posts

Re: A case against currying

#101

Currying was recently removed from Coalton: https://coalton-lang.github.io/20260312-coalton0p2/#fixed-ar...

> Simplicity: Every function takes exactly one input and produces exactly one output. No exceptions. If you didn’t care about the input or output, you used Unit, and we made special syntax for that.

Seems like a disaster to use s-expressions for a language like that. I love s-expressions but they only make sense for variadic languages. The entire point of them is to quickly delimit how many arguments are passed.

In say Haskell `f x y z` is the same thing as `(((f x) y) z)`. That is definitely not the case with s-expressions; braces don't delimit; they denote function application. It's like saying that `f(x,y,z)` being the same as `f(x)(y)(z)` which it really isn't. The point of s-expressions is that you often find yourself calling functions with many arguments that are themselves a result of a function application, at that point `foo(a)(g(a,b), h(x,y))` just becomes easier to parse as ((foo a) (g a b) (h x y))`.

Re: A case against currying

#102
A bunch of Scheme implementations define little-known syntax for partial application[0] that lets you put limits on how many arguments have to be provided at each application step. Using the article's add example:

  (define (((add x) y) z) (+ x y z))
  (define add1 (add 1))
  (define add3 (add1 2))
  (add3 3) ; => 6
it gets tedious with lots of single-argument cases like the above, but in cases where you know you're going be calling a function a lot with, say, the first three arguments always the same and the fourth varying, it can be cleaner than a function of three arguments that returns an anonymous lambda of one argument.

  (define ((foo a b c) d)
    (do-stuff))
  (for-each (foo 1 2 3) '(x y z))
vs

  (define (foo a b c)
    (lambda (d) (do-stuff)))
  (for-each (foo 1 2 3) '(x y z))

There's also a commonly supported placeholder syntax[1]:

    (define inc (cut + 1 ))
    (inc 2) ; => 3
    (define (foo a b c d) (do-stuff))
    (for-each (cut foo 1 2 3 ) '(x y z))
And assorted ways to define or adapt functions to make fully curried ones when desired. I like the "make it easy to do something complicated or esoteric when needed, but don't make it the default to avoid confusion" approach.

[0]: https://srfi.schemers.org/srfi-219/srfi-219.html

[1]: https://srfi.schemers.org/srfi-26/srfi-26.html

Re: A case against currying

#103
post #56

I couldn't agree more. Having spent a lot of time with a language with currying like this recently, it seems very obviously a misfeature. 1. Looking at a function call, you can't tell if it's returning data, or a function from some unknown number of arguments to data, without carefully examining both its declaration and its call site 2. Writing a function call, you can accidentally get a function rather than data if…

> I think programming languages have a tendency to pick up cute features that give you a little dopamine kick when you use them, but that aren't actually good for the health of a substantial codebase. That's not the case with Haskell. Haskell has a tendency to pick up features that have deep theoretical reasoning and "mathematical beauty". Of course, that doesn't always correlate with codebase health very well either…

I always felt Monads were an utterly disgusting hack that was otherwise quite practical though. It didn't feel like mathematical beauty at all to me but like a hack to fool to the optimizer to not sequence out of events.

Re: A case against currying

#104
post #86

What's the steelman argument though? Why do languages like Haskell have currying? I feel like that is not set out clearly in the argument.

I have a web backend with a top-level server that looks something like:

  ...

  : collectionsServer compactor env registry
  : queryServer qp sp struc registry warcFileReader logger
  : indexationServer fetcher indexer

  ...
I.e. a request coming into the top-level server will go into the collections server or the query server or the indexation server. Each server is further broken down (collections has 4 routes, query has 4 routes, indexation has 5 routes.)

So lets try making the the arguments of just the collections server explicit. (It will take me too long to try to do them all.)

You can 'list' collections, 'delete' a collection, merge collectionA into collectionB, or get the directory where the collections live. So the input (the lambda term(s) we're trying to make explicit) can be () or (collectionName) or (collectionNameA, collectionNameB) or ().

In order to put these lambda terms explicitly into the source code, we need to add four places to put them, by replacing collectionsServer with the routes that it serves:

  ...

  : (       listCollections registry
         : (\collectionName -> deleteCollection env registry collectionName)
         : (\collectionName1 collectionName2 -> mergeInto compactor collectionName1 collectionName2)
         :  getCollectionDir env ) 
  : queryServer qp sp struc registry warcFileReader logger
  : indexationServer fetcher indexer

  ...
And now you know what explicit lambda terms collectionsServer takes!

Re: A case against currying

#105
A case for currying:

In languages in which every function is unary but there is a convenience syntax for writing "multiargument" functions that produces curried functions, so that the type functions of type "a -> b -> c" can be written as if their type was "a b -> c", but which also have tuples such that "multiargument" functions could equally conveniently be written as having type "(a, b) -> c", and where the syntax for calling each type of function is equally straightforward in situations that don't require "partial application" (where the curried form has a natural added utility), people overwhelming use the syntax that produces curried functions.

People only predominantly use uncurried multiargument functions in languages which make writing and/or calling curried functions significant syntactic overhead.

Re: A case against currying

#106
post #80

Earlier 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…

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

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

Re: A case against currying

#107
If you're looking for this argument in a language closer to home, it's basically the opposite of your IDE's style guide:

If I write this Java:

  pair.map((a, b) -> Foo.merge(a, b));
My IDE flashes up with Lambda can be replaced with method reference and gives me

  pair.map(Foo::merge);
(TFA does not seem to be arguing against the idea of partial-function-application itself, as much as he wants languages to be explicit about using the full lambda terms and function-call-parentheses.)

Re: A case against currying

#108

Earlier quoted context omitted.

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

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

Re: A case against currying

#109
I also think there’s an interesting effect when cool functional language features like currying and closures are adopted by imperative languages. They make it way too easy to create state in a way that makes you FEEL like you’re writing beautiful pure functions. Of course, in a functional language everything IS pure and this is just how things work. But in an imperative language you can trick yourself into thinking you’ve gotten away with something. At one point I stored practically all state in local variables captured by closures. It was a dark time.

Re: A case against currying

#110

Earlier quoted context omitted.

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.

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

Sure, I like chained method calls too, for simple things. But it gets ridiculous sometimes where people write a ten-stage pipeline in a single expression and then call that "readable."
Post reply on HN