11 First-class functions: Part 2

Image

In this chapter

  • Learn more applications of replace body with callback.
  • Understand how returning functions from functions can give functions superpowers.
  • Get lots of practice writing higher-order functions to sharpen your skills.

In the last chapter, we learned skills for creating higher-order functions. This chapter will deepen the learning as we apply those skills to more examples. We start by codifying our copy-on-write discipline. We then improve the logging system so it’s not as much work.

One code smell and two refactorings

In the last chapter, we learned a code smell and two refactorings that help us eliminate duplication and find better abstractions. They let us create first-class values and higher-order functions. Just as a reminder, here they are again. We’ll apply these new skills throughout the whole of part 2 of this book. Here they are again for reference.

Code smell: Implicit argument in function name

This code smell identifies aspects of code that could be better expressed as first-class values. If you are referring to a value in the body of a function, and that value is named in the function name, this smell applies. The solution is the next refactoring.

Characteristics
  1. There are very similar function implementations.
  2. The name of the function indicates the difference in implementation.

Refactoring: Express implicit argument

When you have an implicit argument in the function name, how do you turn that into an actual argument to the function? This refactoring adds an argument to your function so that the value becomes first-class. This may let you better express the intent of the code and potentially eliminate duplication.

Steps
  1. Identify the implicit argument in the name of the function.
  2. Add explicit argument.
  3. Use new argument in body in place of hard-coded value.
  4. Update the calling code.

Refactoring: Replace body with callback

The syntax of a language is often not first-class. This refactoring allows you to replace the body (the part that changes) of a piece of code with a callback. You can then pass in the behavior in a first-class function. It is a powerful way to create higher-order functions from existing code.

Steps
  1. Identify the before, body, and after sections.
  2. Extract the whole thing into a function.
  3. Extract the body section into a function passed as an argument to that function.

We’ll be iteratively applying these powerful skills so that they become second nature.

Refactoring copy-on-write

Image

Steps of replace body with callback

  1. Identify before, body, and after.
  2. Extract function.
  3. Extract callback.

Jenna:1 Really? I thought replace body with callback only worked with eliminating duplication in syntax like for loops and try/catch statements.

Kim: Well, it does help with those, as we’ve seen. But it can also work with other kinds of duplication—even the duplication of a coding discipline.

Jenna: Whoa! Nice! I’d love to see it.

Kim: Well, you know the first step.

Jenna: Right… identify the before, body, and after sections.

Kim: That’s right. Once you have those, it’s smooth sailing.

Jenna: The rules of copy-on-write are make a copy, modify the copy, return the copy. The thing that varies is how you modify. The other two are always the same for a given data structure.

Kim: Well, if it varies, it must be the body. And it’s nestled between two unchanging sections, the before and after.

Jenna: This refactoring really does apply here!

Kim: Yep! We’re running out of room on this page, so let’s flip to the next one and get to it.

Steps of copy-on-write

  1. Make a copy.**
  2. Modify the copy.***
  3. Return the copy.****

    ** before

    *** body

    **** after

Refactoring copy-on-write for arrays

In chapter 6, we developed several copy-on-write routines for arrays. They all followed the basic pattern of make a copy, modify the copy, return the copy. Let’s apply the replace body with callback refactoring to them to standardize the pattern.

Steps of copy-on-write

  1. Make a copy.
  2. Modify the copy.
  3. Return the copy.

1. Identify before, body, after

Here’s just a few copy-on-write operations. We can see that they have very similar definitions. The copy, modify, return corresponds naturally to before, body, after.

function arraySet(array, idx, value) {

var copy = array.slice();

copy[idx] = value;

return copy;

}

 

function push(array, elem) {

var copy = array.slice();

copy.push(elem);

return copy;

}

function drop_last(array) {

var array_copy = array.slice();

array_copy.pop();

return array_copy;

}

 

function drop_first(array) {

var array_copy = array.slice();

array_copy.shift();

return array_copy;

}

before

body

after

Since the copying and returning are the same, let’s focus on the first one, arraySet(). You could choose any of them.

Steps of replace body with callback

  1. Identify before, body, and after.
  2. Extract function.
  3. Extract callback.

2. Extract function

Our next step is to extract these three sections into a function. That function will contain the before and after code, so we can name it after the important part, which is copying arrays.

Image

That’s the right way to do it, but we won’t be able to run this code yet. Notice that idx and value are undefined in the scope of withArrayCopy(). Let’s continue with the next step.

