Functions as results

Within a function, we can return a function as a result. When would this feature ever be useful? Let's start with a simple example to help us understand why returning a function would be desirable in certain situations.

Simple example

Suppose we want a function that doubles any number given to it. Consider the following implementation.

const double = (x) => x * 2;

The above function does what is required. So what is wrong with it? Nothing, really. Now we want a function that triples a number. Here it is.

const triple = (x) => x * 3;

OK. Now a function that multiplies its argument by 10. No problem.

const tens = (x) => x * 10;

The pattern is that we multiply the argument by a fixed number. Let's abstract the above behaviour by having a function return another function, as shown in the following script.

// Can be simplied to this. // const math = (x) => (y) => x * y; const math = (x) => { return function (y) { return x * y; // value of x is fixed } }; const double = math(2); const triple = math(3); const tens = math(10); console.log(double(1)); console.log(math(2)(1)); console.log(triple(4)); console.log(math(3)(4)); console.log(tens(6)); console.log(math(10)(6));

In the script mult.js, we declare a function that returns a function that has 1 parameter. The function math() returns a function that accepts an argument y. In the body of the returned function, the value of x is already fixed. Only the value of y varies because we are meant to feed values to the returned function. To create a function that doubles its argument, we give the number 2 to math() so as to fix the value of x inside the body of the returned function. After all, 2 multiplied by a number is the doubling operation. To create a function that triples a given number, we pass 3 to math() to fix the value of x. The returned function is consequently a tripling function. And so on.

The returned function can also be invoked like so:

math(x)(y);

Doubling the number 1 is:

console.log(math(2)(1));

Tripling the number 4 is:

console.log(math(3)(4));

Multiplying the number 6 by 10 is:

console.log(math(10)(6));

Partial application

The idea presented in the section Simple example is known as partial function application, commonly shortened to partial application. If we have a function that takes multiple arguments, partial application allows us to fix one or more arguments to obtain a function of the remaining arguments. We can supply all arguments at once to a function. In some cases, we might not know ahead of time the values of all arguments, but only a few. Partial application allows us to supply only the first few arguments, with the remaining arguments being supplied as their values are known.

Partial application can also be understood as a way to generalize/specialize functions. In the script mult.js from the section Simple example, the function math() is a generalized function that abstracts the idea of multiplying 2 numbers. We specialize math() by creating the function double() to double a number.

Let's use another example to help us consolidate our understanding of partial application. We want to develop a simple image writer to help us manipulate and store pet images. Our early prototype has the following function.

