2693. Call Function with Custom Context


Problem Description

This LeetCode problem requires us to implement a callPolyfill method that mimics the behavior of the JavaScript built-in Function.call method without actually using it. The Function.call method is typically used to invoke a function with a specified this context and arguments. When the new callPolyfill method is called on any function, it should be capable of setting the this context of that function to an object that is passed as the first parameter to callPolyfill. Any subsequent parameters passed to callPolyfill should be treated as arguments to the original function.

For example, if we have a function that calculates tax and logs the total cost of an item, normally calling this function without setting this would result in undefined being used for this.item. But if we enhance the function with the callPolyfill method, we can specify an object that has an item property. The callPolyfill method would then ensure that within the function, this refers to the object provided, allowing the function to access the item property and log the correct message.

The challenge is to enable this functionality for all functions within the global Function prototype, without relying on the existing Function.call method.

Intuition

To arrive at the solution, we must first understand what the Function.prototype.bind method does. The bind method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called. Since we can't use Function.call, Function.prototype.bind becomes a key part of our solution because it allows us to bind the this context to the function.

The intuition then is to create a function from the original function using bind, where the context object is applied as this. The args that follow are then passed into this new function. Essentially, the callPolyfill function does two things:

  1. It binds the given context (the obj) to the function it is called on, creating a new function where this refers to obj.
  2. It immediately invokes this new function with the provided arguments.

When we use this.bind(context), we're creating that new function with the context applied. The rest of the arguments are then spread into the call of this new function using ...args, which takes care of passing the arguments to the function.

This approach works because the intrinsic behavior of JavaScript's functions and the way this can be controlled with bind allows us to redirect the context and arguments. However, this does not alter the original function itself; instead, it creates a new function every time callPolyfill is called, which can be invoked right away with the correct context and arguments.

Remember that extending native prototypes in JavaScript can be dangerous as it might cause conflicts if the environment already has a function with the same name or if future versions of JavaScript implement a function with that name. However, for the purpose of this coding problem, such considerations are out of scope.

Solution Approach

The solution approach draws upon the principles of functional programming that exist within JavaScript. In particular, it relies on the Function.prototype.bind method to implement the functionality of Function.call without directly invoking it.

Here is the step-by-step implementation using the provided TypeScript definition:

  1. First, we extend the global Function interface to include the callPolyfill method. This ensures that TypeScript is aware of the new method and does not complain when it is used.

  2. Next, we provide the actual implementation of callPolyfill by assigning a function to Function.prototype.callPolyfill. This method is intended to be available on all function instances since all functions inherit from Function.prototype.

  3. Inside callPolyfill, we use the bind method. This method is called on the function that callPolyfill is being applied (this refers to the function itself). We pass the context object as the first argument to bind, which will set the value of this within the bound function to the context.

    const fn = this.bind(context);

    After calling bind, we now have a new function fn where this is set to context.

  4. We then immediately invoke this bound function with the rest of the arguments that were passed to callPolyfill, using the spread operator ...args to pass them as individual arguments.

    return fn(...args);

    The new function fn is invoked with the provided arguments, and the result of the invocation is returned. The ...args syntax is used to pass an array of arguments as separate arguments to the function call.

The crucial part of this approach is creating a binding between the function and a specific context, then invoking it with the desired arguments. It's effectively a manual implementation of Function.call, replicating its behavior without utilizing the inbuilt method itself.

The function bind is essential here as the mechanism to preset the this value. In JavaScript, bind is often used when the context of this needs to be explicitly defined ahead of time, especially in cases where the function may be executed in a different scope or as a callback.

The use of the spread operator (...) is also an important aspect of the implementation because it allows an array of arguments to be passed to a function as if they were written out separately, making it very handy when forwarding arguments.

This implementation behaves much like the native Function.call, setting the this context for a function and passing along any given arguments seamlessly, obviating the need for Function.call and showing the flexibility and power of JavaScript's function manipulation capabilities.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

To illustrate the solution approach in the given content, let's consider a simple scenario where we have a function showDetails that needs to access the properties of an object.

Let's define an object and the showDetails function as follows:

let product = {
    name: 'Chocolate',
    price: 1.99
};

function showDetails(taxRate) {
    console.log(`${this.name} costs ${(this.price * (1 + taxRate)).toFixed(2)}`);
}

Normally, you would use showDetails.call(product, 0.08) to call showDetails with this pointing to the product object and the tax rate of 8%, which would output "Chocolate costs 2.15".

