A Case Against Currying
27 points by fanf
27 points by fanf
(Automatic) currying also causes serious issues with ownership-style systems. Jane Street is/was considering adding a special non-curried function arrow to OCaml, just because of how much it would simplify their "Oxidizing OCaml" system...
i can confirm: https://oppi.li/posts/auto-currying_rust_functions/
in which i have attempted to curry rust with a proc macros.
There's a Scheme version of such an explicit currying operator: https://srfi.schemers.org/srfi-26/srfi-26.html, e.g.
(cut + 1 <>) is the same as (lambda (x) (+ 1 x))
(And, Scheme being a serious language, it's implemented in the language itself in 20 lines.)
Racket also has curry and curryr.
> ((curry list 1 2) 3 4)
(list 1 2 3 4)
> ((curryr list 3 4) 1 2)
(list 1 2 3 4)
C++ has had std::bind since C++11 to do the partial application. It’s quite rare to see it used though, because the syntax is a little bit clunky and the lambda equivalent is normally more readable.
Dependent types are famously immensely difficult to work with and make type inference undecidable, so even when you're working in a language with this capability, it's best to avoid dependent types in function parameters.
we could argue about the first point, but type inference is also undecidable in any language with generics (since System F's typechecking/inference is undecidable) which is basically all commonly typed languages nowadays except C, so i don't see why that's a reason not to use dependent types
type inference is also undecidable in any language with generics
Standard ML is a counterexample. I think the crucial difference wrt System F is that let-bound variables can be polymorphic but lambda-bound variables cannot.
I don't have a strong opinions on this as I don't deal with currying languages on a daily basis, but to me it always seemed like a case of noticing a special case and running with it too far. Kind of like nulls.
Yeah if the parameter you want to capture isn't the first argument, suddenly you're back to square one.
There are other arguments in favor of currying that didn't get mentioned:
f <$> x1 <*> x2 ...) without it. You could have special syntax for the Applicative case, but there are others then that you'd miss out on.fst. And if that's happening, and you insist on functions being written in uncurried form, you now have to deal with everything in two different ways based on a fact that really shouldn't be relevant - namely, whether it so happens that the function came from a specialization.With these points added, I'd say the balance is "if your language is functional and frequently polymorphic, curried functions are great". And that is in fact where currying gets most often used!
Two other small points:
> (P1, P2) -> R and P1 -> P2 -> R are isomorphic
In lazy languages this ignores the difference between these:
f _ = 1
f (_, _) = 1
I wouldn't be surprised if in Haskell curried functions were if anything faster, since there's one fewer heap objects.
I don’t want to give up the applicative operators, but I also don’t really like them compared to idiom brackets. I suppose GHC doesn’t have them just because people don’t find them better enough?
f :: P1 -> P2 -> R
f _ = 1
I can think of very few reasons why this would typecheck??
It's hard to see how you would use Applicative (and the like) notation (
f <$> x1 <*> x2 <*> x3) without it.
This can be just zip3(x1, x2, x3) . fmap(f, apply, $) (borrowing syntax from post), though sure this is limited in the number of arguments that can be applied
I wouldn't be surprised if in Haskell curried functions were if anything faster, since there's one fewer heap objects.
How so? I don't know much about its execution model but "creating intermediate functions" sure seems like that'd be heap objects, unless tuples are the ones always put there?
These both have signature (P1, P2) -> R, not P1 -> P2 -> R. The point, which is to be fair very nit-picky, is that there is one extra bottom with tuples (and these two functions behave differently on that bottom), so there isn't such an isomorphism.
How so? I don't know much about its execution model but "creating intermediate functions" sure seems like that'd be heap objects, unless tuples are the ones always put there?
First, you may only have to partially apply once, but construct the tuples N number of times. For instance, let h = add 5 in map h. With tuples, this would be map (\x -> add (5, x)), which constructs as many tuples as there are elements in the list.
Second, no, there isn't necessarily one heap object created per argument applied. Details are here.
Oh very interesting! Thanks for the link, I learned a bit even if I didn't understand it all.
From what I did get, it seems that, under the hood, GHC treats all curried functions a -> b -> c -> d like how an imperative language would treat (a, b, c) -> d, for the same performance reasons described in the original post. And then for partial applications, it does exactly what I would hope (\x -> add (5, x)) would do, which is "push 5 to the stack, then call add with all arguments on the stack".
I guess where this falls apart in practice is the fact that "function calling a tuple" isn't first-class, and tuples (like all values without a # suffix) are boxed.
With tuples, this would be map (\x -> add (5, x)), which constructs as many tuples as there are elements in the list.
Heh, earlier I was writing a comment to make the same point but I deleted it when I realised my explanation was wrong :-)
There’s a curious difference in style between ML-family languages, which prefer tuples, and Haskell-family languages, which prefer currying. I wonder how much it is related to the implementation techniques the languages used in the 1980s when these styles were being established.
In early implementations of lazy evaluation based on combinator graph reduction, the evaluation strategy is fundamentally based on curried functions, so it seems natural that the surface-level code should follow suit. A cute feature of combinator reduction is that the program gets automatically optimized at run time as a result of shared expressions being reduced at most once. So a partially-applied tuple-taking add like the example above would get reduced to basically the same code as a curried add.
The real problem occurs in code where there are lots of unshared expressions so the tupling and projections that were not optimized out by a simple compiler are also not optimized out by the runtime.
I’m less familiar with the implementation strategies of early MLs. SML/NJ uses a bump allocator and a generational collector, and the code is compiled in continuation-passing style. There’s no stack, instead arguments are allocated on the heap, and the performance of constructing a tuple is the same as pushing arguments on the stack in a more conventional runtime.
Of course nowadays compilers do a lot more up-front analysis and optimization of the code, so there’s a much bigger difference between source code style and compiled implementation strategies. What I learned from making a fast curry: push/enter vs eval/apply is that modern compilers are able to erase most of performance difference between currying and tupling arguments.
Maybe what we actually want is partial application and/or really easy lambdas? I do confess I'm sick of writing |x| foo.do_thing(x) in Rust iterator chains. It'd be nice to have something like Fennel's "hash function" shortcut where #(+ 1 $1 $2) is shorthand for (lambda (x y) (+ 1 x y))
I really like the tuple style conceptually, but there's a few downsides to it, or at least design wrinkles to think about. For 1-arity functions, do you pass a tuple of length 1 or just the object? The value foo is not the same as the tuple (foo,) and if you say it is then you're making life hard for yourself.
Can you do pattern matching in the function args a la fn foo((x, y, z): (f32, f32, f32), a: i32)? If so your args aren't just a tuple anymore, they're a pattern. And if you want default, optional or keyword-based function args in your language, your args are again no longer a tuple, they're something else that you probably want a convenient shorthand for. ...Though it might be fun to build a language where tuples or structs can contain patterns, keywords, defaults, optionals, etc and every function just takes one of those objects. :-)
this is actually a bit more readable. The more complicated example now looks as follows
I don't think either is particularly readable.
so we can write convoluted definitions for calculating the length of a 2D list and feel very clever
This is just bad programming.