Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Mastering JavaScript Functional Programming

You're reading from   Mastering JavaScript Functional Programming Write clean, robust, and maintainable web and server code using functional JavaScript and TypeScript

Arrow left icon
Product type Paperback
Published in Apr 2023
Publisher Packt
ISBN-13 9781804610138
Length 614 pages
Edition 3rd Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Federico Kereki Federico Kereki
Author Profile Icon Federico Kereki
Federico Kereki
Arrow right icon
View More author details
Toc

Table of Contents (17) Chapters Close

Preface 1. Chapter 1: Becoming Functional – Several Questions 2. Chapter 2: Thinking Functionally – A First Example FREE CHAPTER 3. Chapter 3: Starting Out with Functions – A Core Concept 4. Chapter 4: Behaving Properly – Pure Functions 5. Chapter 5: Programming Declaratively – A Better Style 6. Chapter 6: Producing Functions – Higher-Order Functions 7. Chapter 7: Transforming Functions – Currying and Partial Application 8. Chapter 8: Connecting Functions – Pipelining, Composition, and More 9. Chapter 9: Designing Functions – Recursion 10. Chapter 10: Ensuring Purity – Immutability 11. Chapter 11: Implementing Design Patterns – The Functional Way 12. Chapter 12: Building Better Containers – Functional Data Types 13. Answers to Questions 14. Bibliography
15. Index 16. Other Books You May Enjoy

A functional solution to our problem

Let’s try to be more general; after all, requiring that some function or other be executed only once isn’t that outlandish, and may be required elsewhere! Let’s lay down some principles:

  • The original function (the one that may be called only once) should do whatever it is expected to do and nothing else
  • We don’t want to modify the original function in any way
  • We need a new function that will call the original one only once
  • We want a general solution that we can apply to any number of original functions

A SOLID base

The first principle listed previously is the single responsibility principle (the S in the SOLID acronym), which states that every function should be responsible for a single functionality. For more on SOLID, check the article by Uncle Bob (Robert C. Martin, who wrote the five principles) at butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod.

Can we do it? Yes, and we’ll write a higher-order function, which we’ll be able to apply to any function, to produce a new function that will work only once. Let’s see how! We will introduce higher-order functions in Chapter 6, Producing Functions. There, we’ll go about testing our functional solution, as well as making some enhancements to it.

A higher-order solution

If we don’t want to modify the original function, we can create a higher-order function, which we’ll (inspiredly!) name once(). This function will receive a function as a parameter and return a new function, which will work only once. (As we mentioned previously, we’ll be seeing more of higher-order functions later; in particular, see the Doing things once, revisited section of Chapter 6, Producing Functions).

Many solutions

Underscore and Lodash already have a similar function, invoked as _.once(). Ramda also provides R.once(), and most FP libraries include similar functionality, so you wouldn’t have to program it on your own.

Our once() function may seem imposing at first, but as you get accustomed to working in an FP fashion, you’ll get used to this sort of code and find it to be quite understable:

// once.ts
const once = <FNType extends (...args: any[]) => any>(
  fn: FNType
) => {
  let done = false;
  return ((...args: Parameters<FNType>) => {
    if (!done) {
      done = true;
      return fn(...args);
    }
  }) as FNType;
};

Let’s go over some of the finer points of this function:

  • Our once() function receives a function (fn) as its parameter and returns a new function, of the same type. (We’ll discuss this typing in more detail shortly.)
  • We define an internal, private done variable, by taking advantage of closure, as in Solution 7. We opted not to call it clicked (as we did previously) because you don’t necessarily need to click on a button to call the function; we went for a more general term. Each time you apply once() to some function, a new, distinct done variable will be created and will be accessible only from the returned function.
  • The return statement shows that once() will return a function, with the same type of parameters as the original fn() one. We are using the spread syntax we saw in Chapter 1, Becoming Functional. With older versions of JavaScript, you’d have to work with the arguments object; see developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/arguments for more on that. The modern way is simpler and shorter!
  • We assign done = true before calling fn(), just in case that function throws an exception. Of course, if you don’t want to disable the function unless it has successfully ended, you could move the assignment below the fn() call. (See Question 2.4 in the Questions section for another take on this.)
  • After the setting is done, we finally call the original function. Note the use of the spread operator to pass along whatever parameters the original fn() had.

Typing for once() may be obscure. We have to specify that the type of the input function and the type of once() are the same, and that’s the reason for defining FNType. Figure 2.2 shows that TypeScript correctly understands this (Check the answer to Question 1.7 at the end of this book for another example of this):

Figure 2.2 – Hovering shows that the type of once()’s output matches the type of its input

Figure 2.2 – Hovering shows that the type of once()’s output matches the type of its input

If you’re not still used to TypeScript, let’s see the pure JavaScript equivalent, which is the same code but for typing:

// once_JS.js
const once = (fn) => {
  let done = false;
  return (...args) => {
    if (!done) {
      done = true;
      return fn(...args);
    }
  };
};

So, how would we use it? We first create a new version of the billing function.

const billOnce = once(billTheUser);

Then, we rewrite the onclick method as follows:

<button id="billButton"
  onclick="billOnce(some, sales, data)">Bill me
</button>;

When the user clicks on the button, the function that gets called with the (some, sales, data) argument isn’t the original billTheUser() but rather the result of having applied once() to it. The result of that is a function that can be called only a single time.

You can’t always get what you want!