Our next step is to extract out the body into a callback. Let’s do that on the next page.

On the last page, we started applying replace body with callback to the copy-on-write operations on arrays so that we could codify the discipline in code. We had just finished step 2 of replace body with callback. Here’s the code we were left with:

Steps of replace body with callback

  1. Identify before, body, and after.
  2. Extract function.
  3. Extract callback.

function arraySet(array, idx, value) {

return withArrayCopy(array);

}

 

function withArrayCopy(array) {

var copy = array.slice();

copy[idx] = value;

return copy;

}

the copy-on-write operation

before

two variables not defined in this scope

body

after

We’ve done the second step correctly, but we won’t be able to run this code yet. Notice that idx and value are undefined in the scope of withArrayCopy(). Let’s continue with the next step.

3. Extract callback

Our next step is to extract the body into a callback. Because the callback will modify the array, we’ll call it modify.

Image

And we’re done! Let’s compare the original to where we are after the refactoring on the next page.

On the last page, we completed the refactoring. Let’s compare the code before the refactoring to where we ended up after the refactoring. Then we can discuss what this refactoring has done for us.

Before refactoring

function arraySet(array, idx, value) {

var copy = array.slice();

copy[idx] = value;

return copy;

}

After refactoring

function arraySet(array, idx, value) {

return withArrayCopy(array, function(copy) {

copy[idx] = value;

});

}

 

function withArrayCopy(array, modify) {

var copy = array.slice();

modify(copy);

return copy;

}

reusable function that standardizes the discipline

Sometimes refactoring code and getting rid of duplication shortens the code. But not in this case. The duplicated code was already pretty short—just two lines. We did achieve a big benefit. We have codified and standardized the copy-on-write discipline for arrays. It no longer has to be written the same way all over the codebase. It’s in one place.

Benefits we achieved

  1. Standardized discipline
  2. Applied discipline to new operations
  3. Optimized sequences of modifications

We’ve also gained a new ability. In chapter 6, where we explored the copy-on-write discipline, we developed copy-on-write versions of basically all of the important array operations. However, what if we forgot one? The new withArrayCopy() function—the result of this refactoring—can handle any operation that modifies an array. For instance, what if we found a library with a faster way to sort? We can easily maintain our copy-on-write discipline and use this new sort routine.

var sortedArray = withArrayCopy(array, function(copy) {

SuperSorter.sort(copy);

});

the better sort routine that sorts in place

This benefit is huge. It even gives us an avenue for optimization. A series of copy-on-write operations will create a new copy for each operation. That can be slow and hog memory. withArrayCopy() gives us a way to optimize them by making one single copy.

Makes intermediate copies

var a1 = drop_first(array);

var a2 = push(a1, 10);

var a3 = push(a2, 11);

var a4 = arraySet(a3, 0, 42);

this code makes four copies of the array

Makes one copy

var a4 = withArrayCopy(array, function(copy){

copy.shift();

copy.push(10);

copy.push(11);

copy[0] = 42;

});

make one copy

make four modifications to the copy

Now we can re-implement all array copy-on-write functions using withArrayCopy(). In fact, that sounds like a good exercise.

Image It’s your turn

We’ve just created a function called withArrayCopy() that codifies the copy-on-write discipline we learned in chapter 6. Following the example of arraySet(), rewrite push(), drop_last(), and drop_first().

function withArrayCopy(array, modify) {

var copy = array.slice();

modify(copy);

return copy;

}

Example

function arraySet(array, idx, value) {

var copy = array.slice();

copy[idx] = value;

return copy;

}

 

function arraySet(array, idx, value) {

return withArrayCopy(array, function(copy) {

copy[idx] = value;

});

}

 

function push(array, elem) {

var copy = array.slice();

copy.push(elem);

return copy;

}

Image

Image

function drop_last(array) {

var array_copy = array.slice();

array_copy.pop();

return array_copy;

}

Image

Image

function drop_first(array) {

var array_copy = array.slice();

array_copy.shift();

return array_copy;

}

Image

Image

Image Answer

Original

function push(array, elem) {

var copy = array.slice();

copy.push(elem);

return copy;

}

Using withArrayCopy()

function push(array, elem) {

return withArrayCopy(array, function(copy) {

copy.push(elem);

});

}

Original

