Partial Application and Currying
Overview
Suppose each product price needs 10% VAT. Calling addTax(rate, price) repeats the same 0.10m at every call site, and a mistyped 0.01m changes the rate for that call alone. To pass it to a single-argument operation such as Select, we also keep writing a wrapper like price => addTax(0.10m, price).
Partial application fills in some arguments now to make a function for the remaining ones. Fixing the rate at 0.10 gives withVat, which accepts only a price later; a function that keeps hold of variables from the scope where it was created is a closure.
Currying changes the function's shape: transforming (rate, price) => ... into rate => price => ... makes the result of the first call a function waiting for the second argument. Supplying only that first argument is partial application, which explains why the terms often get mixed up even though currying transforms the function and partial application fills in arguments.
Flow
Filling the rate into addTax directly or supplying it to the curried addTaxCurried produces the same shape of function, one that accepts a price. The calculation happens when it receives the remaining argument, 1000m.
History
The observation that a function of several arguments can be treated as a single-argument function goes back to Frege. The Stanford Encyclopedia of Philosophy entry on lambda calculus explains that, in Grundgesetze der Arithmetik (1893), Frege considered single-argument functions sufficient for studying functions. It calls the procedure currying while noting that naming it after Frege might be more historically accurate.1
Moses Schönfinkel introduced combinators in his 1924 paper, translated as "On the building blocks of mathematical logic," to eliminate bound variables from logical formulas. The same encyclopedia's entry on combinatory logic explains that an intermediate step relies on treating several-argument functions as single-argument ones, the idea commonly called currying. This was Schönfinkel's only published paper on combinatory logic; Haskell B. Curry later rediscovered and substantially developed the subject.2
"A History of Haskell," written by the language's designers, traces the tradition from Frege through Schönfinkel and Haskell Curry and its eventual name, currying. It also notes that Haskell supports both curried definitions and definitions taking a single tuple.3
Why use it?
Once a configuration argument is filled in, callers supply only what remains instead of repeating a relatively stable tax rate or currency. A function that calculates an amount, such as Func<decimal, decimal>, can be passed to LINQ Select. Where needs a function returning true or false, such as Func<decimal, bool>, so a partially applied function must also have the right return type. PriorityFirst(productId) in the Strategy Pattern article follows the same arrangement: it accepts the product ID to prioritize and returns a sorting function with that choice already supplied.
C# expresses currying and partial application with lambdas. Methods take their arguments together, so the curried form uses nested lambdas and a type such as Func<decimal, Func<decimal, decimal>>; with just three arguments, both the type and f(a)(b)(c) can become awkward to read. To fill arguments from the front in this way, put earlier decisions, such as configuration, first.
A C# lambda captures a variable rather than a frozen copy of its value, so changing that variable or the object it refers to can change the result of a function already created. Capturing lambdas also involve heap allocation in the current .NET implementation, giving another reason to build and reuse a function for a fixed configuration rather than recreate it inside a loop.4
Example
Call the same tax calculation three ways. withVat partially applies the rate to addTax, while addTaxCurried expresses the calculation in curried form.
using System;
// The original function takes a rate and a price together.
Func<decimal, decimal, decimal> addTax = (rate, price) => price * (1 + rate);
// Partial application fixes rate at 0.10, producing a function that takes only price.
Func<decimal, decimal> withVat = price => addTax(0.10m, price);
// Currying makes the rate-taking function return a price-taking function.
Func<decimal, Func<decimal, decimal>> addTaxCurried = rate => price => price * (1 + rate);
Console.WriteLine(addTax(0.10m, 1000m));
Console.WriteLine(withVat(1000m));
Console.WriteLine(addTaxCurried(0.10m)(1000m));1100.00
1100.00
1100.00All three calls return the same value. Both withVat and addTaxCurried(0.10m) take only a price.
A broader example
A formatter taking a currency, a number of decimal places, and an amount is curried, then specialized once per currency and passed to Select. The tax calculator validates its rate when the function is created. The final two parts show how capture behaves through their output.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
// Fix the culture so the output is independent of machine locale.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// A curried formatter takes currency, decimal places, and amount one at a time.
Func<string, Func<int, Func<decimal, string>>> formatPrice =
currency => decimals => amount => currency + " " + amount.ToString("N" + decimals);
// Choose currency and precision once, then pass the function directly to Select.
Func<decimal, string> krw = formatPrice("KRW")(0);
Func<decimal, string> usd = formatPrice("USD")(2);
decimal[] prices = [1200m, 3450.5m];
Console.WriteLine(string.Join(" | ", prices.Select(krw)));
Console.WriteLine(string.Join(" | ", prices.Select(usd)));
// Validate the fixed argument once, when creating the function.
foreach (var rate in new[] { 0.10m, 1.5m })
{
var calculator = TaxCalculator(rate);
Console.WriteLine(calculator is null
? $"rate {rate}: rejected"
: $"rate {rate}: {calculator(1000m)}");
}
// A lambda captures the variable rather than a frozen copy of its value.
var settings = new TaxSettings { Rate = 0.10m };
Func<decimal, decimal> live = price => price * (1 + settings.Rate);
var fixedRate = settings.Rate; // Copy the current value into a local variable.
Func<decimal, decimal> snapshot = price => price * (1 + fixedRate);
settings.Rate = 0.20m;
Console.WriteLine($"live: {live(1000m)}, snapshot: {snapshot(1000m)}");
// for shares one i across iterations; foreach introduces a new n each time.
var fromFor = new List<Func<int>>();
for (var i = 0; i < 3; i++) fromFor.Add(() => i);
var fromForeach = new List<Func<int>>();
foreach (var n in new[] { 0, 1, 2 }) fromForeach.Add(() => n);
Console.WriteLine("for: " + string.Join(",", fromFor.Select(f => f())));
Console.WriteLine("foreach: " + string.Join(",", fromForeach.Select(f => f())));
// Reject rates outside 0 to 1 before creating the calculator.
static Func<decimal, decimal>? TaxCalculator(decimal rate) =>
rate is < 0m or > 1m
? null
: price => decimal.Round(price * (1 + rate), 0, MidpointRounding.AwayFromZero);
sealed class TaxSettings
{
public decimal Rate { get; set; }
}KRW 1,200 | KRW 3,451
USD 1,200.00 | USD 3,450.50
rate 0.10: 1100
rate 1.5: rejected
live: 1200.00, snapshot: 1100.00
for: 3,3,3
foreach: 0,1,2The N0 format displays 3450.5 rounded to 3,451. A rate of 1.5 is rejected when the calculator is created, before any price is calculated. live captures the settings object and reads its later rate, 0.20, whereas snapshot uses the 0.10 copied before the function was created. A for loop has one i variable for the whole loop, so all three lambdas read its final value, 3; foreach creates a variable per iteration, producing 0, 1, and 2.5
A null from TaxCalculator says only that the rate was rejected; a result type can carry the reason when that is needed. The curried formatter does not validate combinations of currency and decimal places, and a function with a fixed rate must be rebuilt when the rate changes. Its creator also chooses whether to copy the initial value or keep reading the settings object.
TypeScript example
TypeScript curries a function with nested arrow functions, and JavaScript Function.prototype.bind also supports partial application. Its first argument supplies this; the following arguments fill the target function's leading parameters. The loop examples contrast what var and let capture.67
// A curried formatter takes currency, decimal places, and amount one at a time.
const formatPrice = (currency: string) => (decimals: number) => (amount: number): string =>
currency + " " + amount.toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
const krw = formatPrice("KRW")(0);
const usd = formatPrice("USD")(2);
const prices = [1200, 3450.5];
console.log(prices.map(krw).join(" | "));
console.log(prices.map(usd).join(" | "));
// JavaScript bind supports partial application of leading arguments.
function addTax(rate: number, price: number): number {
return Math.round(price * (1 + rate)); // Round to an integer because number uses floating-point arithmetic.
}
const withVat = addTax.bind(null, 0.1);
console.log("withVat: " + withVat(1000));
// The closure reads the object later; copy its value now to keep a snapshot.
const settings = { rate: 0.1 };
const live = (price: number) => Math.round(price * (1 + settings.rate));
const fixedRate = settings.rate;
const snapshot = (price: number) => Math.round(price * (1 + fixedRate));
settings.rate = 0.2;
console.log(`live: ${live(1000)}, snapshot: ${snapshot(1000)}`);
// var shares one variable across iterations; let creates a binding each time.
const fromVar: Array<() => number> = [];
for (var i = 0; i < 3; i++) fromVar.push(() => i);
const fromLet: Array<() => number> = [];
for (let j = 0; j < 3; j++) fromLet.push(() => j);
console.log("var: " + fromVar.map((f) => f()).join(","));
console.log("let: " + fromLet.map((f) => f()).join(","));KRW 1,200 | KRW 3,451
USD 1,200.00 | USD 3,450.50
withVat: 1100
live: 1200, snapshot: 1100
var: 3,3,3
let: 0,1,2bind also fills arguments from the front, with null as its first argument when the function does not use this. As in C#, live reads the settings object and follows the changed rate.
See also
Example environment: each C# example is a separate .NET 10 console program; compile TypeScript for ES2022 or later, then run the JavaScript with Node.js.
Footnotes
- Alama, Jesse; Korbmacher, Johannes. "The Lambda Calculus". Stanford Encyclopedia of Philosophy (revised 2023). §3 Brief history of λ-calculus: Frege (1893) and the name currying. ↩
- Bimbó, Katalin. "Combinatory Logic". Stanford Encyclopedia of Philosophy (revised 2024). §1.2 and notes 1 and 7. The English translation of Schönfinkel (1924) appears in van Heijenoort (ed.), From Frege to Gödel (1967), pp. 355-366. ↩
- Hudak, Paul; Hughes, John; Peyton Jones, Simon; Wadler, Philip (2007). "A History of Haskell: Being Lazy With Class". HOPL III. §4.2 Functions and function application. ↩
- Microsoft Learn. Lambda expressions: Capture of outer variables and variable scope. A captured variable remains alive until delegates referring to it become eligible for collection; other lambdas capturing it observe the changed value too. ↩
- Microsoft Learn. C# specification, §13.9.5.2 Synchronous foreach. The iteration variable is declared inside the loop, so each iteration captures a separate variable. ↩
- MDN. Function.prototype.bind(). Partially applied functions section. ↩
- MDN. for. Lexical declarations in the initialization block section. ↩