const imgWriter = (format, name, width, height) => { // Insert code to write image to file console.log("Image is written to file"); console.log(`Name: ${name}`); console.log(`Format: ${format}`); console.log(`Dimensions: ${width} x ${height}`); };

Image file format comes in many varieties. Some of the common formats include GIF, JPEG, PNG, and SVG. Recognizing that each image format should be handled differently, we modify the function imgWriter() to use partial application.

const imgWriter = (format) => (name, width, height) => { // Insert code to write image to file console.log("Image is written to file"); console.log(`Name: ${name}`); console.log(`Format: ${format}`); console.log(`Dimensions: ${width} x ${height}`); }; const pngWriter = imgWriter("PNG"); const jpgWriter = imgWriter("JPEG"); const gifWriter = imgWriter("GIF"); pngWriter("cuteCat.png", 200, 400); jpgWriter("puppy.jpg", 300, 600); gifWriter("hamsterDance.gif", 300, 400);

The function imgWriter() is now declared as a generalized function, taking 1 parameter and returning a function having 3 parameters. Whenever we require a writer function that can handle one particular image format, we pass the name of the image format to imgWriter(), which returns a function that handles that particular format. Partial application allows us to specialize imgWriter() to a function that handles a specific image format.

Recall that functions in JavaScript are first-class citizens. We can pass functions as arguments to other functions. Moreover, functions are higher-order because their parameters can be other functions. Combining the above 2 pieces of information, there is nothing stopping us from declaring a function to handle the partial application for us. Observe:

const partial = (fn, arg) => (...moreArg) => fn(arg, ...moreArg); const topk = (n, ...arg) => arg.slice(0, n); const bottomk = (n, ...arg) => arg.slice(arg.length - n); const head = partial(topk, 1); const top3 = partial(topk, 3); const tail = partial(bottomk, 1); const ends = (...arg) => [head(...arg), tail(...arg)].flat(); console.log(head("a", "b", "c", "d")); console.log(top3("gold", "silver", "bronze", "fourth", "fifth")); console.log(tail("a", "b", "c", "d")); console.log(ends("a", "b", "c", "d"));

The function partial() is meant to partially apply an argument arg to a given function fn(). The definition of partial() is rather abstract. We impose no restrictions on what fn() should be, save for the fact that fn() should have at least 2 parameters. The first argument of fn() is arg. The remaining arguments are given by way of the rest parameter syntax. To concretize fn(), we declare the function topk() to return the top k items in a sequence of items. If k = 1, then topk() would be a function that returns the first element in a sequence of elements, i.e. the "head" of a sequence. This is exactly what the following expression does:

const head = partial(topk, 1);

The function top3() returns the first 3 items in a sequence, the function tail() returns the last item in a sequence, and ends() returns the first and last items in a sequence.

Exercises

Exercise 1. Refer to the script mult.js. Use the function math() to obtain a function that halves any given number.

Exercise 2. In the script mult.js, modify the function math() to have 2 parameters: a number x and an operator. The operator should be one of the arithmetic operators: +, -, *, /. The function math() returns a function func() that has 1 parameter, i.e. a number y. The function func() returns the result of applying the arithmetic operator on the numbers x and y. For example, the expression

const increment = math(1, "+");

should result in a function increment() that adds 1 to each given number. The expression

const decrement = math(1, "-");

should result in a function decrement() that subtracts 1 from each given number.

Exercise 3. Modify the script partial.js so that the function fn() has 2 parameters: the first parameter is a number and the second is an array. Modify the rest of the script accordingly.

Exercise 4. A URL consists of various components:

  1. Scheme: The protocol for accessing a resource on the Internet.
  2. Domain: The host holding the resource.
  3. Path: The specific resource in the host.

The following code snippet declares a function to build a URL according to the above components.

const createURL = (scheme, domain, path) => `${scheme}://${domain}/${path}`; console.log(createURL("https", "en.wikipedia.org", "wiki/Duck")); console.log(createURL("https", "doi.org", "10.17487/RFC2549"));

Modify the function createURL() to use partial application.

Exercise 5. Every fixed interval, you want to query an online API for some data. Your early script to poll the API is:

const poll = () => console.log("Query API"); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const spacedCall = async (n, delay) => { for (let i = 0; i < n; i++) { await sleep(delay); poll(); } console.log("Done"); }; const delay = 1000; const howMany = 5; spacedCall(howMany, delay);

Modify the script so that spacedCall() is a higher-order function.

Exercise 6. Declare a function that returns a random string of alphanumeric characters. For the purposes of this exercise, only generate strings having at most 11 characters.

  1. Modify your function to allow the generation of random strings each beginning with a fixed prefix.
  2. Modify your function again to restrict the length of each generated string.
  3. Modify your function to be higher-order.

Exercise 7. This exercise builds upon the head/tail example from the section Partial application.

  1. Declare a function first() that takes a number n and a bunch of other arguments. The function returns the first n items in the bunch, excluding the argument n. Specialize the function first() by declaring a function head() that returns the first element of a sequence. Given the sequence 1, 2, 3, 4, the function head() should return 1.
  2. Declare a function last() that takes a number n and a bunch of other arguments. The function returns the last n items in the bunch, not including the argument n. Specialize the function last() by declaring a function tail() that returns the last element of a sequence. Given the sequence 1, 2, 3, 4, the function tail() should return 4.
  3. Declare a function swap() that takes a bunch of arguments. The function moves the head of the sequence and places it at the end of the sequence. Given the sequence 1, 2, the function swap() should return 2, 1.
  4. Names given as FirstName LastName are sometimes reversed as LastName, FirstName for various purposes. A common example where the reversal occurs is in academic publications, where scholars reference a book or research article by the format of LastName, FirstName. Use the function swap() to print "James Bond" as "Bond, James".

results matching ""

    No results matching ""