function drop_last(array) {

var array_copy = array.slice();

array_copy.pop();

return array_copy;

}

Using withArrayCopy()

function drop_last(array) {

return withArrayCopy(array, function(copy) {

copy.pop();

});

}

Original

function drop_first(array) {

var array_copy = array.slice();

array_copy.shift();

return array_copy;

}

Using withArrayCopy()

function drop_first(array) {

return withArrayCopy(array, function(copy) {

copy.shift();

});

}

Image It’s your turn

We just developed withArrayCopy(), which implements a copy-on-write discipline for arrays. Can you do the same for objects?

Here is the code for a couple of copy-on-write implementations:

function objectSet(object, key, value) {

var copy = Object.assign({}, object);

copy[key] = value;

return copy;

}

function objectDelete(object, key) {

var copy = Object.assign({}, object);

delete copy[key];

return copy;

}

Write a function withObjectCopy() and use it to re-implement these two object copy-on-write functions.

Image

Image

Image Answer

function withObjectCopy(object, modify) {

var copy = Object.assign({}, object);

modify(copy);

return copy;

}

 

function objectSet(object, key, value) {

return withObjectCopy(object, function(copy) {

copy[key] = value;

});

}

 

function objectDelete(object, key) {

return withObjectCopy(object, function(copy) {

delete copy[key];

});

}

Image It’s your turn

George just finished wrapping everything he needed to in withLogging(). It was a big task, but he’s done. However, he sees another way he could make a more general version. The try/catch has two parts that vary, the body of the try and the body of the catch. We only let the body of the try vary. Could you help him adapt this refactoring to the case where it has two bodies that vary? In essence, George would like to write

tryCatch(sendEmail, logToSnapErrors)

instead of

try {

sendEmail();

} catch(error) {

logToSnapErrors(error);

}

Your task is to write tryCatch().

Hint: It will be a lot like withLogging(), but with two function arguments.

Image

Image

Image Answer

function tryCatch(f, errorHandler) {

try {

return f();

} catch(error) {

return errorHandler(error);

}

}

Image It’s your turn

Just for fun, let’s wrap another piece of syntax using the replace body with callback refactoring. This time, we will refactor an if statement. This might not be practical, but it’s good practice. To make it easier, let’s just do an if statement with no else. Here are two if statements to work with:

if(array.length === 0) {

console.log("Array is empty");

}

if(hasItem(cart, "shoes")) {

return setPriceByName(cart, "shoes", 0);

}

“test” clause

“then” clause

Use these two examples and the refactoring to write a function called when(). You should be able to use it like this:

when(array.length === 0, function() {

console.log("Array is empty");

});

when(hasItem(cart, "shoes"), function() {

return setPriceByName(cart, "shoes", 0);

});

“test” clause

“then” clause

Image

Image

Image Answer

function when(test, then) {

if(test)

return then();

}

Image It’s your turn

After writing the function called when() in the last exercise, a few people started using it—and loving it! They want a way to add an else statement. Let’s rename the function from when() to IF() and add a new callback for the else branch.

IF(array.length === 0, function() {

console.log("Array is empty");

}, function() {

console.log("Array has something in it.");

});

“test” clause

“then” clause

“else” clause

IF(hasItem(cart, "shoes"), function() {

return setPriceByName(cart, "shoes", 0);

}, function() {

return cart; // unchanged

});

Image

Image

Image Answer

function IF(test, then, ELSE) {

if(test)

return then();

else

return ELSE();

}

Returning functions from functions

Image

George: I hope they can help. We need to wrap code in a try/catch and send errors to Snap Errors®. We are taking some normal code and giving it the superpower. The superpower runs the code, catches any errors, and sends them to Snap Errors®. It’s like putting on a superhero outfit that gives your code superpowers.

Image

George: It works fine. But there are thousands of lines of code that need to be wrapped like that. Even after the refactoring we did before, I had to do it manually, one at a time.

Image

George: What would be nice is if we could write a function that just did it for me, so I wouldn’t have to manually do it thousands of times.

Image

Kim: Well, let’s just write it, then! It’s just a higher-order function.

Let’s review George’s problem and the current prototype solution.

George needed to catch errors and log them to the Snap Errors® service. Some snippets of code looked like this:

Image

George was going to have to write very similar try/catch blocks all throughout the code—everywhere these functions were called. He wanted to solve that duplication problem up front.

Here is what he and Jenna came up with:

function withLogging(f) {

try {

f();

} catch (error) {

logToSnapErrors(error);

}

}

this function encapsulates the repeated code

Using this new function, the try/catch statements above are transformed into this.

Image

Now we have a standard system. But there are still two problems:

  1. We could still forget to log in some places.
  2. We still have to manually write this code everywhere.

Even though there is much less code to duplicate, there is still enough duplication to be annoying. We want to get rid of all of the duplication.

What we really want is a thing to call that has all of the functionality—the original functionality of the code plus the superpower of catching and logging errors. We can write that, but we want something that will write it for us automatically. Let’s see how on the next page.

Snap Errors®

To err is human, to Snap is divine.

From Snap Errors API docs

logToSnapErrors(error)

Send an error to the Snap Errors® service. The error should be an error thrown and caught in your code.

On the last page, we summarized where George’s prototype is now and how it needs to improve. He could wrap any code in a standard way so that it logged errors consistently. Let’s imagine what it would look like if we moved that functionality directly into a function. Luckily, we’re dealing with a prototype, and we can make changes easily. We’ll go back to the original code:

Image

Just to make things super clear, let’s rename those functions to reflect that they don’t do logging on their own:

Image

We can wrap these snippets of code in functions with names that reflect that they do logging. This is also just for clarity.

Image

We are wrapping these two functions that don’t log with logging functionality. This way, any time we call the logging versions, we know the logging is happening. If we have these functions, we don’t have to remember to wrap our code in try/catch blocks. And we only need to write a handful of these superpowered functions instead of wrapping thousands of calls to the no-logging versions.

But now we have new duplication. These two functions are very similar. We want a way to automatically make functions that look like this. Let’s see that on the next page.

On the last page, we had two very similar functions. They did different things, but there was a lot of code in common. We want to deduplicate the code.

Image

Let’s imagine for the moment that these functions didn’t have names. We’ll remove the names and make them anonymous. We will also rename the argument to something generic.

function(arg) {

try {

saveUserDataNoLogging(arg);

} catch (error) {

logToSnapErrors(error);

}

}

function(arg) {

try {

fetchProductNoLogging(arg);

} catch (error) {

logToSnapErrors(error);

}

}

before

body

after

Now we have a very clear case of before, body, and after. Let’s apply our refactoring replace body with callback. But instead of adding a callback to this function, we’ll do what we did originally and wrap it in a new function. Let’s do it to the function on the left:

function(arg) {

 

try {

saveUserDataNoLogging(arg);

} catch (error) {

logToSnapErrors(error);

}

}

function wrapLogging(f) {

return function(arg) {

try {

f(arg);

} catch (error) {

logToSnapErrors(error);

}

}

}

 

var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);

takes function as argument

returns a function

defer execution of superpower code by wrapping in function

assign the return value to a variable to name it

call wrapLogging() with the function we want to transform

Now, wrapLogging() takes a function f and returns a function that wraps f in our standard try/catch. We can take our non-logging versions and easily convert them to logging versions. We can give any function logging superpowers!

var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);

var fetchProductWithLogging = wrapLogging(fetchProductNoLogging);

We have eliminated the duplication. Now we have an easy way to add standard logging behavior to any function. Let’s review.

On the last page, we created a function that can wrap any function we want in our logging superpower. Let’s look at the before and after:

Image

Of course, a lot went on behind the scenes. We have a function that will give any function this same superpower:

function wrapLogging(f) {

return function(arg) {

try {

f(arg);

} catch (error) {

logToSnapErrors(error);

}

}

}

And we can use that to define saveUserDataWithLogging() in terms of saveUserData(). Let’s also visualize it:

Image

Returning functions from functions lets us make function factories. They automate the creation of functions and codify a standard.

Image Brain break

There’s more to go, but let’s take a break for questions

Q: You’re assigning the return value of that function to a variable, but I’m used to all functions being defined using the function keyword at the top level. Won’t it be confusing?

A: That’s a good question. It does take a little bit of getting used to. But even without using this pattern, you were probably already using other signals to know which variables contain data and which contain functions. For instance, functions typically are named using verbs, while variables are named using nouns.

What you have to get used to is that there are different ways to define functions. Sometimes you define them directly with code you write; sometimes you define them as the return value from another function.

Q: The function wrapLogging() took a function that worked on one argument. How can you make it so it works on more? And how can we get the return value from a function?