Note that our once() function uses functions such as first-class objects, arrow functions, closures, and the spread operator. Back in Chapter 1, Becoming Functional, we said we’d be needing those, so we’re keeping our word! All we are missing from that chapter is recursion, but as the Rolling Stones sang, You Can’t Always Get What You Want!

We now have a functional way of getting a function to do its thing only once, but how would we test it? Let’s get into that topic now.

Testing the solution manually

We can run a simple test. Let’s write a squeak() function that will, appropriately, squeak when called! The code is simple:

// once.manual.ts
const squeak = a => console.log(a, " squeak!!");
squeak("original"); // "original squeak!!"
squeak("original"); // "original squeak!!"
squeak("original"); // "original squeak!!"

If we apply once() to it, we get a new function that will squeak only once. See the highlighted line in the following code:

// continued...
const squeakOnce = once(squeak);
squeakOnce("only once"); // "only once squeak!!" squeakOnce("only once"); // no output
squeakOnce("only once"); // no output

The previous steps showed us how we could test our once() function by hand, but our method is not exactly ideal. In the next section, we’ll see why and how to do better.

Testing the solution automatically

Running tests by hand isn’t suitable: it gets tiresome and boring, and it leads, after a while, to not running the tests any longer. Let’s do better and write some automatic tests with Jest:

// once.test.ts
import once } from "./once";
describe("once", () => {
  it("without 'once', a function always runs", () => {
    const myFn = jest.fn();
    myFn();
    myFn();
    myFn();
    expect(myFn).toHaveBeenCalledTimes(3);
  });
  it("with 'once', a function runs one time", () => {
    const myFn = jest.fn();
    const onceFn = jest.fn(once(myFn));
    onceFn();
    onceFn();
    onceFn();
    expect(onceFn).toHaveBeenCalledTimes(3);
    expect(myFn).toHaveBeenCalledTimes(1);
  });
});

There are several points to note here:

  • To spy on a function (for instance, to count how many times it was called), we need to pass it as an argument to jest.fn(); we can apply tests to the result, which works exactly like the original function, but can be spied on.
  • When you spy on a function, Jest intercepts your calls and registers that the function was called, with which arguments, and how many times it was called.
  • The first test only checks that if we call the function several times, it gets called that number of times. This is trivial, but we’d be doing something wrong if that didn’t happen!
  • In the second test, we apply once() to a (dummy) myFn() function, and we call the result (onceFn()) several times. We then check that myFn() was called only once, though onceFn() was called three times.

We can see the results in Figure 2.3:

Figure 2.3 – Running automatic tests on our function with Jest

Figure 2.3 – Running automatic tests on our function with Jest

With that, we have seen not only how to test our functional solution by hand but also in an automatic way, so we are done with testing. Let’s just finish by considering an even better solution, also achieved in a functional way.

Producing an even better solution

In one of the previous solutions, we mentioned that it would be a good idea to do something every time after the first click, and not silently ignore the user’s clicks. We’ll write a new higher-order function that takes a second parameter – a function to be called every time from the second call onward. Our new function will be called onceAndAfter() and can be written as follows:

// onceAndAfter.ts
const onceAndAfter = <
  FNType extends (...args: any[]) => any
>(
  f: FNType,
  g: FNType
) => {
  let done = false;
  return ((...args: Parameters<FNType>) => {
    if (!done) {
      done = true;
      return f(...args);
    } else {
      return g(...args);
    }
  }) as FNType;
};

We have ventured further into higher-order functions; onceAndAfter() takes two functions as parameters and produces a third one, which includes the other two within.

Function as default

You could make onceAndAfter() more powerful by giving a default value for g, such as () => {}, so if you didn’t specify the second function, it would still work fine because the default do-nothing function would be called instead of causing an error.

We can do a quick-and-dirty test along the same lines as we did earlier. Let’s add a creak() creaking function to our previous squeak() one and check out what happens if we apply onceAndAfter() to them. We can then get a makeSound() function that should squeak once and creak afterward:

// onceAndAfter.manual.ts
import { onceAndAfter } from "./onceAndAfter";
const squeak = (x: string) => console.log(x, "squeak!!");
const creak = (x: string) => console.log(x, "creak!!");
const makeSound = onceAndAfter(squeak, creak);
makeSound("door"); // "door squeak!!"
makeSound("door"); // "door creak!!"
makeSound("door"); // "door creak!!"
makeSound("door"); // "door creak!!"

Writing a test for this new function isn’t hard, only a bit longer. We have to check which function was called and how many times:

// onceAndAfter.test.ts
import { onceAndAfter } from "./onceAndAfter";
describe("onceAndAfter", () => {
  it("calls the 1st function once & the 2nd after", () => {
    const func1 = jest.fn();
    const func2 = jest.fn();
    const testFn = jest.fn(onceAndAfter(func1, func2));
    testFn();
    testFn();
    testFn();
    testFn();
    expect(testFn).toHaveBeenCalledTimes(4);
    expect(func1).toHaveBeenCalledTimes(1);
    expect(func2).toHaveBeenCalledTimes(3);
  });
});

Notice that we always check that func1() is called only once. Similarly, we check func2(); the count of calls starts at zero (the time that func1() is called), and from then on, it goes up by one on each call.

You have been reading a chapter from
Mastering JavaScript Functional Programming - Third Edition
Published in: Apr 2023
Publisher: Packt
ISBN-13: 9781804610138
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime