Facebook Pixel

2629. Function Composition

Problem Description

This problem asks you to create a function that performs function composition on an array of functions.

Function composition means applying multiple functions in sequence, where the output of one function becomes the input of the next. When you have functions [f1, f2, f3, ..., fn], the composed function should work from right to left.

For example:

  • If you have [f(x), g(x), h(x)], the composition creates a new function where fn(x) = f(g(h(x)))
  • This means h is applied first to x, then g is applied to that result, and finally f is applied to that result

Key requirements:

  • Each function in the input array takes one integer as input and returns one integer as output
  • The composed function should also take one integer and return one integer
  • If the array is empty, return the identity function (a function that returns its input unchanged: f(x) = x)

Example walkthrough:

  • Given compose([x => x + 1, x => 2 * x]) and calling fn(4):
    1. Start with x = 4
    2. Apply the rightmost function: 2 * 4 = 8
    3. Apply the next function: 8 + 1 = 9
    4. Return 9

The solution uses reduceRight to iterate through the functions array from right to left, applying each function to the accumulated result, starting with the initial input value x.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that function composition naturally follows a right-to-left execution pattern. When we write f(g(h(x))), we evaluate from the innermost function outward - first h(x), then g of that result, then f of that result.

This right-to-left pattern immediately suggests using reduceRight as our core mechanism. The reduceRight method processes array elements from right to left, which perfectly matches our composition order.

Think of it like a pipeline running backwards:

  • We start with our initial value x
  • We pass it through the rightmost function
  • The output becomes the input for the next function to the left
  • This continues until we've processed all functions

Why reduceRight is the natural choice:

  1. It automatically handles the right-to-left iteration we need
  2. It maintains an accumulator (acc) that carries the result from one function to the next
  3. It elegantly handles the empty array case - when there are no functions, reduceRight returns the initial value unchanged, giving us the identity function behavior for free

The solution structure becomes clear:

  • Return a new function that captures the functions array in its closure
  • When this new function is called with x, use reduceRight to thread x through all functions
  • Each step applies the current function fn to the accumulated result acc
  • The initial accumulator value is x itself

This approach is both concise and efficient, avoiding the need for explicit loops or recursion while naturally handling all edge cases.

Solution Approach

The implementation uses a higher-order function pattern - a function that returns another function. Let's break down the solution step by step:

Function Signature:

function compose(functions: F[]): F
  • Takes an array of functions as input
  • Returns a new function of the same type F (a function that accepts and returns a number)

Creating the Composed Function:

return function (x) {
    return functions.reduceRight((acc, fn) => fn(acc), x);
};

The solution creates and returns an anonymous function that:

  1. Accepts a single parameter x (the initial input)
  2. Uses reduceRight to process the functions array

How reduceRight Works Here:

  • Callback function: (acc, fn) => fn(acc)
    • acc is the accumulator (the running result)
    • fn is the current function being processed
    • Simply applies the current function to the accumulated value
  • Initial value: x (the input to our composed function)
  • Direction: Processes array from right to left

Execution Flow Example: For compose([f, g, h])(x):

  1. Start with accumulator = x
  2. Process h: accumulator = h(x)
  3. Process g: accumulator = g(h(x))
  4. Process f: accumulator = f(g(h(x)))
  5. Return final accumulator value

Edge Case Handling:

  • Empty array: When functions is empty, reduceRight returns the initial value x unchanged, automatically giving us the identity function behavior
  • Single function: Works correctly, applying just that one function
  • Multiple functions: Chains them in the correct right-to-left order

The beauty of this solution lies in its simplicity - reduceRight naturally handles the composition pattern without any explicit conditionals or loops, making the code both readable and efficient.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through a concrete example to understand how the solution works.

Given:

functions = [x => x + 1, x => x * x, x => 2 * x]
compose(functions)(5)

Step-by-step execution:

  1. Initial Setup:

    • We call the composed function with x = 5
    • reduceRight starts from the rightmost function
    • Initial accumulator = 5
  2. First iteration (rightmost function: x => 2 * x):

    • Current function: fn = x => 2 * x
    • Current accumulator: acc = 5
    • Apply function: fn(acc) = 2 * 5 = 10
    • New accumulator = 10
  3. Second iteration (middle function: x => x * x):

    • Current function: fn = x => x * x
    • Current accumulator: acc = 10
    • Apply function: fn(acc) = 10 * 10 = 100
    • New accumulator = 100
  4. Third iteration (leftmost function: x => x + 1):

    • Current function: fn = x => x + 1
    • Current accumulator: acc = 100
    • Apply function: fn(acc) = 100 + 1 = 101
    • New accumulator = 101
  5. Final Result:

    • reduceRight completes and returns 101
    • The composed function returns 101

