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 wherefn(x) = f(g(h(x)))
- This means
h
is applied first tox
, theng
is applied to that result, and finallyf
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 callingfn(4)
:- Start with
x = 4
- Apply the rightmost function:
2 * 4 = 8
- Apply the next function:
8 + 1 = 9
- Return
9
- Start with
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
.
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:
- It automatically handles the right-to-left iteration we need
- It maintains an accumulator (
acc
) that carries the result from one function to the next - 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
, usereduceRight
to threadx
through all functions - Each step applies the current function
fn
to the accumulated resultacc
- 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:
- Accepts a single parameter
x
(the initial input) - 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)
:
- Start with accumulator =
x
- Process
h
: accumulator =h(x)
- Process
g
: accumulator =g(h(x))
- Process
f
: accumulator =f(g(h(x)))
- Return final accumulator value
Edge Case Handling:
- Empty array: When
functions
is empty,reduceRight
returns the initial valuex
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 EvaluatorExample 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:
-
Initial Setup:
- We call the composed function with
x = 5
reduceRight
starts from the rightmost function- Initial accumulator =
5
- We call the composed function with
-
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
- Current function:
-
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
- Current function:
-
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
- Current function:
-
Final Result:
reduceRight
completes and returns101
- 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 takesO(n)
space - During execution of
reduceRight
, the call stack depth isO(1)
sincereduceRight
is implemented iteratively in JavaScript - The intermediate value
acc
takesO(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
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!