- HOW TO DEAL WITH DIRTY SIDE EFFECTS IN YOUR PURE FUNCTIONAL JAVASCRIPT
- By James Sinclair
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: Gavin – Gong
- Proofreader: Huangyuanzhen, AceLeeWinnie
How do I handle dirty side effects using purely functional JavaScript
First, assume that you dabble in functional programming. It won’t take you long to understand the concept of pure functions. As you dig deeper, functional programmers seem to be obsessed with pure functions. They say, “Pure functions let you drill code,” “Pure functions are unlikely to start a thermonuclear war,” and “pure functions provide referential transparency.” And so on. They’re right. Pure functions are a good thing. But there’s a problem…
A pure function is one that has no side effects. [1] But if you know anything about programming, you know that side effects are key. If you can’t read the 𝜋 value, why evaluate it in so many places? To print out the value, we need to write a console statement, send it to printer, or something else that can be read. What good is a database if it can’t enter any data? We need to read data from the input device and request information over the network. None of these things are without side effects. Functional programming, however, is based on pure functions. So how does a functional programmer accomplish a task?
In short, do what mathematicians do: cheat.
Call them cheaters and technically follow the rules. But they found loopholes in the rules and took advantage of them. There are two main approaches:
- Dependency injection, or what we might call problem shelving
- Using the Effect functor, we can think of it as severe procrastination [2]
Dependency injection
Dependency injection is the first way we deal with side effects. In this approach, impure parts of the code are put into function parameters, and then we can treat them as part of the functionality of other functions. To illustrate what I mean, let’s look at some code:
// logSomething :: String -> ()
function logSomething(something) {
const dt = new Date().toIsoString();
console.log(`${dt}: ${something}`);
return something;
}
Copy the code
The logSomething() function is impure in two ways: it creates a Date() object and prints it to the console. Therefore, it not only performs IO operations, but also gives different results each time it runs. So, how do I make this function pure? With dependency injection, we accept impure parts in the form of function arguments, so the logSomething() function takes three arguments instead of one:
// logSomething: Date -> Console -> String -> ()
function logSomething(d, cnsl, something) {
const dt = d.toIsoString();
cnsl.log(`${dt}: ${something}`);
return something;
}
Copy the code
Then call it, and we must pass in the impure part explicitly ourselves:
const something = "Curiouser and curiouser!";
const d = new Date(a); logSomething(d,console, something);
// Quirouser and Curiouser!
Copy the code
Now, you might be thinking, “That’s kind of stupid. This makes the problem worse, and the code is just as impure as before. You’re right. It’s a total loophole.
YouTube video link: youtu.be/9ZSoJDUD_bU
It’s like playing dumb: “Oh! No! Officer, I didn’t know that calling log() on CNSL would perform IO operations. It was handed down to me. I don’t know where it came from “, which seems a bit lame.
This is not as stupid as it seems, notice our logSomething() function. If you’re going to deal with something impure, you have to make it impure. We can simply pass in different arguments:
const d = {toISOString: (a)= > "The 1865-11-26 T16:00:00) 000 z"};
const cnsl = {
log: (a)= > {
// do nothing}}; logSomething(d, cnsl,"Off with their heads!");
// ← "Off with their heads!"
Copy the code
Now, our function does nothing but return something. But it’s pure. If you call it with the same arguments, it will return the same result every time. That’s the point. In order to impure it, we must take deliberate action. Or in other words, the function depends on the signature on the right-hand side. Functions cannot access global variables like console or Date. That makes everything very clear.
It is also important to note that we can also pass functions to previously impure functions. Let’s look at another example. Suppose the form has a username field. We want to get its value from the form:
// getUserNameFromDOM :: () -> String
function getUserNameFromDOM() {
return document.querySelector("#username").value;
}
const username = getUserNameFromDOM();
username;
/ / please mhatter ""
Copy the code
In this example, we try to query information from the DOM. This is impure because document is a global variable that can change at any time. One way to turn our function into a pure function is to pass in the global Document object as a parameter. But we could also pass a querySelector() function like this:
// getUserNameFromDOM :: (String -> Element) -> String
function getUserNameFromDOM($) {
return $("#username").value;
}
// qs :: String -> Element
const qs = document.querySelector.bind(document);
const username = getUserNameFromDOM(qs);
username;
/ / please mhatter ""
Copy the code
Now, you might still be thinking, “That’s just as stupid!” All we did was remove the impure code from getUsernameFromDOM(). It doesn’t disappear, we just put it in another function qs(). It doesn’t seem to do much except make the code longer. Our two functions replace the previous impure function, but one of them is still impure.
Don’t worry, let’s say we want to write a test for getUserNameFromDOM(). Now, comparing the impure version to the pure version, which is easier to write tests for? To test the impure version of the function, we need a global Document object, along with an element with an ID of USERNAME. If I want to test it outside of the browser, then I have to import something like JSDOM or a headless browser. All this to test a very small function. But using the second version of the function, I can do this:
const qsStub = (a)= > ({value: "mhatter"});
const username = getUserNameFromDOM(qsStub);
assert.strictEqual("mhatter", username, `Expected username to be ${username}`);
Copy the code
Now, this doesn’t mean you shouldn’t create integration tests that run in a real browser. (Or, at least, an emulated version like JSDOM). But what this example shows is that getUserNameFromDOM() is now completely predictable. If we pass it qsStub it always returns mhatter. We have moved the unpredictability into the smaller function QS.
If we do that, we can push the unpredictability further and further away. Finally, we push them to the edge of the code. So we end up with a thin shell of impure code that surrounds a test-friendly, predictable core. This predictability helps a lot when you start building larger applications.
Disadvantages of dependency injection
Large, complex applications can be created in this way. I know because I’ve done it. Dependency injection makes testing easier and makes the dependencies of each function clear. But it also has some disadvantages. The main thing is that you end up with long function signatures like this:
function app(doc, con, ftch, store, config, ga, d, random) {
// Here is the application code
}
app(document.console, fetch, store, config, ga, new Date(), Math.random);
Copy the code
That’s not too bad, except you might have parametric drilling problems. In an underlying function, you might need one of these arguments. Therefore, you must concatenate parameters through many layers of function calls. It’s irritating. For example, you might need to pass dates through five layers of intermediate functions. None of these intermediate functions use date objects. It’s not the end of the world, at least it’s nice to see these explicit dependencies. But it’s still irritating. There’s another way…
Lazy function
Let’s look at the second vulnerability exploited by functional programmers. It goes something like this: “Side effects are side effects that occur.” I know it sounds mysterious. Let’s try to make it a little more explicit. Consider this code:
// fZero :: () -> Number
function fZero() {
console.log("Launching nuclear missiles");
// Here is the code to launch the nuke
return 0;
}
Copy the code
I know it’s a silly example. If we wanted to have a 0 in our code, we could just write it out. I know you, gentle reader, would never write code to control a nuclear weapon in JavaScript. But it helps to illustrate the point. This is clearly impure code. As it outputs logs to the console, it may also start a thermonuclear war. Let’s say we want 0. Let’s say we want to calculate what happens after a missile launches, we might need to start something like a countdown. In this case, it makes perfect sense to plan ahead on how to do the calculations. We will be very careful when these missiles fly, we don’t want to mess up our calculations in case they accidentally launch them. So what if we wrapped fZero() in another function that just returns it? It’s kind of like safety packaging.
// fZero :: () -> Number
function fZero() {
console.log("Launching nuclear missiles");
// Here is the code to launch the nuke
return 0;
}
// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
return fZero;
}
Copy the code
I can run returnZeroFunc() as many times as I want, and as long as I don’t call the return value, I’m theoretically safe. My code doesn’t launch any nukes.
const zeroFunc1 = returnZeroFunc();
const zeroFunc2 = returnZeroFunc();
const zeroFunc3 = returnZeroFunc();
// No nukes were launched.
Copy the code
Now, let’s define pure functions more formally. We can then examine our returnZeroFunc() function in more detail. A function is called pure if it meets the following conditions:
- There are no obvious side effects
- Reference transparency. That is, given the same input, it always returns the same output.
Let’s look at returnZero of unc(). Are there any side effects? Well, we made sure that calling returnZeroFunc() doesn’t launch any nuclear missiles. Nothing happens unless you perform the extra step of calling the return function. So, this function has no side effects.
Is the returnZeroFunc() reference transparent? That is, given the same input, it always returns the same output, right? Well, the way it’s written so far, we can test it:
zeroFunc1 === zeroFunc2; // true
zeroFunc2 === zeroFunc3; // true
Copy the code
But it’s not pure yet. The returnZeroFunc() function refers to a variable outside the function scope. To solve this problem, we can rewrite it in this way:
// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
function fZero() {
console.log("Launching nuclear missiles");
// Here is the code to launch the nuke
return 0;
}
return fZero;
}
Copy the code
Now our function is pure. But JavaScript gets in the way. We can no longer use === to verify reference transparency. This is because returnZeroFunc() always returns a new function reference. But you can check reference transparency by reviewing the code. The returnZeroFunc() function does nothing but return the same function each time.
This is a clever little loophole. But can we really use it in real code? The answer is yes. But before we talk about how to implement it in practice, let’s put that aside. Back to the dangerous fZero() function:
// fZero :: () -> Number
function fZero() {
console.log("Launching nuclear missiles");
// Here is the code to launch the nuke
return 0;
}
Copy the code
Let’s try using the zero returned by fZero(), but that won’t start a thermonuclear war (laughs). We’ll create a function that takes the final zero returned by fZero() and adds one to it:
// fIncrement :: (() -> Number) -> Number
function fIncrement(f) {
return f() + 1;
}
fIncrement(fZero);
// The missile is fired happily
/ / please 1
Copy the code
Ouch! We accidentally started a thermonuclear war. Let’s try again. This time, we’re not going to return a number. Instead, we’ll return a function that eventually returns a number:
// fIncrement :: (() -> Number) -> (() -> Number)
function fIncrement(f) {
return (a)= > f() + 1;
}
fIncrement(zero);
/ / please [Function]
Copy the code
Yo! Crisis averted. Let’s move on. With these two functions, we can create a series of ‘final numbers’ :
const fOne = fIncrement(zero);
const fTwo = fIncrement(one);
const fThree = fIncrement(two);
/ / and so on...
Copy the code
We can also create a set of f*() functions to handle the final value:
// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number)
function fMultiply(a, b) {
return (a)= > a() * b();
}
// fPow :: (() -> Number) -> (() -> Number) -> (() -> Number)
function fPow(a, b) {
return (a)= > Math.pow(a(), b());
}
// fSqrt :: (() -> Number) -> (() -> Number)
function fSqrt(x) {
return (a)= > Math.sqrt(x());
}
const fFour = fPow(fTwo, fTwo);
const fEight = fMultiply(fFour, fTwo);
const fTwentySeven = fPow(fThree, fThree);
const fNine = fSqrt(fTwentySeven);
// No console log or thermonuclear war. Good job!
Copy the code
See what we did? If we can do it with regular numbers, we can do it with final numbers. Mathematics calls this isomorphism. We can always take an ordinary number and put it in a function and turn it into a final number. We can get the final number by calling this function. In other words, we set up a mapping between the number and the final number. This is more exciting than it sounds. I promise, we’ll get back to that very soon.
This is a legitimate strategy for function wrapping. We can hide behind the function for as long as we want. As long as we don’t call these functions, they’re theoretically pure. World peace. In regular (non-core) code, we actually want those side effects to run in the end. Wrapping everything in a function gives us precise control over these effects. We decide exactly when these side effects will occur. But typing those parentheses is a pain. Creating a new version of each function is annoying. We have some very nice functions built into the language, such as math.sqrt (). If only there was a way to use these ordinary functions with delay values. Move on to the next section, Effect functors.
Effect functor
For purposes, an Effect functor is simply an object into which a delay function is placed. We want to put the fZero function into an Effect object. But before you do that, take it down a notch
// zero :: () -> Number
function fZero() {
console.log("Starting with nothing");
// There will be no nuclear strike here.
// But this function is still not pure
return 0;
}
Copy the code
Now we create a constructor that returns an Effect object
// Effect :: Function -> Effect
function Effect(f) {
return {};
}
Copy the code
So far, there’s not much to see. Let’s do something useful. We want to use the regular fZero() function with Effetct. We’ll write a method that takes the normal function and delays the return value, and it runs without firing any effects. We call this map. This is because it creates a mapping between the regular function and the Effect function. It might look something like this:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x= >g(f(x))); }}; }Copy the code
Now, if you’re watching carefully, you might be wondering what map() does. It looks like a combination. We’ll talk about that later. Now, let’s try:
const zero = Effect(fZero);
const increment = x= > x + 1; // An ordinary function.
const one = zero.map(increment);
Copy the code
Well. We didn’t see what happened. Let’s modify Effect so we have a way to “pull the trigger.” It can be written like this:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x= > g(f(x)));
},
runEffects(x) {
returnf(x); }}; }const zero = Effect(fZero);
const increment = x= > x + 1; // Just a normal function
const one = zero.map(increment);
one.runEffects();
// And nothing is started up
/ / please 1
Copy the code
And we can call the map function as long as we want:
const double = x= > x * 2;
const cube = x= > Math.pow(x, 3);
const eight = Effect(fZero)
.map(increment)
.map(double)
.map(cube);
eight.runEffects();
// And nothing is started up
/ / please 8
Copy the code
This is where it gets interesting. We call this a functor, which means that Effect has a map function that follows some rules. These rules don’t mean you can’t do it. They are your code of conduct. They’re more like priorities. Because Effect is part of the functor family, it can do a few things, one of which is called a “composition rule.” It looks like this:
If we have an Effect e, two functions f and g then e.map.map (f) is the same as e.map.x (x => f(g(x))).
In other words, writing two map functions in a row is equivalent to combining them. That is, Effect could be written like this (recall the above example) :
const incDoubleCube = x= > cube(double(increment(x)));
// If you are using a library like Ramda or Lodash/FP, we can also write:
// const incDoubleCube = compose(cube, double, increment);
const eight = Effect(fZero).map(incDoubleCube);
Copy the code
When we do this, we can be sure that we get the same results as the triple Map version. We can use it to refactor our code and be confident that it won’t crash. In some cases, we can even improve performance by swapping between different methods.
But enough of these examples, let’s get started.
Effect shorthand
Our Effect constructor takes a function as its argument. This is handy because most of the side effects we want to delay are also functions. For example, math.random () and console.log() are both of these types. But sometimes we want to compress a plain old value into an Effect. For example, suppose we append some configuration object to the browser’s Window global object. We want to get a value of A, but it’s not a pure operation. We can write a small shorthand to make the task easier: [3]
// of :: a -> Effect a
Effect.of = function of(val) {
return Effect((a)= > val);
};
Copy the code
To illustrate how handy this might be, suppose we’re working with a Web application. The app has some standard features, such as a list of articles and user profiles. But in HTML, these components are presented to different customers. Because we are smart engineers, we decided to store their locations in a global configuration object so we could always find them. Such as:
window.myAppConf = {
selectors: {
"user-bio": ".userbio"."article-list": "#articles"."user-name": ".userfullname"
},
templates: {
greet: "Pleased to meet you, {name}".notify: "You have {n} alerts"}};Copy the code
Now with effect.of (), we can quickly wrap the values we want into an Effect container, like this
const win = Effect.of(window);
userBioLocator = win.map(x= > x.myAppConf.selectors["user-bio"]);
/ / please Effect (' userbio ')
Copy the code
Embedded and unembedded Effect
Mapping effects can help us a lot. However, we sometimes encounter situations where the mapped function also returns an Effect. We have defined a getElementLocator() that returns an Effect containing a string. If we really want to get the DOM element, we need to call another impure function document.querySelector(). So we might purify it by returning an Effect:
// $ :: String -> Effect DOMElement
function $(selector) {
return Effect.of(document.querySelector(s));
}
Copy the code
Now if we want to put the two together, we can try using map() :
const userBio = userBioLocator.map($);
/ / please Effect (Effect (< div >))
Copy the code
It’s a little awkward to actually make it work. If we want to access that div, we have to use a function to map what we want to do. For example, if we wanted to get innerHTML, it would look like this:
const innerHTML = userBio.map(eff= > eff.map(domEl= > domEl.innerHTML));
/ / please Effect (Effect (' < h2 > User Biography < / h2 > '))
Copy the code
Let’s try to break it down. We’ll go back to userBio and continue. It’s a little tedious, but we’re trying to figure out what’s going on here. The tag Effect(‘user-bio’) we used was a bit misleading. If we wrote it in code, it would look more like this:
Effect((a)= > ".userbio");
Copy the code
But that’s not accurate either. What we really do is:
Effect((a)= > window.myAppConf.selectors["user-bio"]);
Copy the code
Now, when we map, it is equivalent to combining an inner function with another function (as we saw above). So when we use $mapping, it looks like this:
Effect((a)= > window.myAppConf.selectors["user-bio"]);
Copy the code
Expand it to get:
Effect(
(a)= > Effect.of(document.querySelector(window.myAppConf.selectors['user-bio'))));Copy the code
Expanding Effect. Of gives us a clearer overview:
Effect((a)= >
Effect((a)= > document.querySelector(window.myAppConf.selectors["user-bio")));Copy the code
Note: All of the code that actually performs the operation is in the innermost function, and none of this leaks out to the external Effect.
Join
Why do you spell it that way? We want these inline effects to be in non-inline form. Make sure that no unexpected side effects are introduced during the conversion process. For Effect, the way not to embed is to call.runeffects () externally. But that can be confusing. We have gone through the entire exercise to check that we are not running any effects. We’ll create another function that does the same thing and call it Join. We use Join to solve the problems embedded in Effect, and runEffects() to actually run all effects. Even if the running code is the same, it makes our intentions clearer.
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x= > g(f(x)));
},
runEffects(x) {
return f(x);
}
join(x) {
returnf(x); }}}Copy the code
It can then be used to untangle embedded user profile elements:
const userBioHTML = Effect.of(window)
.map(x= > x.myAppConf.selectors["user-bio"])
.map($)
.join()
.map(x= > x.innerHTML);
/ / please Effect (' < h2 > User Biography < / h2 > ')
Copy the code
Chain
The.map() pattern is often followed by.join(). In fact, it’s handy to have a shorthand function. This way, whenever we have a function that returns Effect, we can use the shorthand function. It saves us from writing a Map over and over again and then following a Join. We write it like this:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x= > g(f(x)));
},
runEffects(x) {
return f(x);
}
join(x) {
return f(x);
}
chain(g) {
returnEffect(f).map(g).join(); }}}Copy the code
We call the new function chain() because it allows us to link effects together. (It’s also because the standard says you can call it this way). [4] The innerHTML of the user profile element might look something like this:
const userBioHTML = Effect.of(window)
.map(x= > x.myAppConf.selectors["user-bio"])
.chain($)
.map(x= > x.innerHTML);
/ / please Effect (' < h2 > User Biography < / h2 > ')
Copy the code
Unfortunately, other functional languages have different names for this implementation. If you read it, you might be a little confused. It is sometimes called a flatMap, which makes sense because we do a normal mapping and then flatten the result using.join(). But in Haskell, chain is given the confusing name bind. So if you read this elsewhere, remember that chain, flatMap, and bind are actually references to the same concept.
Combining with the Effect
This is the last situation where using Effect is a bit awkward, where we want to combine two or more functors in a function. For example, how do I get a user’s name from the DOM? What about getting the name and then inserting it into the template provided by the application configuration? Therefore, we might have a template function (note that we will create a corrified version of the function)
// tpl :: String -> Object -> String
const tpl = curry(function tpl(pattern, data) {
return Object.keys(data).reduce(
(str, key) = > str.replace(new RegExp(` {${key}} `, data[key]),
pattern
);
});
Copy the code
Everything is fine, but now to get the data we need:
const win = Effect.of(window);
const name = win.map(w= > w.myAppConfig.selectors['user-name'])
.chain($)
.map(el= > el.innerHTML)
.map(str= > ({name: str});
// ← Effect({name: 'Mr. Hatter'});
const pattern = win.map(w= > w.myAppConfig.templates('greeting'));
// ← Effect('Pleased to meet you, {name}');
Copy the code
We already have a template function. It takes a string and an object and returns a string. But our strings and objects (name and pattern) are already wrapped in Effect. All we have to do is elevate our TPL () function to a higher level so that it works well with Effect.
Let’s see what happens if we call TPL () with map() on pattern Effect:
pattern.map(tpl);
/ / please Effect ([Function])
Copy the code
A comparison of genres might make things a little clearer. A map function declaration might look like this:
_map :: Effect a ~> (a -> b) -> Effect b_
Copy the code
This is the function declaration for the template function:
_tpl :: String -> Object -> String_
Copy the code
Therefore, when we call map on pattern, we get a partial application function inside Effect (remember that we generalized TPL).
_Effect (Object -> String)_
Copy the code
Now we want to pass values from within the pattern Effect, but we don’t have a way to do that yet. We’ll write another Effect method (called AP ()) to handle this problem:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x= > g(f(x)));
},
runEffects(x) {
return f(x);
}
join(x) {
return f(x);
}
chain(g) {
return Effect(f).map(g).join();
}
ap(eff) {
If someone calls ap, we assume there is a function inside eff instead of a value.
// We will use map to get inside eff and access that function
// Get g, pass in the return value of f()
return eff.map(g= >g(f())); }}}Copy the code
With it, we can apply our template function by running.ap() :
const win = Effect.of(window);
const name = win
.map(w= > w.myAppConfig.selectors["user-name"])
.chain($)
.map(el= > el.innerHTML)
.map(str= > ({ name: str }));
const pattern = win.map(w= > w.myAppConfig.templates("greeting"));
const greeting = name.ap(pattern.map(tpl));
// ← Effect('Pleased to meet you, Mr Hatter')
Copy the code
We have achieved our goals. One thing I will admit, though, is that I find AP () confusing at times. It’s hard to remember that I have to map the function first and then run ap(). And then I might forget the order of the arguments. But there is a way around this. Most of the time, what I want to do is elevate a normal function into the world of applications. That is, I have simple functions and I want them to work with Effect with the.ap() method. We can write a function to do this:
// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c)
const liftA2 = curry(function liftA2(f, x, y) {
return y.ap(x.map(f));
// We can also write:
// return x.map(f).chain(g => y.map(g));
});
Copy the code
We call it liftA2() because it elevates a function that takes two arguments. We could write a similar liftA3(), like this:
// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d)
const liftA3 = curry(function liftA3(f, a, b, c) {
return c.ap(b.ap(a.map(f)));
});
Copy the code
Note that liftA2 and liftA3 never mention Effect. In theory, they can work with any object that has compatible AP () methods. Using liftA2() we can rewrite the previous example as follows:
const win = Effect.of(window);
const user = win.map(w= > w.myAppConfig.selectors['user-name'])
.chain($)
.map(el= > el.innerHTML)
.map(str= > ({name: str});
const pattern = win.map(w= > w.myAppConfig.templates['greeting']);
const greeting = liftA2(tpl)(pattern, user);
// ← Effect('Pleased to meet you, Mr Hatter')
Copy the code
So what?
At this point you might be thinking, “That seems like a lot of effort to avoid the weird side effects that are everywhere.” What does that matter? Passing in parameters inside Effect, encapsulating ap() seems like a daunting task. Why bother when impure code works? When would you need this in a real world scenario?
Functional programmers sound like medieval monks who abstained from worldly pleasures in the hope that it would make them virtuous.
– John Hughes [5]
Let’s break these objections down into two questions:
- Does function purity really matter?
- When is it useful in a real world situation?
Importance of function purity
Function purity does matter. When you look at a small function in isolation, a little side effect doesn’t matter. Write a const pattern = window. MyAppConfig. Templates [‘ greeting ‘]; It’s faster and easier than writing code like this.
const pattern = Effect.of(window).map(w= > w.myAppConfig.templates("greeting"));
Copy the code
If your code is full of little functions like this, you can keep doing it, and the side effects aren’t a problem. But this is just one line of code in an application that can contain thousands or even millions of lines of code. Function purity becomes even more important when you’re trying to figure out why your application somehow stops working “seemingly for no reason.” If something unexpected happens, you try to break it down and figure out why. In this case, the more code you can exclude, the better. If your functions are pure, you can be sure that the only thing that affects their behavior is the input passed to them. This significantly Narrows down the range of exceptions to consider. In other words, it makes you think less. This is especially important in large, complex applications.
Effect mode in the actual scenario
All right. If you’re building a large, complex application, like Facebook or Gmail. So the purity of the function might be important. But what if it’s not a big app? Let’s consider an increasingly common scenario. You have some data. Not just a little bit of data, but a lot of data — millions of rows, in CSV text files or large database tables. Your job is to process the data. Maybe you’re training an artificial neural network to build a model of reasoning. Maybe you’re trying to figure out the next big thing in cryptocurrencies. Anyway, the problem is that it takes a lot of processing to get the job done.
Joel Spolsky has convincingly argued that functional programming can help solve this problem. We can write alternative versions of Map and Reduce that run in parallel, and functional purity makes this possible. But that’s not the end of the story. Of course, you can write some fancy parallel processing code. But even then, your development machine will still only have four kernels (maybe eight or 16 if you’re lucky). That work still takes a long time. Unless, that is, you can run it on a bunch of processors, like a GPU, or an entire cluster of processing servers.
To make this work, you need to describe the computation you want to run. However, you need to describe them without actually running them. Sound familiar? Ideally, you should pass the description to some kind of framework. The framework is carefully responsible for reading all the data and splitting it among the processing nodes. The framework then gathers the results together and tells you how it works. This is how TensorFlow works.
TensorFlow™ is a high-performance open source software library for numerical computation. Its flexible architecture supports cross-platform (CPU, GPU, AND TPU) computing deployment from desktop to server cluster, and from mobile devices to edge devices. TensorFlow was originally developed by researchers and engineers in the Google Brain group within the Google AI organization to support machine learning and deep learning, and its flexible numerical computing kernel is also used in other scientific fields.
– TensorFlow homepage [6]
When you use TensorFlow, you don’t use the normal data types in your programming language. Instead, you need to create tensors. If we wanted to add two numbers, it would look like this:
node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0, tf.float32)
node3 = tf.add(node1, node2)
Copy the code
The above code is written in Python, but it doesn’t look too different from JavaScript, does it? Like our Effect, add doesn’t run until we call it (in this case, sess.run()) :
print("node3: ", node3)
print("sess.run(node3): ", sess.run(node3))
# courtesnode3: Tensor(" add_2:0 ", Shape =(), dType =float32)
# ⦘ sess. Run (node3) : 7.0
Copy the code
We don’t get 7.0 until we call sess.run(). As you can see, it’s very similar to the delay function. We planned our calculations in advance. Then, as soon as you’re ready, go to war.
conclusion
This article covers a lot of ground, but we’ve explored two ways to deal with function purity in code:
- Dependency injection
- Effect functor
Dependency injection works by moving impure parts of code out of functions. So you have to pass them in as arguments. An Effect functor, by contrast, works by wrapping everything behind a function. To run these effects, we must first run the wrapper function.
Both methods are cheating. They don’t get rid of impurity completely, they just push it to the edge of the code. But that’s a good thing. It specifies exactly which parts of the code are impure. This has advantages when debugging problems in complex code bases.
-
This is not a complete definition, but it can be used temporarily. We’ll come back to the formal definition later. ↩
-
In other languages (such as Haskell), this is called an IO functor or IO monad. PureScript uses Effect as the term. I find it more descriptive. ↩
-
Note that different languages have different names for this abbreviation. For example, in Haskell, it is called pure. I don’t know why. ↩
-
In this example, the Fantasy Land Specification for Chain specification is used. ↩
-
John Hughes, 1990, ‘Why Functional Programming Matters’, Research Topics in Functional Programming Ed. D. Turner, Addison Wesley, pp. 17-42, www.cs.kent.ac.uk/people/staf… ↩
-
TensorFlow™ : Open Source Machine learning Framework for Everyone, www.tensorflow.org/, 12 May 2018. ↩
- [Welcome to Twitter](twitter.com/share?url=h… to deal with dirty side effects in your pure functional JavaScript%E2%80%9D+by+%40jrsinclair)
- Subscribe to the latest news via email system
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.