Verification: This is equivalent to computing (x + 1)((x * x)(2 * x)) with x = 5:

  • First: 2 * 5 = 10
  • Then: 10 * 10 = 100
  • Finally: 100 + 1 = 101

Empty Array Case:

functions = []
compose(functions)(5)
  • reduceRight with empty array returns initial value unchanged
  • Result: 5 (identity function behavior)

Solution Implementation

1from typing import List, Callable
2
3# Type definition for a function that takes a number and returns a number
4F = Callable[[float], float]
5
6def compose(functions: List[F]) -> F:
7    """
8    Composes multiple functions into a single function that applies them from right to left
9  
10    Args:
11        functions: Array of functions to compose
12      
13    Returns:
14        A new function that is the composition of all input functions
15    """
16    # Return a new function that takes a number as input
17    def composed_function(x: float) -> float:
18        # Apply functions from right to left
19        # Starting with x as the initial value, each function transforms the accumulated result
20        accumulator = x
21        # Iterate through functions in reverse order (right to left)
22        for current_function in reversed(functions):
23            # Apply the current function to the accumulated value
24            accumulator = current_function(accumulator)
25        return accumulator
26  
27    return composed_function
28
29"""
30Example usage:
31fn = compose([lambda x: x + 1, lambda x: 2 * x])
32result = fn(4)  # Returns 9
33Explanation: First applies (2 * x) to 4 getting 8, then applies (x + 1) to 8 getting 9
34"""
35
1import java.util.List;
2import java.util.function.Function;
3
4/**
5 * Solution class for function composition problem
6 */
7public class Solution {
8  
9    /**
10     * Composes multiple functions into a single function that applies them from right to left
11     * @param functions - List of functions to compose
12     * @return A new function that is the composition of all input functions
13     */
14    public Function<Integer, Integer> compose(List<Function<Integer, Integer>> functions) {
15        // Return a new function that takes an Integer as input
16        return new Function<Integer, Integer>() {
17            @Override
18            public Integer apply(Integer x) {
19                // Initialize result with the input value
20                Integer result = x;
21              
22                // Apply functions from right to left by iterating backwards through the list
23                for (int i = functions.size() - 1; i >= 0; i--) {
24                    // Apply the current function to the accumulated result
25                    Function<Integer, Integer> currentFunction = functions.get(i);
26                    result = currentFunction.apply(result);
27                }
28              
29                // Return the final transformed value
30                return result;
31            }
32        };
33    }
34  
35    /**
36     * Example usage:
37     * List<Function<Integer, Integer>> functions = Arrays.asList(
38     *     x -> x + 1,
39     *     x -> 2 * x
40     * );
41     * Function<Integer, Integer> fn = compose(functions);
42     * Integer result = fn.apply(4); // Returns 9
43     * Explanation: First applies (2 * x) to 4 getting 8, then applies (x + 1) to 8 getting 9
44     */
45}
46
1#include <vector>
2#include <functional>
3
4// Type definition for a function that takes a number and returns a number
5using F = std::function<int(int)>;
6
7/**
8 * Composes multiple functions into a single function that applies them from right to left
9 * @param functions - Vector of functions to compose
10 * @return A new function that is the composition of all input functions
11 */
12F compose(std::vector<F> functions) {
13    // Return a new lambda function that takes an integer as input
14    return [functions](int x) -> int {
15        // Apply functions from right to left
16        // Start with x as the initial value
17        int result = x;
18      
19        // Iterate through functions in reverse order (right to left)
20        for (int i = functions.size() - 1; i >= 0; i--) {
21            // Apply the current function to the accumulated result
22            result = functions[i](result);
23        }
24      
25        return result;
26    };
27}
28
29/**
30 * Example usage:
31 * auto fn = compose({
32 *     [](int x) { return x + 1; },
33 *     [](int x) { return 2 * x; }
34 * });
35 * int result = fn(4); // Returns 9
36 * Explanation: First applies (2 * x) to 4 getting 8, then applies (x + 1) to 8 getting 9
37 */
38
1// Type definition for a function that takes a number and returns a number
2type F = (x: number) => number;
3
4/**
5 * Composes multiple functions into a single function that applies them from right to left
6 * @param functions - Array of functions to compose
7 * @returns A new function that is the composition of all input functions
8 */
9function compose(functions: F[]): F {
10    // Return a new function that takes a number as input
11    return function (x: number): number {
12        // Apply functions from right to left using reduceRight
13        // Starting with x as the initial value, each function transforms the accumulated result
14        return functions.reduceRight((accumulator: number, currentFunction: F) => {
15            // Apply the current function to the accumulated value
16            return currentFunction(accumulator);
17        }, x);
18    };
19}
20
21/**
22 * Example usage:
23 * const fn = compose([x => x + 1, x => 2 * x])
24 * fn(4) // Returns 9
25 * Explanation: First applies (2 * x) to 4 getting 8, then applies (x + 1) to 8 getting 9
26 */
27

