Taro Logo

Call Function with Custom Context

Medium
Asked by:
Profile picture
10 views
Topics:
Bit Manipulation

Enhance all functions to have the callPolyfill method. The method accepts an object obj as its first parameter and any number of additional arguments. The obj becomes the this context for the function. The additional arguments are passed to the function (that the callPolyfill method belongs on).

For example if you had the function:

function tax(price, taxRate) {
  const totalCost = price * (1 + taxRate);
  console.log(`The cost of ${this.item} is ${totalCost}`);
}

Calling this function like tax(10, 0.1) will log "The cost of undefined is 11". This is because the this context was not defined.

However, calling the function like tax.callPolyfill({item: "salad"}, 10, 0.1) will log "The cost of salad is 11". The this context was appropriately set, and the function logged an appropriate output.

Please solve this without using the built-in Function.call method.

Example 1:

Input:
fn = function add(b) {
  return this.a + b;
}
args = [{"a": 5}, 7]
Output: 12
Explanation:
fn.callPolyfill({"a": 5}, 7); // 12
callPolyfill sets the "this" context to {"a": 5}. 7 is passed as an argument.

Example 2:

Input: 
fn = function tax(price, taxRate) { 
 return `The cost of the ${this.item} is ${price * taxRate}`; 
}
args = [{"item": "burger"}, 10, 1.1]
Output: "The cost of the burger is 11"
Explanation: callPolyfill sets the "this" context to {"item": "burger"}. 10 and 1.1 are passed as additional arguments.

Constraints:

  • typeof args[0] == 'object' and args[0] != null
  • 1 <= args.length <= 100
  • 2 <= JSON.stringify(args[0]).length <= 105

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What type of value will 'thisArg' be? Can it be null or undefined?
  2. What types of values can the function being called return? Should I handle any specific return values (e.g., null, undefined, errors)?
  3. Can the function being called accept arguments? If so, what types of arguments should I expect? Can it accept zero arguments?
  4. If the function throws an error, should my implementation also throw that error or handle it in a specific way?
  5. Are there any limitations on the complexity or behavior of the function being called? (e.g., can it modify the 'thisArg' or any global state?)

Brute Force Solution

Approach

The goal is to allow a function to act as if it belongs to a different object. The brute force way would be to temporarily change the object the function thinks it belongs to and then execute the function.

Here's how the algorithm would work step-by-step:

  1. First, we temporarily remember the original owner of the function.
  2. Next, we pretend the function belongs to the new, specified object.
  3. Now, we run the function as if it were part of this new object.
  4. After the function has finished running, we make sure to set the function back to its original owner, so it behaves correctly in the future.

Code Implementation

def call_function_with_context(function_to_call, context_object, *args, **kwargs):    original_owner = function_to_call.__self__
    # Temporarily bind the function to the new context.
    function_to_call.__self__ = context_object

    try:
        # Execute the function as if it belongs to the new object.
        result = function_to_call(*args, **kwargs)

    finally:
        # Restore the function's original context to avoid unexpected behavior.
        function_to_call.__self__ = original_owner

    return result

Big(O) Analysis

Time Complexity
O(1) – The provided solution involves a fixed number of operations: temporarily storing the original function owner, assigning the function to the new context, executing the function, and restoring the original owner. These operations do not depend on the input size (n) of any data structure. Therefore, the time complexity is constant, denoted as O(1).
Space Complexity
O(1) – The algorithm temporarily stores the original owner of the function, which is a single variable. After the function execution, the original owner is restored. No additional data structures are allocated, and the memory usage does not depend on the input size (N). Thus, the auxiliary space complexity is constant.

Optimal Solution

Approach

The core idea is to mimic the 'call', 'apply', and 'bind' functionalities available in many programming languages. We want to make a function execute as if it belonged to a particular object, even if it wasn't originally defined that way.

Here's how the algorithm would work step-by-step:

  1. First, add the function you want to execute to the object whose context you want to use. Think of it as temporarily lending the function to the object.
  2. Next, execute the function using the object's name, like you would call any other function belonging to that object.
  3. If you need to pass arguments to the function, include them when you execute it.
  4. After executing the function, immediately remove it from the object to keep the object clean and avoid unexpected behavior later.
  5. Finally, return the result of the function execution.

Code Implementation

def call_with_context(function_to_call, context_object, *function_arguments):
    # Temporarily add the function to the context object.

    context_object.temporary_function = function_to_call

    # Call the function using the context object.
    function_result = context_object.temporary_function(*function_arguments)

    # Remove the temporary function to avoid polluting the object.

    del context_object.temporary_function
    return function_result

Big(O) Analysis

Time Complexity
O(1) – The provided algorithm performs a fixed number of operations regardless of the input size. It adds a function to an object, calls it, removes the function, and returns the result. Each of these steps takes constant time. Therefore, the overall time complexity is O(1).
Space Complexity
O(1) – The algorithm temporarily adds a function to the input object. While the function itself occupies space, it's assumed to be already loaded into memory before the function call; thus, adding a reference to it on the object does not significantly increase memory usage, and we're cleaning it immediately. The algorithm may also require a constant amount of space to store the result of the function execution before returning it. Therefore, the space required is constant and independent of the input size, resulting in O(1) auxiliary space.

Edge Cases

Null or undefined context object
How to Handle:
Handle by assigning a default context, such as the global object or an empty object, to prevent null pointer exceptions.
Null or undefined function
How to Handle:
Throw an error immediately since there's nothing to call.
Arguments passed are null or undefined
How to Handle:
Treat null/undefined arguments as valid values, passing them to the function as they are.
The function throws an error during execution
How to Handle:
Catch the error and re-throw it or return a specific error value based on requirement.
The context object is a primitive type (string, number, boolean)
How to Handle:
Box the primitive type into its corresponding object wrapper (String, Number, Boolean) before setting the context.
Calling the function with a very large number of arguments
How to Handle:
Be mindful of argument limits imposed by the engine; consider alternative patterns if limits are reached, such as passing arguments within a single array or object.
The function modifies the context object
How to Handle:
Be aware that the called function may alter the context object, and document this potential side effect or consider creating a copy of the context beforehand.
The function is bound already
How to Handle:
Overriding an already bound context can lead to unexpected behaviour, so consider copying or detaching the bound function before applying the custom context.