However, since the aim is to create a callPolyfill that does not use Function.call, let's walk through how it would be achieved with our solution approach.

  1. We first add the callPolyfill function to the Function.prototype, so it becomes available on all functions:
Function.prototype.callPolyfill = function(context, ...args) {
    const fn = this.bind(context);
    return fn(...args);
};
  1. Now let's use our new callPolyfill method to achieve the same result as the Function.call method.
showDetails.callPolyfill(product, 0.08);

When we use callPolyfill, we are essentially doing the following things:

  • this.bind(context): We are creating a new function where this is permanently set to the product object. Thus, inside showDetails, this.name will refer to 'Chocolate' and this.price will refer to 1.99.

  • fn(...args): The new bound function is called immediately with the taxRate argument spread into it. In our case, it is as if we called fn(0.08), which results in the showDetails function being executed with the tax rate as its argument.

The execution of showDetails.callPolyfill(product, 0.08) will output the expected "Chocolate costs 2.15" by setting this to the product object and passing the tax rate correctly to the function. This example demonstrates how the callPolyfill method functions similarly to the native Function.call method, providing the necessary functionality without directly using Function.call.

Solution Implementation

1class FunctionPolyfill:
2    def call_polyfill(self, context, *args):
3        """Simulate the `Function.prototype.call` method in Python.
4      
5        This method allows binding an object's attributes to a function,
6        enabling the function to access those attributes as self attributes.
7      
8        Args:
9            context (dict): The object whose attributes should be bound.
10            *args: Variable length argument list.
11      
12        Returns:
13            The return value of the function after being called with the bound context and arguments.
14        """
15
16        # A closure is defined to capture the function (self) and the provided context.
17        def method(*inner_args):
18            return self(context, *inner_args)
19
20        # The context's attributes are added to the function as self attributes.
21        self.__dict__.update(context)
22
23        # Call the closure with any additional arguments provided.
24        return method(*args)
25
26
27# An example function 'increment' that increases a 'count' attribute in a context.
28def increment(context):
29    """Increment a 'count' attribute within the given context.
30
31    Args:
32        context (dict): The context object with 'count' attribute.
33  
34    Returns:
35        int: The incremented 'count' value.
36    """
37    context['count'] += 1
38    return context['count']
39
40
41# Instantiate the FunctionPolyfill class to use the 'call_polyfill'.
42increment_function = FunctionPolyfill()
43
44# Apply the 'call_polyfill' function, setting 'this' (context) to an object with a 'count' property.
45# The result should be 2, as 'call_polyfill' increments the count from 1.
46result = increment_function.call_polyfill({'count': 1}, increment)  # result is 2
47
48# Print the result to verify the correctness of the 'call_polyfill' implementation.
49print(result)  # Expected output: 2
50
1import java.lang.reflect.Method;
2import java.util.HashMap;
3
4// Create a CallPolyfill interface with a polyfillMethod that needs to be implemented
5interface CallPolyfill {
6    Object polyfillMethod(Object... args);
7}
8
9public class FunctionPolyfill {
10
11    // Static method that takes a method, a context object, and varargs for parameters
12    // It mimics the call() function from JavaScript
13    public static Object callPolyfill(Method method, Object context, Object... args) throws Exception {
14        // Sets the context object's class as the method's declaring class if it's not already the same
15        if (!context.getClass().equals(method.getDeclaringClass())) {
16            throw new IllegalArgumentException("Incompatible method context provided.");
17        }
18
19        // Invokes the method on the context with the provided arguments
20        return method.invoke(context, args);
21    }
22
23    // Example usage
24    public static void main(String[] args) {
25        try {
26            // Instantiate an example context object (HashMap simulates a JavaScript object with key-value pairs)
27            HashMap<String, Integer> context = new HashMap<>();
28            context.put("count", 1);
29
30            // Create an instance of the class where increment method is defined
31            Incrementer incrementer = new Incrementer();
32
33            // Get the increment method from the Incrementer instance
34            Method incrementMethod = Incrementer.class.getMethod("increment");
35
36            // Apply the polyfill function to the increment method,
37            // setting 'this' to refer to a context with a count property.
38            // The result should be 2, as it increments the count from 1.
39            Object result = callPolyfill(incrementMethod, incrementer, context);
40
41            // Logging the result to verify the correctness of the callPolyfill implementation.
42            System.out.println(result); // Expected output: 2
43
44        } catch (Exception e) {
45            e.printStackTrace();
46        }
47    }
48}
49
50class Incrementer implements CallPolyfill {
51    private HashMap<String, Integer> context;
52
53    // Example method that increments a count value within a context
54    @Override
55    public Object polyfillMethod(Object... args) {
56        this.context = (HashMap<String, Integer>) args[0];
57        this.context.put("count", this.context.get("count") + 1);
58        return this.context.get("count");
59    }
60
61    // Need to define an empty constructor since it is required when we reflectively invoke the method.
62    public Incrementer() {
63        // Empty constructor
64    }
65
66    // Public method to be referred by the reflect method calling
67    public int increment() {
68        return (int) this.polyfillMethod(this.context);
69    }
70}
71
1#include <iostream>
2#include <functional>
3#include <map>
4#include <string>
5
6// FunctionWrapper is a utility class that wraps a member function
7// and allows it to be called with a specific context.
8template<typename Return, typename Context, typename... Args>
9class FunctionWrapper {
10public:
11    // The constructor takes a member function and saves it.
12    FunctionWrapper(Return(Context::*func)(Args...)) : function(func) {}
13
14    // callPolyfill emulates the JavaScript Function.prototype.call using C++ features.
15    // It calls the member function with the provided context and arguments.
16    Return callPolyfill(Context& context, Args... args) {
17        // Create a functor by binding the function to the context.
18        std::function<Return(Args...)> functor = std::bind(function, &context, std::placeholders::_1, std::placeholders::_2);
19
20        // Call the functor with the supplied arguments and return the result.
21        return functor(args...);
22    }
23
24private:
25    Return(Context::*function)(Args...); // Pointer to the member function
26};
27
28// Context structure that will hold the 'this' context equivalent for the increment function.
29struct Context {
30    int count;
31
32    // The increment member function which will be called using callPolyfill.
33    int increment() {
34        count++;
35        return count;
36    }
37};
38
39int main() {
40    Context obj = {1}; // Create a context object with count initialized to 1
41
42    // Instantiate a FunctionWrapper with the increment member function.
43    FunctionWrapper<int, Context> fw(&Context::increment);
44
45    // Call the increment method on obj context using the polyfill method.
46    // The result should be 2, as it increments the count from 1.
47    int result = fw.callPolyfill(obj); // result should be 2
48
49    // Logging the result to verify the correctness of the callPolyfill implementation.
50    std::cout << "Result is: " << result << std::endl; // Expected output: Result is: 2
51
52    return 0;
53}
54
1// Extending the global Function interface to include callPolyfill
2// This allows all functions to use the callPolyfill method
3declare global {
4    interface Function {
5        callPolyfill(context: Record<any, any>, ...args: any[]): any;
6    }
7}
8
9// Implementing the callPolyfill method on Function's prototype.
10// This method replicates the behavior of Function.prototype.call but without using call directly.
11Function.prototype.callPolyfill = function (context: Record<any, any>, ...args: any[]): any {
12    // The context object is where 'this' will point to inside the invoked function.
13    // We bind 'this' (which refers to the currently called function) to the provided context.
14    const boundFunction = this.bind(context);
15
16    // We invoke the bound function with the provided arguments and return the result.
17    return boundFunction(...args);
18};
19
20// Explicitly adding the extension to the global scope.
21export {};
22
23// Example usage:
24// Define an example function increment, which uses 'this' to refer to the context object.
25function increment(): number {
26    this.count++;
27    return this.count;
28}
29
30// Applying the polyfill function to the increment function,
31// setting 'this' to refer to an object with a count property.
32// The result should be 2, as it increments the count from 1.
33const result = increment.callPolyfill({ count: 1 }); // result is 2
34
35// Logging the result to verify the correctness of the callPolyfill implementation.
36console.log(result); // Expected output: 2
37

Time and Space Complexity

The time complexity of the callPolyfill method is primarily governed by two operations:

  1. The .bind() method which creates a new function from the calling one, with this set to the provided context.
  2. The spread operator (...) which is used to pass all the arguments to the newly bound function.

The .bind() method itself is generally O(1) since it doesn't iterate over data, but the implementation might vary depending on the JavaScript engine. The function call that happens after binding (with fn(...args)) has a time complexity dependent on the specific function being called. However, for the callPolyfill method itself, assuming that the subsequent function call is O(1), we could consider callPolyfill to be O(1) in the best case, where the time complexity is not influenced by the input size.

The space complexity of callPolyfill involves storing the newly bound function. This is O(1) because a single function reference is created regardless of the size of context or args. However, if the spread operator has to deal with a large number of args, this could lead to O(n) space complexity due to the creation of an arguments list to pass to the bound function, where n is the number of arguments provided.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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


Load More