2632. Curry


Problem Description

In this problem, you are given a function fn. The task is to create a curried version of this function. The concept of currying comes from functional programming and involves transforming a function that takes multiple arguments into a sequence of functions, each taking a single argument.

For instance, if you have a function sum that adds three numbers like sum(1,2,3), a curried version csum should allow you to call csum(1)(2)(3), csum(1)(2,3), csum(1,2)(3), or csum(1,2,3). Each of these calls should produce the same result as the original sum.

The point of currying is that you can hold ('freeze') some parameters while passing the remaining ones later. This process is useful for creating more specific functions from general-purpose ones and can be a powerful technique in many programming scenarios.

Intuition

The intuition behind creating a curried function starts with understanding that we need to return a function that can take any number of arguments and hold them until we have enough arguments to call the original function fn. Our curried function should return another function if it doesn't have all the necessary arguments, or call the original function with all the available arguments if it does.

To implement this, we:

  1. Define a function curry that accepts a function fn, which is the one we want to transform.
  2. The curry function returns a new function named curried, utilizing closures to keep track of the arguments given so far.
  3. The curried function checks if the number of arguments it has received (args.length) is at least the number of arguments that fn needs (fn.length).
  4. If we have enough arguments, we call fn directly with those arguments.
  5. If we do not yet have all the arguments, curried returns a new function that accepts the next set of arguments (nextArgs). This new function, when called, will concatenate the new arguments to the existing ones and call curried again.
  6. This process continues recursively until curried receives enough arguments to call the original function fn.

This approach allows us to partially apply arguments to the function and call it multiple times with different arguments until all the necessary arguments are provided.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Solution Approach

The implementation of the curry function uses the concepts of closures and recursion in JavaScript, allowing us to progressively accumulate arguments until we are ready to apply them all to the original function. The critical aspects of the solution are as follows:

  1. Closure: A closure is a function that remembers the environment in which it was created. In the curry implementation, the curried function forms a closure over args, which allows it to remember the arguments passed to it across multiple calls.

  2. Recursion: The curried function is recursive, as it can return itself with partially applied arguments until it has enough arguments to apply to fn.

Here's a step-by-step breakdown of the algorithm used in the solution:

  • The curry function takes a function fn as its argument and returns another function curried.

  • The curried function can receive multiple arguments each time it is called (...args in the parameter list is a rest parameter that collects all arguments into an array). It checks if the received arguments are sufficient to call the original function by comparing args.length (the length of the accumulated arguments) with fn.length (the number of parameters expected by fn).

  • If enough arguments are provided (args.length >= fn.length), curried immediately calls fn with those arguments using the spread syntax ...args, which expands the array contents as separate arguments to the function call.

  • If there are not enough arguments yet, curried returns a new function. This new function takes additional arguments (...nextArgs) and again calls curried, this time with both the previously accumulated arguments and the new ones concatenated together (...args, ...nextArgs). This is where the recursion happens.

  • This process continues until the call to curried has enough arguments to call the original function fn, at which point the final value is returned.

By using these techniques, the implementation supports creating a curried function that is flexible in its invocation style.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)

Example Walkthrough

To illustrate the solution approach for creating a curried function, let's use a straightforward sum function that adds two numbers:

1function sum(a, b) {
2  return a + b;
3}

Now, let's go through the process of using the provided solution approach to create a curried version of this function:

  1. First, we define the curry function that converts any given function into a curried function. Assume that it does all the things mentioned in the solution approach.
1function curry(fn) {
2  // Returns the curried function.
3  return function curried(...args) {
4    // If the arguments are sufficient, call the original function.
5    if (args.length >= fn.length) {
6      return fn.apply(this, args);
7    } else {
8      // If not, return a new function waiting for the rest of the arguments.
9      return function (...nextArgs) {
10        // Combine the already given args with the new ones and retry.
11        return curried.apply(this, args.concat(nextArgs));
12      };
13    }
14  };
15}
  1. We transform the sum function into its curried equivalent:
1let curriedSum = curry(sum);
  1. Let's use our new curriedSum in different ways, which shows the flexibility of currying:
1console.log(curriedSum(1)(2)); // Outputs: 3 - as it's a curried function.
2
3console.log(curriedSum(1, 2)); // Also outputs: 3 - it works even if we pass all arguments in one go.

In the first instance, curriedSum(1) doesn't have enough arguments to call sum, so it returns a new function that takes the second argument. Then, when we call this new function with (2), we fulfill the number of arguments, and sum is called with both values.

In the second instance, even though curriedSum is a curried function, we provide all required arguments at once, so it just applies those to sum immediately and returns the result.

This example demonstrates the concept of creating a curried function using closures to maintain state across multiple function calls and recursion to continue gathering arguments until the original function can be called with all of them.

Solution Implementation

