LeetCode 2632 - Curry
The problem is asking us to implement currying for a given function. Currying is a functional programming technique where a function with multiple parameters is transformed into a sequence of functions, each accepting a subset of the original parameters.
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
The problem is asking us to implement currying for a given function. Currying is a functional programming technique where a function with multiple parameters is transformed into a sequence of functions, each accepting a subset of the original parameters. Essentially, instead of calling a function with all its arguments at once, you can call it multiple times with fewer arguments, and once all arguments are provided, it returns the final result.
The input to this problem is a function fn with a fixed number of parameters, which can be obtained using fn.length. The expected output is a curried version of fn that can be called with any combination of arguments until all required arguments are supplied. Once the arguments are complete, the curried function returns the same result as the original function. For example, a function sum(a, b, c) can be called as curriedSum(1)(2)(3) or curriedSum(1, 2)(3) or even curriedSum()(1, 2, 3).
The constraints tell us that the number of inputs is reasonable (1 <= inputs.length <= 1000), the number of function parameters is manageable (0 <= fn.length <= 1000), and all parameters are integers (0 <= inputs[i][j] <= 10^5). We are guaranteed that the total number of arguments in inputs matches the arity of fn, which simplifies validation. Edge cases include zero-parameter functions, which should return the result immediately, and functions called with empty arrays, which should still accumulate arguments until the function is ready to execute.
Key observations: the function may be called multiple times with varying numbers of arguments. A naive implementation that does not accumulate arguments will fail for calls like curriedSum(1)(2, 3). Another potential pitfall is calling the function with empty arguments multiple times, which should not disrupt argument accumulation.
Approaches
The brute-force approach would attempt to handle each specific calling pattern explicitly by checking the number of arguments received each time and manually deciding whether to return a result or a nested function. This approach is cumbersome and error-prone because it requires accounting for every combination of argument splits, making the code complex and difficult to maintain.
The optimal approach leverages closure and argument accumulation. We define a recursive function that collects arguments every time it is called. If the total collected arguments are fewer than the original function's arity, the function returns a new function that accepts more arguments. If the collected arguments match the arity, the original function is executed with all collected arguments. This approach is elegant because it naturally handles any combination of argument calls, including empty calls.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Would attempt to enumerate all splits of arguments; inefficient for large n |
| Optimal | O(1) per call | O(n) | Uses closures to accumulate arguments; executes fn only once when all arguments are collected |
Algorithm Walkthrough
- Start by defining a function
currythat accepts the original functionfn. - Inside
curry, define a helper functioncurriedthat maintains a list ofargsaccumulated so far. - Each time
curriedis called, append the new arguments to the existing list of arguments. - Check if the total number of accumulated arguments is less than the arity of
fn(i.e.,fn.length). If it is, return a new function that will continue accumulating arguments. - If the number of accumulated arguments is equal to or exceeds the arity of
fn, callfnwith all collected arguments and return the result. - Return the initial
curriedfunction fromcurry.
Why it works: The invariant is that curried always keeps track of all arguments supplied so far. Since JavaScript and Python functions can be called with any number of arguments, we can accumulate them incrementally. Once the total arguments match the original function's expected count, we know it is safe to execute the function. This guarantees correct results for any valid sequence of function calls.
Python Solution
from typing import Callable, Any
def curry(fn: Callable) -> Callable:
def curried(*args_so_far):
if len(args_so_far) >= fn.__code__.co_argcount:
return fn(*args_so_far)
return lambda *next_args: curried(*args_so_far, *next_args)
return curried
The curry function defines an inner function curried that takes a variable number of arguments. It checks if the total number of accumulated arguments is at least equal to the original function's arity. If so, it executes the original function. Otherwise, it returns a new lambda function that continues to accumulate arguments. The use of *args allows the curried function to accept any number of arguments at each call, including zero.
Go Solution
package main
import "reflect"
func Curry(fn interface{}) interface{} {
fnValue := reflect.ValueOf(fn)
fnType := fnValue.Type()
arity := fnType.NumIn()
var curried func([]reflect.Value) reflect.Value
curried = func(argsSoFar []reflect.Value) reflect.Value {
if len(argsSoFar) >= arity {
result := fnValue.Call(argsSoFar)
return result[0]
}
return reflect.MakeFunc(reflect.FuncOf(fnType.In(len(argsSoFar):), fnType.Out(), fnType.IsVariadic()), func(newArgs []reflect.Value) []reflect.Value {
return []reflect.Value{curried(append(argsSoFar, newArgs...))}
})
}
return curried([]reflect.Value{})
}
In Go, we use the reflect package to handle functions generically, since Go is statically typed and does not natively support variadic currying. The curried function accumulates arguments in a slice of reflect.Value. When the required number of arguments is collected, it calls the original function using Call. Otherwise, it returns a new function using reflect.MakeFunc that continues accumulating arguments.
Worked Examples
Example 1: sum(a, b, c)
| Call | Accumulated Args | Result |
|---|---|---|
curriedSum(1) |
[1] | returns function |
(2) |
[1, 2] | returns function |
(3) |
[1, 2, 3] | returns 6 |
Example 2: curriedSum(1, 2)(3)
| Call | Accumulated Args | Result |
|---|---|---|
curriedSum(1, 2) |
[1, 2] | returns function |
(3) |
[1, 2, 3] | returns 6 |
Example 3: curriedSum()()(1, 2, 3)
| Call | Accumulated Args | Result |
|---|---|---|
curriedSum() |
[] | returns function |
() |
[] | returns function |
(1, 2, 3) |
[1, 2, 3] | returns 6 |
Example 4: life()
| Call | Accumulated Args | Result |
|---|---|---|
curriedLife() |
[] | returns 42 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) per call | Each curried call only appends arguments and may return a lambda; execution of fn occurs once |
| Space | O(n) | Accumulates arguments in a closure until execution; requires space proportional to function arity |
The complexity is efficient because argument accumulation is lightweight, and the function executes only once. The recursion depth corresponds to the number of times the curried function is called, but this is bounded by fn.length.
Test Cases
def test_curry():
def sum3(a, b, c):
return a + b + c
csum = curry(sum3)
assert csum(1)(2)(3) == 6 # sequential calls
assert csum(1, 2)(3) == 6 # partial arguments
assert csum()(1, 2, 3) == 6 # empty calls
def life():
return 42
clife = curry(life)
assert clife() == 42 # zero-parameter function
def add4(a, b, c, d):
return a + b + c + d
cadd4 = curry(add4)
assert cadd4(1, 2)(3, 4) == 10
assert cadd4()(1)(2)(3)(4) == 10
assert cadd4(1)(2, 3, 4) == 10
test_curry()
| Test | Why |
|---|---|
csum(1)(2)(3) |
Checks sequential currying with one argument at a time |
csum(1, 2)(3) |
Checks partial arguments at each step |
csum()(1, 2, 3) |
Checks empty calls before providing arguments |
clife() |
Checks zero-parameter function handling |
cadd4(1, 2)(3, 4) |
Checks multiple arguments at different steps |