Skip to main content

Day 10: Allow One Function Call

LC2666

Given a function fn, return a new function that is identical to the original function except that it ensures fn is called at most once.

  • The first time the returned function is called, it should return the same result as fn.
  • Every subsequent time it is called, it should return undefined.

Example 1:

Input: fn = (a,b,c) => (a + b + c), calls = [[1,2,3],[2,3,6]]
Output: [{"calls":1,"value":6}]
Explanation:
const onceFn = once(fn);
onceFn(1, 2, 3); // 6
onceFn(2, 3, 6); // undefined, fn was not called

Example 2:

Input: fn = (a,b,c) => (a * b * c), calls = [[5,7,4],[2,3,6],[4,6,8]]
Output: [{"calls":1,"value":140}]
Explanation:
const onceFn = once(fn);
onceFn(5, 7, 4); // 140
onceFn(2, 3, 6); // undefined, fn was not called
onceFn(4, 6, 8); // undefined, fn was not called

Constraints:

  • calls is a valid JSON array
  • 1 <= calls.length <= 10
  • 1 <= calls[i].length <= 100
  • 2 <= JSON.stringify(calls).length <= 1000

Solution

/**
* @param {Function} fn
* @return {Function}
*/
var once = function(fn) {
let isExecuted = false;
return function(...args){
if (!isExecuted) {
isExecuted = true;
return fn(...args);
}

return undefined;
}
};

/**
* let fn = (a,b,c) => (a + b + c)
* let onceFn = once(fn)
*
* onceFn(1,2,3); // 6
* onceFn(2,3,6); // returns undefined without calling fn
*/

Overview

This question asks you to write a function that modifies a function such that it can only be called once. This is an example of a higher-order function.

Example Use-cases of Functions That Modify Functions

Functions that modify or extends the behavior of functions are extremely common in JavaScript and are a key concept in functional programming. It is critical to fully understand them to be an effective developer.

They are very useful for writing elegant, reusable code, and have a variety of use-cases, a few of which will be discussed.

Throttle

Imagine you were implementing a search field. Querying the database for results every single time a character is typed could put unnecessary load on the database and the user interface. To prevent this, you could employ a technique known as throttling. By throttling the function responsible for sending requests to the database, we ensure that only a limited number of requests are sent per second, thereby optimizing system performance.

Memoize

A common optimization is to never call a pure function with the same inputs twice. Instead you can avoid the computation by just returning the previously cached result. This is also an important concept in dynamic programming. Benefitting from this optimization could be as simple calling memoize() on the function. memoizee is popular package for this.

Time Limit

Suppose you have a long running process that repeats itself (like syncing data from a database to an in-memory cache). If for some reason, an asynchronous function never returns a value (or takes a very long time), that process will become frozen. To guarantee that never happens, you could wrap the asynchronous functions used with time limits.


Use-cases for Only Allowing One Function Call

The simple answer is it useful for initialization logic. You may have a function that initializes a cache (for example, load a file into memory) and you want to guarantee it only loads once.

You might be wondering, why not just simply avoid calling the function more than once, it's your code after all? Well for one, it's still nice to have that guarantee. But there are also cases where you may want perform the initialization logic lazily.

let json;
function loadJsonFromFile() {
// logic to load file
json = loadFile();
}

const loadFileOnce = once(loadJsonFromFile);

function getValue(key) {
loadFileOnce();
return json[key];
}

In this example, you may only want to do the expensive operation of loading the file AFTER getValue is called. For example, getValue might be part of a library and may never be called.

Another example of a common use-case is showing a welcome message or introduction when a user first clicks a button.

const button = document.querySelector('#start-button');
button.addEventListener('click', once(function() {
displayWelcomeMessage();
}));

The popular library lodash includes an implementation of once.


Syntax Required for Transforming Functions

Rest Syntax

In JavaScript, you can access all passed arguments as an array using rest syntax. You can then spread the array of arguments and pass them back into a function.

function sum(...nums) {
let sum = 0;
for (const num of nums) {
sum += num;
}
return sum;
}
sum(1, 2, 3); // 6

In the above example, the variable nums is [1, 2, 3].

But more importantly, you can use this syntax to transform arbitrary functions.

function withLogging(fn) {
return function(...args) {
console.log('Arguments', args);
const result = fn(...args);
console.log('Result', result);
return result;
}
}
const add = withLogging((a, b) => a + b);
add(1, 2); // Logs: Arguments [1, 2] Result 3

In this example, all the arguments, no matter what they are or how many, will be passed to fn.

Argument Syntax

If you use function syntax (not arrow function), you have access to the arguments variable.

function sum() {
let sum = 0;
for (const num of arguments) {
sum += num;
}
return sum
}
sum(1, 2, 3); // 6

arguments is a special iterable value bound to the function. It's good to understand for reading older codebases, however is has been largely replaced with rest syntax. One reason for this is that it can lead to confusion when you don't document the input values upfront. This is especially problematic if you want to annotate your functions with TypeScript.

Approach 1: Rest Syntax

We can declare a boolean which tracks whether the function has been called or not. Then we can use rest and spread syntax to pass the arguments if it has not been called yet.

var once = function(fn) {
let hasBeenCalled = false;
return function(...args){
if (hasBeenCalled) {
return undefined;
} else {
hasBeenCalled = true;
return fn(...args);
}
}
};

Approach 2: Implicitly Return Undefined

If you don't return any value from a function, it will implicitly return undefined. We can use this fact to shorten the code slightly.

var once = function(fn) {
let hasBeenCalled = false;
return function(...args){
if (!hasBeenCalled) {
hasBeenCalled = true;
return fn(...args);
}
}
};

Approach 3: Bind Function to a Context

JavaScript functions are sometimes bound to a this context. The most technically correct way to implement a higher-order function is to ensure the provided function is bound to the same context as the returned function.

The following code showcases how functions can behave differently depending on which this context they are bound to.

const context = { Name: 'Alice' };

function getName() {
return this.Name;
}
const boundGetName = getName.bind(context);

getName(); // undefined
getName.apply(context, []); // "Alice"

In the above example getName is NOT bound to context so it returns undefined. However you can use the Function.apply method to call getName() AND set this to be context. You can read more about Function.apply here.

Now suppose you wanted to apply once to a method on a prototype.

const Person = function(name) {
this.name = name;
}

Person.prototype.getName = once(function() {
return this.name;
});

const alice = new Person('Alice');
alice.getName(); // Expected Output: "Alice"

In order to get this behavior, you need to use the Function.apply method to ensure getName is provided with the appropriate context (the alice object in this case). Without this, getName will always return undefined.

Note that this functionality is fairly niche, and isn't required in the problem. But it may be expected in a professional implementation.

var once = function(fn) {
let hasBeenCalled = false;
return function(...args){
if (!hasBeenCalled) {
hasBeenCalled = true;
return fn.apply(this, args);
}
}
};