1# Function that curries another function
2def curry(fn):
3    # The curried function
4    def curried(*args):
5        # If the number of provided arguments is sufficient, call the original function
6        if len(args) >= fn.__code__.co_argcount:
7            return fn(*args)
8        # Otherwise, return a function that awaits the rest of the arguments
9        else:
10            def curried_more(*more_args):
11                return curried(*(args + more_args))
12            return curried_more
13    return curried
14
15# Example of usage:
16# A simple function that adds two numbers
17def sum(a, b):
18    return a + b
19
20# Curry the 'sum' function
21curried_sum = curry(sum)
22
23# Call the curried 'sum' function with one argument at a time
24result = curried_sum(1)(2) # Returns 3
25
26# Now 'result' variable holds the result of calling 'curried_sum'
27print(result)  # Output will be 3
28
1import java.util.function.BiFunction;
2import java.util.function.Function;
3
4// Interface for a curried function that can accept one argument
5interface CurriedFunction<T, U> {
6    Function<U, T> apply(T t);
7}
8
9public class CurryExample {
10    // Method that curries another BiFunction
11    public static <T, U, R> CurriedFunction<R, U> curry(BiFunction<T, U, R> biFunction) {
12        // The curried function
13        return (T t) -> (U u) -> biFunction.apply(t, u);
14    }
15  
16    // Example of usage:
17    public static void main(String[] args) {
18        // A simple function that adds two numbers
19        BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
20      
21        // Curry the 'sum' function
22        CurriedFunction<Integer, Integer> curriedSum = curry(sum);
23      
24        // Call the curried 'sum' function with one argument at a time
25        Function<Integer, Integer> addToOne = curriedSum.apply(1);
26        Integer result = addToOne.apply(2); // Returns 3
27      
28        // Output the result
29        System.out.println(result); // Prints: 3
30    }
31}
32
1#include <functional>
2#include <iostream>
3
4// Curries another function
5template <typename Function, typename... Args>
6auto curry(Function&& fn, Args&&... args) {
7    // Check if the number of arguments is sufficient to call the function
8    if constexpr (sizeof...(args) >= fn.length) {
9        // If enough arguments are provided, call the original function
10        return std::bind(std::forward<Function>(fn), std::forward<Args>(args)...);
11    } else {
12        // If not enough arguments, return a lambda that takes the rest of the arguments
13        return [=](auto&&... rest) {
14            // Capture current arguments and call `curry` with all of them
15            return curry(fn, args..., std::forward<decltype(rest)>(rest)...);
16        };
17    }
18}
19
20// Example of usage:
21// A simple function that adds two numbers
22int sum(int a, int b) {
23    return a + b;
24}
25
26// Deduction guide for the curry function, to help the compiler deduce the return type
27template <typename Function, typename... Args>
28auto curry(Function&& fn, Args&&... args) -> decltype(auto);
29
30int main() {
31    // Curry the 'sum' function
32    auto curriedSum = curry(sum);
33
34    // Call the curried 'sum' function with one argument at a time
35    auto addOne = curriedSum(1);
36    int result = addOne(2);
37    std::cout << result; // Outputs 3
38
39    return 0;
40}
41
1// Function that curries another function
2function curry(fn: (...args: any[]) => any): (...args: any[]) => any {
3    // The curried function
4    return function curried(...args: any[]): any {
5        // If the number of provided arguments is sufficient, call the original function
6        if (args.length >= fn.length) {
7            return fn(...args);
8        }
9        // Otherwise, return a function that awaits the rest of the arguments
10        return (...nextArgs: any[]) => curried(...args, ...nextArgs);
11    };
12}
13
14// Example of usage:
15// A simple function that adds two numbers
16function sum(a: number, b: number): number {
17  return a + b;
18}
19
20// Curry the 'sum' function
21const curriedSum = curry(sum);
22
23// Call the curried 'sum' function with one argument at a time
24curriedSum(1)(2); // Returns 3
25
Not Sure What to Study? Take the 2-min Quiz:

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.

Time and Space Complexity

Time Complexity

The time complexity of the curry function itself is O(1), as it's simply returning another function. However, the returned curried function can be called multiple times, depending on how many arguments it receives each time. When the final curried function is executed with sufficient arguments, it calls the original function fn.

Assuming fn takes n arguments and has a time complexity of O(f(n)), where f(n) is the complexity of function fn, each partial application of curried is a constant time operation O(1). If a curried function is invoked sequentially for each argument, the overall time complexity for all the calls until fn is invoked with all n arguments would be O(n). Therefore, provided that the function fn has a linear number of arguments, and ignoring the complexity of fn itself, the time complexity of making the series of calls until fn's execution completes would be O(n).

Space Complexity

The space complexity is associated with the closures created for each partial application. Since each call to curried potentially returns a new closure that holds the given arguments, up to n closures could be created where n is the number of arguments fn requires.

Therefore, if there are n arguments, the space complexity would be O(n) due to the n closures that store the accumulated arguments until all are provided.

Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