| name | curryfy |
| description | Use when applying function currying and partial application patterns in C# to create specialized, reusable function compositions.
USE FOR: function currying, partial application, function composition, creating specialized functions from general ones, builder-like functional APIs
DO NOT USE FOR: full functional programming library (use language-ext), parser combinators (use pidgin or fparsec), optional value handling (use optional)
|
| license | MIT |
| metadata | {"displayName":"Currying & Partial Application","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | claude, copilot, cursor |
| references | [{"title":"Curryfy GitHub Repository","url":"https://github.com/leandromoh/Curryfy"},{"title":"Curryfy NuGet Package","url":"https://www.nuget.org/packages/Curryfy"}] |
Currying & Partial Application in C#
Overview
Currying transforms a function that takes multiple arguments into a sequence of functions, each accepting a single argument. Partial application fixes one or more arguments of a function, producing a new function with fewer parameters. While C# is not a functional-first language, these patterns are directly expressible with Func<> delegates and lambda expressions. The Curryfy NuGet package and similar libraries provide extension methods to curry standard Func<> delegates automatically, but the patterns can also be implemented manually for full control.
Currying vs Partial Application
| Concept | Description | Result |
|---|
| Currying | Transforms f(a, b, c) into f(a)(b)(c) | Chain of single-argument functions |
| Partial application | Fixes some arguments of f(a, b, c) | New function with fewer parameters |
| Composition | Chains f and g into g(f(x)) | Single function from two functions |
Basic Currying
static decimal CalculatePrice(decimal basePrice, decimal taxRate, decimal discount)
=> (basePrice * (1 + taxRate)) - discount;
static Func<decimal, Func<decimal, decimal>> CurriedPrice(decimal basePrice)
=> taxRate => discount => (basePrice * (1 + taxRate)) - discount;
var priceWith20Tax = CurriedPrice(100m)(0.20m);
var finalPrice = priceWith20Tax(10m);
public static class CurryExtensions
{
public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(
this Func<T1, T2, TResult> func)
=> a => b => func(a, b);
public static Func<T1, Func<T2, Func<T3, >>> <, , , >()
=> a => b => c => func(a, b, c);
=> a => b => c => d => func(a, b, c, d);
}
Func<, , , > calcPrice = CalculatePrice;
curried = calcPrice.Curry();
withBase = curried(m);
withTax = withBase(m);
result = withTax(m);
Partial Application
public static class PartialExtensions
{
public static Func<T2, TResult> Partial<T1, T2, TResult>(
this Func<T1, T2, TResult> func, T1 arg1)
=> arg2 => func(arg1, arg2);
public static Func<T3, TResult> Partial<T1, T2, T3, TResult>(
this Func<T1, T2, T3, TResult> func, T1 arg1, T2 arg2)
=> arg3 => func(arg1, arg2, arg3);
public static Func<T2, T3, TResult> Partial<T1, T2, T3, TResult>(
this Func<T1, T2, T3, TResult> func, T1 arg1)
=> (arg2, arg3) => func(arg1, arg2, arg3);
}
Func<string, int, int, IEnumerable<Order>> searchOrders =
(customerId, page, pageSize) =>
db.Orders
.Where(o => o.CustomerId == customerId)
.Skip((page - ) * pageSize)
.Take(pageSize)
.ToList();
customerOrders = searchOrders.Partial();
page1 = customerOrders(, );
page2 = customerOrders(, );
Function Composition
public static class ComposeExtensions
{
public static Func<T, TResult2> Compose<T, TResult1, TResult2>(
this Func<TResult1, TResult2> g,
Func<T, TResult1> f)
=> x => g(f(x));
public static Func<T, TResult2> Then<T, TResult1, TResult2>(
this Func<T, TResult1> f,
Func<TResult1, TResult2> g)
=> x => g(f(x));
}
Func<string, string> trim = s => s.Trim();
Func<string, string> toLower = s => s.ToLowerInvariant();
Func<string, string> removeSpaces = s => s.Replace(" ", "-");
var slugify = trim
.Then(toLower)
.Then(removeSpaces);
var slug = slugify(" Hello World ");
Practical Patterns
Configuration Builders
Func<string, Func<TimeSpan, Func<int, HttpClient>>> createClient =
baseUrl => timeout => maxRetries =>
{
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
};
var client = new HttpClient(handler)
{
BaseAddress = new Uri(baseUrl),
Timeout = timeout
};
client.DefaultRequestHeaders.Add("X-Max-Retries", maxRetries.ToString());
return client;
};
var productionClient = createClient("https://api.example.com")(TimeSpan.FromSeconds(30));
var devClient = createClient("https://localhost:5001")(TimeSpan.FromSeconds(5));
var apiClient = productionClient(3);
var testClient = devClient(0);
Validation Pipelines
Func<string, bool> notEmpty = s => !string.IsNullOrWhiteSpace(s);
Func<string, bool> maxLength100 = s => s.Length <= 100;
Func<string, bool> validEmail = s => s.Contains('@') && s.Contains('.');
public static Func<T, bool> And<T>(
this Func<T, bool> left,
Func<T, bool> right)
=> x => left(x) && right(x);
var validateEmail = notEmpty
.And(maxLength100)
.And(validEmail);
var isValid = validateEmail("user@example.com");
var isInvalid = validateEmail("");
Middleware-Like Pipelines
Func<Func<Order, Task<Order>>, Func<Order, Task<Order>>> withLogging =
next => async order =>
{
Console.WriteLine($"Processing order {order.Id}");
var result = await next(order);
Console.WriteLine($"Completed order {order.Id}");
return result;
};
Func<Func<Order, Task<Order>>, Func<Order, Task<Order>>> withValidation =
next => async order =>
{
if (order.Total <= 0)
throw new ValidationException("Invalid total");
return await next(order);
};
Func<Order, Task<Order>> processOrder = async order =>
{
order.Status = OrderStatus.Processed;
return order;
};
var pipeline = withLogging(withValidation(processOrder));
var result = await pipeline(new Order { Id = 1, Total = 50m });
Dependency Injection Factories
Func<ILogger, IDbConnection, string, IOrderService> createService =
(logger, connection, region) =>
new OrderService(logger, connection, region);
builder.Services.AddScoped<IOrderService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<OrderService>>();
var connection = sp.GetRequiredService<IDbConnection>();
var factory = createService.Partial(logger).Partial(connection);
return factory("us-east-1");
});
Curryfy NuGet Package
using Curryfy;
Func<int, int, int, int> add3 = (a, b, c) => a + b + c;
var curried = add3.Curry();
var result = curried(1)(2)(3);
var add1 = add3.Partial(1);
var add1and2 = add3.Partial(1, 2);
Best Practices
- Use currying to create families of related functions from a single general function, fixing environment or configuration parameters early (e.g.,
createLogger("orders") returns a specialized logger).
- Prefer partial application over currying when you only need to fix one or two arguments and the remaining function still takes multiple parameters.
- Use function composition (
Then/Compose) to build data transformation pipelines that are more readable than nested function calls.
- Keep curried functions focused: if a function takes more than 3-4 parameters, consider refactoring into an options record or builder pattern instead.
- Document the expected argument order in curried functions, since the first argument is fixed first and determines the specialization hierarchy.
- Use currying for middleware and decorator patterns where each layer wraps the next, creating composable processing pipelines.
- Combine currying with generic type constraints to build type-safe fluent APIs that guide the developer through required configuration steps.
- Avoid excessive currying in team codebases unfamiliar with functional patterns; provide XML documentation and named factory methods as wrappers for discoverability.
- Test curried functions at each level of application to verify that partially applied functions behave correctly with different fixed arguments.
- Use the
Curryfy NuGet package for automatic curry/partial extension methods on standard Func<> delegates rather than implementing your own for every arity.