Time and Space Complexity

Time Complexity: O(n) where n is the number of functions in the array.

When compose is called, it returns a new function immediately in O(1) time. However, when the returned function is invoked with a value x, it executes reduceRight which iterates through all n functions in the array, applying each function exactly once. Each function application is assumed to be O(1), so the total time complexity of executing the composed function is O(n).

Space Complexity: O(n) where n is the number of functions in the array.

The space complexity includes:

  • The returned function holds a reference to the functions array through closure, which takes O(n) space
  • During execution of reduceRight, the call stack depth is O(1) since reduceRight is implemented iteratively in JavaScript
  • The intermediate value acc takes O(1) space

Therefore, the overall space complexity is O(n) due to storing the reference to the functions array in the closure.

Common Pitfalls

1. Incorrect Order of Composition

The most common mistake is applying functions in left-to-right order instead of right-to-left. Many developers intuitively think of reading the array from start to end.

Wrong Implementation:

def compose(functions: List[F]) -> F:
    def composed_function(x: float) -> float:
        accumulator = x
        for fn in functions:  # Wrong: iterates left to right
            accumulator = fn(accumulator)
        return accumulator
    return composed_function

Impact: For compose([f, g, h])(x), this gives f(g(h(x))) instead of the correct f(g(h(x))). Wait, that looks the same! But consider compose([x => x + 1, x => 2 * x])(4):

  • Wrong: (4 + 1) * 2 = 10
  • Correct: (2 * 4) + 1 = 9

Solution: Always use reversed() or iterate backwards through the array.

2. Mutating the Original Functions Array

Some implementations might accidentally modify the input array when trying to reverse it.

Wrong Implementation:

def compose(functions: List[F]) -> F:
    functions.reverse()  # Mutates the original array!
    def composed_function(x: float) -> float:
        accumulator = x
        for fn in functions:
            accumulator = fn(accumulator)
        return accumulator
    return composed_function

Solution: Use reversed() which returns an iterator without modifying the original list, or create a copy before reversing.

3. Type Confusion with Return Values

Forgetting that each function must return a value that can be passed to the next function.

Wrong Usage:

# This will fail if any function returns None
functions = [
    lambda x: print(x),  # Returns None!
    lambda x: x * 2
]

Solution: Ensure all functions in the array return appropriate values:

functions = [
    lambda x: (print(f"Value: {x}"), x)[1],  # Prints and returns x
    lambda x: x * 2
]

4. Not Handling Edge Cases Properly

Explicitly checking for empty arrays with unnecessary conditional logic.

Overcomplicated Implementation:

def compose(functions: List[F]) -> F:
    def composed_function(x: float) -> float:
        if len(functions) == 0:  # Unnecessary check
            return x
        accumulator = x
        for fn in reversed(functions):
            accumulator = fn(accumulator)
        return accumulator
    return composed_function

Solution: The loop naturally handles empty arrays - when there are no functions to iterate over, the accumulator remains as the initial value x, giving us the identity function behavior automatically.

5. Performance Issues with Large Function Arrays

Creating unnecessary intermediate data structures or copies.

Inefficient Implementation:

def compose(functions: List[F]) -> F:
    def composed_function(x: float) -> float:
        reversed_list = list(reversed(functions))  # Creates unnecessary copy
        accumulator = x
        for fn in reversed_list:
            accumulator = fn(accumulator)
        return accumulator
    return composed_function

Solution: Use reversed() directly as an iterator without converting to a list, or use negative indexing to iterate backwards without creating any copies:

def compose(functions: List[F]) -> F:
    def composed_function(x: float) -> float:
        accumulator = x
        for i in range(len(functions) - 1, -1, -1):
            accumulator = functions[i](accumulator)
        return accumulator
    return composed_function
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More