If you learned functional programming through JavaScript, you almost certainly learned currying. It appears in the first FP tutorial: turn add(a, b) into add(a)(b), pre-fill one argument, and get a reusable function. By the time someone hands you Ramda or lodash/fp, every function curries itself. Partial application - fixing a few arguments now, handing over the rest later - starts to feel like the natural shape of composition.
Then you open Elixir, a language everyone agrees is functional, and currying is just… not there. There’s no curry helper in the standard library. You can’t call a two-argument function with one argument and get a function back - you get a BadArityError. The community has answered this question on forums for over a decade: use pipes and the capture operator instead. Currying “feels foreign.”
These are not gaps on either side. They’re two languages taking the same idea - let me fix some arguments and keep going - and making opposite decisions about what a function is. This article explains why and what that means for the code you write in each language.
What currying actually is (and the thing it isn’t)
Start with the terms. The JavaScript community uses them loosely, and the distinction matters here.
Currying transforms a function that takes multiple arguments into a chain of functions that each take exactly one. A curried add must be called add(1)(2)(3) - one argument per step, left to right. The technique is named after Haskell Curry, though Moses Schönfinkel described it first in 1924; Christopher Strachey coined the name “currying” in 1967, and it stuck despite the quip that “Schönfinkeling” would be more accurate.
Partial application is different. It fixes some arguments and returns a function that accepts the rest in a single call, in any grouping. A partially applied add might be called add(1, 2)(3) or add(1)(2, 3) - the remaining arguments can arrive bunched.
The mechanical difference is simple: in currying, every intermediate value is a unary function you can name and reuse (const add5 = add(5)); in partial application, the intermediates are typically single-use. The JavaScript community routinely blurs these terms. Even experienced authors describe bind() as “currying” when it’s technically partial application. That borrowing causes bugs. If you build curry-shaped expectations on a partial-application mechanism, you import assumptions - one argument per step, no receiver - that the mechanism does not honor. The traps below follow from that mismatch.
A philosophical wrinkle appears once you’ve internalized both ideas. In a truly curried language, “partial application” is a misnomer: every function already takes exactly one argument, so there is nothing to “partially” apply. The term only makes sense when functions genuinely have multiple parameters. Elixir lives in that world; Haskell does not. Remember that distinction.
The JavaScript side: it works, and you build it yourself
JavaScript has no built-in currying, yet it is the language where currying is most often taught, demonstrated, and broken. The reason is structural: JavaScript functions are first-class values and closures are cheap, so currying is always possible. The calling conventions make it fragile in real code.
The three techniques
Arrow chains are the idiomatic manual form. Each application allocates a closure capturing the arguments seen so far:
const add = (a) => (b) => (c) => a + b + c;
add(1)(2)(3); // 6
const add1 = add(1); // a reusable intermediate
add1(2)(3); // 6
Arrow functions made this ergonomic. The nested function form you’d write pre-ES6 (function(a) { return function(b) { ... } }) was ugly enough to discourage the style. The syntax has two limits: arity is fixed at write time (three nested arrows means exactly three arguments), and extra arguments are silently ignored (add(1, 2)(3) doesn’t error, it just drops the 2).
A generic curry helper converts an existing function without rewriting it. The key heuristic is fn.length - the function’s declared parameter count. The helper accumulates arguments recursively until that count is met:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const sum3 = (a, b, c) => a + b + c;
const curriedSum = curry(sum3);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6 - mixed groupings allowed
This is more permissive than an arrow chain because it accepts curriedSum(1, 2)(3). It is therefore not true currying. It is partial application wearing a curried interface. Libraries ship this hybrid because it is more ergonomic.
Function.prototype.bind is the only native mechanism, and it is partial application, not currying. It prefills a fixed set of leading arguments and, unavoidably, pins the this receiver as its first parameter:
function greet(greeting, name) {
return greeting + ", " + name + "!";
}
const sayHello = greet.bind(null, "Hello"); // first arg is `this`, not an argument
sayHello("Ada"); // "Hello, Ada!"
That null placeholder trips people up. Developers using bind as a currying tool put the intended first argument in the receiver slot and bind nothing. The TC39 partial-application proposal, which would have given JS native syntax (f~(1, ?)) for this, has sat dormant at Stage 1 for years with no recent committee advancement. Write code as if it will never ship.
The traps
Every technique above has the same weakness: JavaScript functions are variadic by nature. Every function accepts any number of arguments without error, so arity-driven automation is a heuristic that declaration syntax can defeat.
fn.length lies. Per the ECMAScript semantics, .length “excludes the rest parameter and only includes parameters before the first one with a default value”:
((a, b, c) => {}).length; // 3
((a, b = 1, c) => {}).length; // 1 - defaults truncate everything after
((...args) => {}).length; // 0 - rest params invisible
A defaulted parameter truncates the perceived arity. The generic curry then calls the underlying function early with undefined in the tail positions, and the result is invoked as if it were a function. Ramda’s own documentation gives an explicit warning: “default parameters don’t count towards a function arity and therefore curry won’t work well with those.” The library workaround is R.curryN(n, fn), which takes the arity explicitly.
Currying a method drops its receiver. Closure-based currying captures arguments, not this. Curry a method and the dynamic receiver disappears:
const cart = {
taxRate: 0.2,
total(price, qty) {
return price * qty * (1 + this.taxRate);
},
};
const curriedTotal = curry(cart.total);
curriedTotal(10)(5); // TypeError in strict mode - this.taxRate is unreachable
The fixes are awkward. Pass cart.total.bind(cart) into the curry, which specifies the receiver and curries at the same time, or stop using method syntax. The practical guidance is clear: currying comes from functional programming and works best with functions that do not depend on dynamic this.
There’s a performance cost. Each partial application allocates a new closure object holding the accumulated arguments; heavily curried call paths allocate “layers of nested closures to store intermediate arguments.” Ramda states openly that it “strives for performance” and that “a reliable and quick implementation wins over any notions of functional purity,” while third-party comparisons consistently show lodash’s imperative implementations outpacing Ramda’s curried, immutable ones. Curry at module boundaries where functions are applied once. Profile before currying inside hot loops.
When the JS style earns its keep
JavaScript currying is not bad. It is a discipline imposed on a runtime that was not built for it. The payoff is real when you reuse functions with some arguments fixed: loggers with preset levels, API calls with preset base URLs, validators with preset rules. The curried shape lets you build small, pre-configured transformers and compose them. Libraries solved the ergonomics problem years before the committee did: Ramda auto-curries every function and arranges parameters data-last, so R.map(fn) returns a reusable transformer awaiting its collection; lodash followed with lodash/fp, an auto-curried, immutable, data-last variant.
The honest community verdict is situational. If partially applied intermediates are rarely reused, “currying adds unnecessary complexity.” Readability is the counterweight: much of a typical team cannot read point-free curried code fluently, so currying as a default style is an anti-pattern even among its fans. Start with vanilla JavaScript. Use Ramda or lodash/fp when the composition actually demands it.
The Elixir side: it’s not missing, it’s refused
This surprises JavaScript developers. Elixir is functional through and through - map, filter, reduce, immutability, pattern matching, the pipe - and it does not curry. Not “doesn’t curry by default.” It does not curry semantically. That follows from deliberate design, not an oversight.
Arity is identity
The Erlang Reference Manual states the rule plainly: “A function is uniquely defined by the module name, function name, and arity. That is, two functions with the same name and in the same module, but with different arities are two different functions.” Joe Armstrong made the same point in his own textbook: “In Erlang, two functions with the same name and different arity in the same module represent entirely different functions. They have nothing to do with each other apart from a coincidental use of the same name.”
That fact rules out currying at the semantic level. Currying means that calling a two-argument add with one argument returns a function waiting for the second. In Erlang and Elixir, add(1) does not “partially apply” add/2; it resolves to add/1, which is either a different function you defined or an undef error because you did not. There is no intermediate state called “a function with some of its arguments.” Calling a two-arity Elixir function with one argument raises BadArityError.
Try it. Capture a two-argument function and pass it where a one-argument function is expected:
Enum.map([1, 2, 3], &MyMath.pow/2)
# ** (BadArityError) &MyMath.pow/2 with arity 2 called with 1 argument (1)
Enum.map/2 passes each element of the list to the function and nothing else, so the function must accept a single argument. You cannot “partially apply” pow/2 by handing it over with one argument missing. The language has no such operation.
Contrast this with the ML family. In Haskell and OCaml, “there’s really no such thing as an ‘arity 3 function,’ just a single-argument function that returns a new single-argument function” - multi-argument functions are syntactic sugar over nested unary functions, so partial application is simply calling the outermost one. The Haskell type Int -> Int -> Int parses right-associative as Int -> (Int -> Int), a unary function returning a unary function, and application is left-associative, so add 1 2 already means (add 1) 2. Partial application is not a technique in Haskell; it is the default evaluation model. Currying is not a feature bolted onto Haskell - it is the fingerprint of a language in which arity is not part of a function’s identity. Erlang made the opposite choice, and everything else follows.
The arity-is-identity rule is inherited from Prolog. Armstrong described Erlang’s genesis directly: “I started fiddling around with Prolog… I wrote a Prolog meta-interpreter that added parallel processes to Prolog, and then I added error handling mechanisms… After a while, this set of changes to Prolog acquired a name, Erlang.” Prolog predicates are identified by functor/arity, and Erlang kept the convention.
What Elixir offers instead: the capture operator
Elixir’s answer to partial application is the capture operator, &. It does two jobs: capturing a named function as a value (which requires writing the arity - add = &Kernel.+/2, the identity rule appears in the syntax itself), and creating anonymous functions with numbered placeholders:
# Capture a named function - note the explicit /2
add = &Kernel.+/2
add.(1, 2) # => 3
# Placeholder form - fixes every argument position at write time
adder = &(&1 + &2)
add_two = &(&1 + 2) # a complete one-argument function, not an intermediate
add_two.(3) # => 5
&(&1 + 2) is a complete one-argument function as soon as you write it. It is not an intermediate waiting for more arguments. The author of Elixir’s Currying hex package describes the limit this way: “You can only use captures if you exactly know how many arguments you’re going to put in your function right now. In the case of partial application (which might happen in multiple steps), this becomes a problem… In essence, currying lets you delay the ‘capture’ step to runtime. … Elixir’s native tool is the write-time, fixed-arity alternative.”
When you need to pre-fill an argument of a named function, capture it with one placeholder in the open position. The Advanced Functional Programming with Elixir book presents this as the alternative to currying:
def suggested_rides(%Patron{} = patron, rides) when is_list(rides) do
Enum.filter(rides, &suggested?(patron, &1))
end
Rather than curry(&suggested?/2).(patron), this capture fixes patron and creates a unary function that takes the ride as &1. The book’s verdict is: “Capture syntax is more idiomatic in Elixir, but it can be brittle… I find curried functions easier to read, especially when chaining transformations.” Capture wins on idiom. The point is that Elixir has the tool, but it is spelled differently from the JavaScript version.
You can build currying - and the community will tell you not to
Currying is not impossible on the BEAM. Closures work fine; you can write a curry/1 that wraps a function in single-parameter anonymous functions until the original arity is reached:
def curry(fun) when is_function(fun) do
arity = :erlang.fun_info(fun, :arity) |> elem(1)
curry(fun, arity, [])
end
defp curry(fun, 1, args),
do: fn last_arg -> apply(fun, args ++ [last_arg]) end
defp curry(fun, arity, args) when arity > 1 do
fn next_arg -> curry(fun, arity - 1, args ++ [next_arg]) end
end
It works. The Currying hex package ships exactly this. The community has given the same response for a decade across two forums: do not bother. Nobbz, a longtime forum voice, says: “I think it is nice to play with currying and partial application in Elixir, but I might probably stick with pipes and captures, since they are a native language thing, while the curried stuff feels foreign.” And: “FP doesn’t mean currying. Elixir is a functional programming language, but it doesn’t have any syntactic sugar for currying.”
Gleam - the statically typed BEAM language, whose type system could have expressed currying beautifully - shows that this is a preference, not a limitation. Gleam’s standard library once shipped curry2 through curry6. In stdlib v0.44.0 (2024) they were all marked “Deprecated: Use the anonymous function syntax instead,” and by v1.0.x the module was reduced to identity alone. A language that could have curried deprecated and deleted its currying helpers, replacing them with advice to write the nested fn. That says more than never offering them would have.
The performance argument behind the refusal
Design coherence explains the status quo, but performance matters too, and it connects directly to the performance article’s theme: the runtime decides. An Ecto core team member put it sharply in a 2016 forum discussion: “I think currying can be very cool, but there isn’t a plan here for how to deal with the massive overhead incurred by layering anonymous function on anonymous function for every argument ever. … Proper currying requires support [that] goes far deeper than even Elixir the language can provide.” The author of the Currying library conceded the point: “The BEAM does not have built-in support for currying. The library I made here is indeed slower than not using currying, as anonymous functions are being built in intermediate steps.”
In a curry-by-default language like Haskell, the compiler collapses these chains; on the BEAM, a curried call of arity N allocates N closures and pays N fun calls, with no VM-level optimization to remove them. As the performance article explains, the BEAM optimizes for fairness and concurrency over single-thread throughput, and every process gets its own heap. Layered closures are real allocation costs on a runtime whose entire bet is that you can spawn a million cheap processes. The BEAM does not hide that cost behind a calling convention it was never built to optimize. Language design and runtime design make the same decision.
The deeper question: where does abstraction live?
The currying question stops being about syntax here. Ask “how does your language curry?” and you have asked “what is a function here?” The answer shows where the language puts its abstractions.
Haskell’s answer is: abstraction lives at compile time, in the type system. A Haskell function is unary by construction, so currying costs nothing at runtime and everything at the type level, where the compiler verifies each partial application before the program runs. Currying is invisible there - fully applied and partially applied calls are textually indistinguishable, and only the type checker knows the difference.
The BEAM’s answer is: abstraction lives at runtime, in processes. A function’s identity is name-plus-arity, so auto-currying is not merely awkward but semantically incoherent - add(1) cannot mean “wait for one more argument” when it already means a complete call to a different function. The BEAM makes partial application textual: Elixir’s capture &(&1 + 1), Gleam’s f(1, 2, _), nested fns everywhere. The runtime absorbs complexity - closures are real allocated costs - so the language does not hide them behind a calling convention.
JavaScript’s answer is the uncomfortable one: abstraction lives in the programmer’s discipline. The language gives you first-class functions and closures - Scheme’s inheritance, as it happens; Brendan Eich has said he was recruited to Netscape on the promise of “doing Scheme in the browser” - but no enforcement. Variadic calling, silent extra-argument tolerance, a mutable this receiver, and an fn.length that erodes mean currying in JavaScript is always possible and never guaranteed. The proposal that would have put it into syntax remains dormant, so the spec will not rescue you.
Currying works as a diagnostic instrument because it is small. Three communities, one question, three answers that cannot be interchanged.
What to actually do with this
A few practices survive the comparison.
In JavaScript, curry deliberately, never accidentally. Write the arrow chain by hand when a call site wants it (const add5 = add(5)), or use a curryN that takes arity explicitly - never a generic curry that infers arity from fn.length, because rest parameters and defaults silently corrupt that count. Never pass multi-argument callbacks bare into higher-order functions: ['1','2','3'].map(parseInt) evaluating to [1, NaN, NaN] is the lesson in one line. map passes (element, index, array) to its callback, and parseInt‘s second argument is the radix, so the calls are parseInt('1', 0), parseInt('2', 1), parseInt('3', 2). The pointed lambda arr.map(x => parseInt(x, 10)) is the adapter that shields the callee from the caller’s argument list.
In Elixir, embrace the explicit captures. &suggested?(patron, &1) is not a missing feature; it is the design - partial application you can see in a diff. Use the pipe as the composition mechanism Elixir gives you instead of currying. The pipe |> feeds a value into the first argument position of the next call, which is why Elixir functions take their “most important argument” first (the opposite of Ramda’s data-last convention). A pipeline:
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> Enum.sum()
|> Kernel.*(10)
# => 120
…delivers the readability benefit that currying-plus-composition is after - no intermediate variable names, data-flow order - while keeping every step visible. The limitation is that the piped value can only enter as the first argument, so when you need it somewhere else, leave the pipeline and write a named helper. That’s a trade-off, not a defect.
In both, recognize that “functional” does not mean “curried.” This needs saying because JavaScript FP literature has implied the opposite for a decade. Elixir is unambiguously a functional language - immutable data, pure functions as the norm, pattern matching, pervasive higher-order functions - and it has no currying whatsoever. Currying is one technique for composition, and it only makes sense in a language where arity is not part of a function’s identity. The BEAM’s function model makes arity load-bearing, so composition takes a different shape: visible, named, piped. Both work. Neither is more “functional” than the other.
The honest summary
JavaScript currying works because closures work, and it is fragile because the calling conventions fight it at every step - fn.length lies, this escapes, extra arguments are silently reinterpreted. You can build reliable currying by hand with arrow chains or by library with curryN and explicit arity. Do so when composition genuinely demands it. Elixir currying does not exist because in the BEAM’s function model it can’t - arity is identity, so a “partially applied function” is a category error, and the runtime would charge real closure-allocation cost for it. The capture operator and the pipe provide the same composition power in a different, more explicit shape.
The technique is the same on both sides: fix some arguments, keep going. What differs is whether the language treats that as the default evaluation model (Haskell), a discipline you maintain by hand (JavaScript), or a non-idea you replace with a better-fit tool (Elixir). Know which choice your language made - and why. That is how you stop fighting the tool.
Where to go next
-
Lambda calculus: a formal system in three rules
- currying’s birthplace: where “one argument per step” is the only rule, and the philosophical wrinkle this article names becomes the whole game.
-
a monad is a monoid in the category of endofunctors
- what composition looks like when you stop treating functions as the only composable thing.