>>107250513
I don't hate currying in and of itself, I hate implicit currying because it royally fucks function type signatures. My problems are twofold:
- Arrows are the ML-family's equivalent of Lisp parens. Things start getting a bit annoying at 4 arrows. (Int * Int * Int) -> Int is immediately skimmable in a way that Int -> Int -> Int -> Int is not.
- Ambiguity. Consider for a moment someone who doesn't write a lot of Haskell. They haven't even begun to memorize all the associativity rules of the built-ins. They write a function with this signature:
Int -> Maybe Int -> Int
And they expect it to be equivalent to:
Int -> (Maybe Int) -> Int
Rather than
Int -> (Maybe Int -> Int)
They're going to have a C++ templates-tier compiler error trying to hunt that down.
It's not unique to learners of Haskell. I don't write it enough to have all the associativity stuff memorized, and I fall victim to it whenever I pick Haskell back up. It gets especially bad once we introduce type variables.
These two points compound each other, too. Can't skim signatures and I have to disentangle ambiguous ones. Currying is powerful and implicit currying is ergonomic in functional programming, but I've come to prefer explicitness in code. Rather than treat everything as composable, I like to be a bit more deliberate and indicate composability boundaries. You can easily lose sight of the logic of a program that composes multi-argument functions that are partially applied in a way that you can't with functions that accept and return tuples. Makes them less flexible, sure, but I find that flexibility can end up a footgun.
I don't necessarily hate partial application either, but I greatly prefer partial instantiation. Imagine a type system where this is possible:
f :: (Int * Int * Int) -> Int
let x: Int -> Int = f (1, 2, _)
let y: Int = x 3