In this chapter
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.
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.
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.
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.
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.
We’ll be iteratively applying these powerful skills so that they become second nature.
Steps of replace body with 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
** before
*** body
**** after
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
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
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.
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
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.
Our next step is to extract the body into a callback. Because the callback will modify the array, we’ll call it modify.
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.
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 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
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.
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
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.
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.
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.
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.
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:
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.
Now we have a standard system. But there are still two problems:
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.
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:
Just to make things super clear, let’s rename those functions to reflect that they don’t do logging on their own:
We can wrap these snippets of code in functions with names that reflect that they do logging. This is also just for clarity.
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.
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:
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:
Returning functions from functions lets us make function factories. They automate the creation of functions and codify a standard.
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.
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.
52.15.63.145