A: Great questions. The return value is easy. Just add a return keyword to return it from the inner function. That will be the return value of the new function you’re making.

Dealing with variable arguments in classic JavaScript could be kind of a pain. It is much easier in ES6, the new style of JavaScript. If you’re using ES6, you should search for rest arguments and the spread operator. Other languages may have similar features.

However, it’s not that difficult in practice, even in classic JavaScript, since JavaScript is very flexible with arguments if you pass too many or not enough. And in practice, we rarely have functions with more than a handful of arguments.

If you want to make wrapLogging() work for functions with up to nine arguments, you could do this:

function wrapLogging(f) {

return function(a1, a2, a3, a4, a5, a6, a7, a8, a9) {

try {

return f(a1, a2, a3, a4, a5, a6, a7, a8, a9);

} catch (error) {

logToSnapErrors(error);

}

}

}

JavaScript will ignore unused arguments when you call it

just return from the inner function to get the return value out

There are other methods, but this one is simple to explain and doesn’t require deeper understanding of JavaScript. Look up in your language how to apply functions to variable numbers of arguments.

Image It’s your turn

Write a function that transforms the function you pass it into a function that catches and ignores all errors. If there is an error, just return null. Make it work on functions of at least three arguments.

Hint

  • You normally ignore errors by wrapping code in a try/catch and doing nothing in the catch.

    try {

    codeThatMightThrow();

    } catch(e) {

    // ignore errors by doing nothing

    }

Image

Image

Image Answer

function wrapIgnoreErrors(f) {

return function(a1, a2, a3) {

try {

return f(a1, a2, a3);

} catch(error) { // error is ignored

return null;

}

};

}

Image It’s your turn

Write a function called makeAdder() that makes a function to add a number to another number. For instance,

var increment = makeAdder(1);

 

> increment(10)

11

var plus10 = makeAdder(10);

 

> plus10(12)

22

Image

Image

Image Answer

function makeAdder(n) {

return function(x) {

return n + x;

};

}

Image Brain break

There’s more to go, but let’s take a break for questions

Q: It looks like there’s a lot we can do by returning functions from higher-order functions. Can we write our whole program that way?

A: That’s a great question. It is probably possible to write your whole program using nothing but higher-order functions. The better question, though, is whether you would want to.

Writing functions this way is more general. It is very easy to get carried away with the fun of writing higher-order functions. It tickles a center in our brain that makes us feel clever, like solving intricate puzzles. Good engineering is not about solving puzzles. It’s about solving problems in effective ways.

The truth is, you should use higher-order functions for their strength, which is reducing the repetition we find in codebases. We loop a lot, so it’s nice to have a higher-order function for getting those right (forEach()). We catch errors a lot, so something to do that in a standard way might also be helpful.

Many functional programmers do get carried away. There are entire books written about how to do the simplest things using only higher-order functions. But when you look at the code, is it really clearer than writing it the straightforward way?

You should explore and experiment. Try higher-order functions in lots of places for lots of purposes. Find new uses for them. Explore their limits. But do so outside of production code. Remember that the exploration is just for learning.

When you do come up with a solution using a higher-order function, compare it to the straightforward solution. Is it really better? Does it make the code clearer? How many duplicate lines of code are you really removing? How easy would it be for someone to understand what the code is doing? We mustn’t lose sight of that.

Bottom line: These are powerful techniques, but they come at a cost. They’re a little too pleasant to write, and that blinds us to the problem of reading them. Get good at them, but only use them when they really make the code better.

Conclusion

This chapter deepened the ideas of first-class values, first-class functions, and higher-order functions. We are going to be exploring the potential of these ideas in the next chapters. After the distinction between actions, calculations, and data, the idea of higher-order functions opens a new level of functional programming power. The second part of this book is all about that power.

Summary

  • Higher-order functions can codify patterns and disciplines that otherwise we would have to maintain manually. Because they are defined once, we can get them right once and can use them many times.
  • We can make functions by returning them from higher-order functions. The functions can be used just like normal by assigning them to a variable to give them a name.
  • Higher-order functions come with a set of tradeoffs. They can remove a lot of duplication, but sometimes they cost readability. Learn them well and use them wisely.

Up next…

In the last chapter we saw a function called forEach() that let us iterate over arrays. In the next chapter, we will explore a functional style of iteration by expanding on that idea. We will learn three functional tools that capture common patterns of iteration over arrays.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.63.145