NOW LET US – AI RAG SaaS Studio TP.HCM
NOW LET US
Digital Product Studio
Back to news
DEV-TOOLS...7 min read

A Case Against Currying

Share
NOW LET US Article – A Case Against Currying

While currying is a staple of functional programming, it may introduce performance overhead and logical complexity. This article explores different parameter-passing styles and challenges the perceived superiority of curried functions.

Curried functions are probably one of the first new things you come across if you go from an imperative language to a functional language. In purely functional languages, the convention is to define an n-parameter function inductively by staggering the parameters: applying the function to argument #1 returns a function that takes parameters 2..n, which in turn can be applied to argument #2 to return a function that takes parameters 3..n, etc. until all the arguments are given and the result is returned. For instance, we can define a 3-parameter add function that adds three numbers:

add x y z = x + y + z
-- this is syntactic sugar for:
add = \x -> (\y -> (\z -> x + y + z))
-- add has the following type:
add :: Int -> (Int -> (Int -> Int))
-- you can apply left-to-right:
((add 1) 2) 3 -- returns 6

We make the arrow -> right-associative, so we can write Int -> Int -> Int -> Int. Additionally, we make function application left-associative, so we can write add 1 2 3 and minimize our use of parentheses. I want to argue that when you define functions in this style, although it is nice and elegant, there are also some things that are lost.

There are roughly three different "styles" that programming languages offer for writing multi-parameter function definitions with. Firstly, there is the imperative style which I will call the "parameter list" style. Here, multiple parameters is a baked-in feature of functions. This is the default in imperative languages like Rust:

fn f(p1: P1, p2: P2, p3: P3) -> R { ... } // definition
f(a1, a2, a3) // call
f : fn(P1, P2, P3) -> R // type

Another form is the "curried" style, offered in pure functional languages like Haskell:

f p1 p2 p3 = ... -- definition
f a1 a2 a3 -- call
f :: P1 -> P2 -> P3 -> R -- type

Finally, there is the "tuple" style. It looks similar to the parameter list style, but the multiple parameters are not part of the functions itself. The function just has one parameter, but that parameter is a tuple so it effectively carries multiple values. This is usually possible in functional languages like Haskell as well but not the standard:

f(p1, p2, p3) = ... -- definition
f(a1, a2, a3) -- call
f :: (P1, P2, P3) -> R -- type

Note that some imperative languages also offer these functional styles, but it gets a bit unwieldy. For instance, we can do the curried style in JavaScript:

const f = p1 => p2 => p3 => ...;
f(a1)(a2)(a3)

We can also do the tuple style in Rust:

fn f((p1, p2, p3): (P1, P2, P3)) -> R { ... }
f((a1, a2, a3))
f : fn((P1, P2, P3)) -> R

Ultimately, even though these styles have different practical use cases, they are equivalent in theory: the types (P1, P2) -> R and P1 -> P2 -> R are isomorphic, which means there is a one-to-one mapping between functions of those types. So, what's the reason for preferring the currying style to the others?

Partial Application

When you ask the internet why we do curried functions, the main response is "because it makes partial application straightforward". Partial application is a mechanism where you fix one of the parameters of a multi-argument function to a certain value, and get a new function that only takes the rest of the parameters as input. It is true that partial application is very natural and elegant for the currying style; for instance, if we take our 3-parameter add from before, you can do the following in Haskell:

add' = add 1
-- now, add' = \y -> (\z -> 1 + y + z)
add' :: Int -> Int -> Int
add'' = add' 2
-- now, add'' = \z -> 1 + 2 + z
add'' :: Int -> Int
add'' 3 -- returns 6

This is especially nice when we have higher-level functions like map and fold. For instance, we can do pretty crazy cool things with partial applications and function composition:

length = foldr (+) 0 . map (const 1)
length2d = foldr (+) 0 . map length
length2d [[1, 4, 2], [], [7, 13]] -- returns 5

However, it is often wrongly assumed that the option of doing partial application is a special property of curried functions! We can totally do partial application for functions in the parameter list style or tuple style. If we redefine add in the tuple style, we get something like the following:

add(x, y, z) = x + y + z
add :: (Int, Int, Int) -> Int
add' = let x = 1 in \(y, z) -> add(x, y, z)
add'' = let y = 2 in \z -> add'(y, z)
add''(3) -- returns 6

"But," I hear you say, "this clearly looks horrible." Or maybe not, I don't want to put words in your mouth. But we can easily define a bit of syntactic sugar to make it look nice, for instance by defining a $ "hole operator":

add' = add(1, $, $)
add'' = add'(2, $)
add''(3) -- returns 6

In my opinion, this is actually a bit more readable. The more complicated example now looks as follows:

length = foldr((+), 0, $) . map(const(1), $)
length2d = foldr((+), 0, $) . map(length, $)
length2d([[1, 4, 2], [], [7, 13]]) -- returns 5

I actually find this form more clear. It kinds of shows the "flow" of the data that you feed into length or length2d: first into the second parameter of map, then the result of that is fed into the third parameter of foldr.

Additionally, such a feature allows partial applications of not just the first parameter, which does not work by default for the curried style. For instance if we want to fix the second parameter of map:

-- this is all colors. ever made.
allColors = ["red", "green", "blue"]
forEachColor = map($, allColors)

This feature does have some limitations, for instance when we have multiple nested function calls, but in those cases an explicit lambda expression is always still possible.

So, despite its elegance, the curried function style doesn't really make partial application more powerful; with a little bit of syntactic sugar we can easily emulate its powers. However, I suspect there is another, more "vibe-based" reason why functional programmers have a dependence on curried functions stronger than water or sleep.

When you first learn functional programming and finally understand curried function types, it's like you're looking inside the matrix. It's so cool that when you make -> right-associative and function application left-associative, successively applying a curried function to multiple parameters unfolds beautifully! We don't even have to write parentheses when doing function application. And we get partial application completely for free, so we can write convoluted definitions for calculating the length of a 2D list and feel very clever.

Even more beautifully, it's essentially an inductive "shape", which reflects the dichotomy between imperative languages and functional languages: they've got their parameter lists, which are more like arrays (iterative), but we have curried functions, which are more like lists (inductive). It makes me feel all warm and fuzzy inside just thinking about the elegance! This is clearly meant to be!

Just because you can, doesn't mean you should. There are some good reasons to prefer the tuple style.

First of all, performance is a bit of a concern. When you call a curried function like add 2 3, the add 2 first evaluates to a new function expression \y -> add 2 y, which is then applied to 3. Every call to a multi-parameter function creates a bunch of intermediate functions. However, I'm sure a good enough optimizer can eliminate that overhead, so this is not really the greatest concern.

More importantly, curried function types have a weird shape. The whole idea about functions is that they take an input and give back an output, so they have types like In -> Out. When you unify this with a curried function type like P1 -> P2 -> P3 -> R, you get In = P1 and Out = P2 -> P3 -> R. When you unify it with a tupled function type like (P1, P2, P3) -> R, you get In = (P1, P2, P3) and Out = R, which seems more logical.

A consequence of this

© 2026 Now Let Us. All rights reserved.

Source: Hacker News

Advertisement
Ad slot ready: 5887729102

More in this category

EXPLORE TOPICS

Discover All Categories

Deep dive into the specific technology sectors that matter most to you.