📜 ⬆️ ⬇️

Combating dirty side effects in pure, functional JavaScript code

If you try your hand at functional programming, it means that you will soon come across the concept of pure functions. As you continue your studies, you will find that programmers who prefer a functional style seem to be obsessed with these functions. They say that pure functions allow you to talk about code. They say that pure functions are entities that are unlikely to work so unpredictably that they will lead to thermonuclear war. You can also find out from such programmers that pure functions provide referential transparency. And so - to infinity.

By the way, functional programmers are right. Pure functions are good. But there is one problem…


The author of the material, the translation of which we present to your attention, wants to talk about how to deal with the side effects in pure functions.

The problem of clean functions


A pure function is a function that has no side effects (in fact, this is not a complete definition of a pure function, but we will return to this definition). However, if you at least understand something in programming, then you know that the most important thing here is precisely the side effects. Why calculate the number Pi to the hundredth decimal place if no one can read this number? In order to display something on the screen or print it out on a printer, or present it in some other form that is accessible for perception, we need to call the appropriate command from the program. And what is the use of databases, if nothing can be recorded in them? To ensure the operation of applications, you need to read data from input devices and request information from network resources. All this can not be done without side effects. But, despite this state of affairs, functional programming is built around pure functions. How do programmers who write programs in a functional style manage to solve this paradox?
')
If you answer this question in a nutshell, functional programmers do the same thing as mathematics: they cheat. Although, despite this accusation, I must say that, from a technical point of view, they simply follow certain rules. But they find loopholes in these rules and expand them to incredible sizes. They do it in two main ways:

  1. They use dependency injection (dependency injection). I call it throwing a problem over the fence.
  2. They use functors (functor), which seems to me an extreme form of procrastination. It should be noted here that in Haskell it is called “IO functor” or “IO monad”, in PureScript the term “Effect” is used, which, I think, is a little better suited to describe the essence of functors.

Dependency injection


Dependency injection is the first method of dealing with side effects. Using this approach, we take everything that pollutes the code and put it into the parameters of the function. Then we can consider all this as something that falls under the responsibility of some other function. Let me explain this with the following example:

// logSomething :: String -> String function logSomething(something) {    const dt = (new Date())toISOString();    console.log(`${dt}: ${something}`);    return something; } 

Here I would like to make a note for those who are familiar with type signatures. If we strictly adhered to the rules, we would need to take into account here the side effects. But we will deal with this later.

The logSomething() function has two problems that do not allow it to be recognized as clean: it creates a Date object and outputs something to the console. That is, our function not only performs I / O operations, it also produces, when it is called at different times, different results.

How to make this feature clean? With the help of dependency injection technique, we can take everything that pollutes a function and make it function parameters. As a result, instead of taking one parameter, our function will take three parameters:

 // logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) {   const dt = d.toIsoString();   return cnsl.log(`${dt}: ${something}`); } 

Now, in order to call a function, we need to independently transfer to it everything that it has contaminated before:

 const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something); //  "Curiouser and curiouser!" 

Here you might think that all this is nonsense, that we only moved the problem one level up, and this did not add purity to our code. And you know, these are the right thoughts. This is a loophole in its purest form.

It looks like a feigned ignorance: “I didn’t know that calling the log method of a cnsl object cnsl lead to an I / O operator. I just gave it to someone, but I don’t know where all this came from. ” This attitude is wrong.

And, in fact, what is happening is not so stupid as it may seem at first glance. Take a closer look at the features of the logSomething() function. If you want to do something unclean, then you must do it yourself. Let's say this function can pass various parameters:

 const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = {   log: () => {       //      }, }; logSomething(d, cnsl, "Off with their heads!"); //   "Off with their heads!" 

Now our function does nothing (it only returns the parameter something ). But she is completely clean. If you call it with the same parameters several times, it will return the same thing every time. And the whole thing is this. In order to make this function unclean, we need to deliberately perform certain actions. Or, to put it another way, everything that a function depends on is in its signature. It does not refer to any global objects like console or Date . This all formalizes.

In addition, it is important to note that we can transfer other functions to our function, which previously did not differ in purity. Take a look at another example. Imagine that in some form there is a user name and we need to get the value of the corresponding field of this form:

 // getUserNameFromDOM :: () -> String function getUserNameFromDOM() {   return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; //   "mhatter" 

In this case, we are trying to load some information from the DOM. Pure functions do not do this, since document is a global object that can change at any time. One way to make such a function clean is to pass a global document object as a parameter to it. However, it can still be passed the function querySelector() . It looks 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; //   "mhatter" 

Here, again, you might get the idea that this is stupid. After all, here we just removed from the function getUsernameFromDOM() that does not allow to call it clean. However, we didn’t get rid of this, just by transferring the call to the DOM to another function, qs() . It may seem that the only noticeable result of this step was that the new code was longer than the old one. Instead of one impure function, we now have two functions, one of which is still impure.

Wait a bit. Imagine that we need to write a test for the getUserNameFromDOM() function. Now, comparing the two variants of this function, think about which of them will be easier to work with? In order for the impure version of the function to work at all, we need a global document object. Moreover, in this document there should be an element with the username identifier. If you need to test this feature outside the browser, then you will need to use something like JSDOM or a browser without a user interface. Please note that all this is needed only to test a small function with a length of several lines. And in order to test the second, clean version of this function, it is enough to do the following:

 const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`); 

This, of course, does not mean that testing such functions does not require integration tests performed in a real browser (or, at least, using something like JSDOM). But this example demonstrates a very important thing, which is that the getUserNameFromDOM() function has now become completely predictable. If we pass it the qsStub() function, it will always return mhatter . “Unpredictability” we moved to a small qs() function.

If necessary, we can bring unpredictable mechanisms to levels that are even more distant from the main function. As a result, we can bring them, conditionally speaking, to the “border areas” of the code. This will lead to the fact that we will have a thin shell of unclean code that surrounds a well-tested and predictable kernel. Predictability of code turns out to be an extremely valuable feature of it when the size of projects created by programmers grows.

â–Ť Disadvantages of dependency injection mechanism


Using dependency injection you can write a large and complex application. I know this because I wrote such an application myself. With this approach, testing is simplified, the dependencies of functions become clearly visible. But the introduction of dependencies is not without flaws. The main one is that when it is applied, very long function signatures can be obtained:

 function app(doc, con, ftch, store, config, ga, d, random) {   //     } app(document, console, fetch, store, config, ga, (new Date()), Math.random); 

In fact, this is not so bad. The disadvantages of such constructions manifest themselves in the event that some of the parameters need to be transferred to certain functions that are very deeply embedded in other functions. It looks like the need to pass parameters through many levels of function calls. When the number of such levels grows, it becomes annoying. For example, it may be necessary to transfer an object representing a date through 5 intermediate functions, despite the fact that none of the intermediate functions use this object. Although, of course, one cannot say that such a situation is something like a universal catastrophe. In addition, it gives the opportunity to clearly see the dependence of functions. True, be that as it may, it is still not so pleasant. Therefore, consider the following mechanism.

â–ŤLazy features


Let's look at the second loophole, which is used by adherents of functional programming. It consists in the following idea: a side effect is not a side effect until it actually happens. I know it sounds mysterious. In order to understand this, consider the following example:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

An example of this, perhaps, stupid, I know that. If we need the number 0, then in order for us to have it, it is enough to write it in the right place in the code. And I also know that you will not write JavaScript code to control nuclear weapons. But we need this code to illustrate the technology in question.

So, we have an example of an unclean function. It displays the data in the console and is still the cause of nuclear war. However, imagine that we need the zero that this function returns. Imagine a scenario in which we need to calculate something after a rocket launch. Let's say we may need to start a countdown timer or something like that. In this case, it is completely natural to think in advance about performing calculations. And we have to make sure that the rocket starts exactly when needed. We do not need to perform calculations in such a way that they could accidentally lead to the launch of this rocket. Therefore, we will think about what happens if we wrap the function fZero() into another function that simply returns it. Let's say it will be something like a security wrapper:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   return fZero; } 

You can call the returnZeroFunc() function as many times as you like. At the same time, until the implementation of what it returns, we are (theoretically) safe. In our case, this means that the execution of the following code will not lead to the beginning of a nuclear war:

 const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc(); //     . 

Now, a little more strictly than before, let's approach the definition of the term “pure function”. This will allow us to examine the function returnZeroFunc() in more detail. So, the function is pure under the following conditions:


returnZeroFunc() analyze the returnZeroFunc() function.

Does it have side effects? We have just found out that calling returnZeroFunc() does not returnZeroFunc() missiles. If you do not call what this function returns, nothing will happen. Therefore, we can conclude that this function has no side effects.

Is this feature referential transparent? That is, does it always return the same when passing the same input data to it? Let's check this by using the fact that in the above code snippet we called this function several times:

 zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true 

All this looks good, but the returnZeroFunc() function is not quite clean yet. It refers to a variable outside its own scope. To solve this problem, we rewrite the function:

 // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   function fZero() {       console.log('Launching nuclear missiles');       //              return 0;   }   return fZero; } 

Now the function can be recognized as clean. However, in this situation, the rules of JavaScript play against us. Namely, we can no longer use the === operator to check the referential transparency of a function. This happens because returnZeroFunc() will always return a new function reference. True, reference transparency can be checked by examining the code yourself. Such an analysis will show that each time the function is called, it returns a reference to the same function.

Before us is a small neat loophole. But can it be used in real projects? The answer to this question is positive. However, before we talk about how to use this in practice, we will develop our idea a little. Namely, back to the dangerous function fZero() :

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

We will try to use the zero that this function returns, but we will do it so that (so far) we do not start a nuclear war. To do this, create a function that takes the zero that the fZero() function returns and adds one to it:

 // fIncrement :: (() -> Number) -> Number function fIncrement(f) {   return f() + 1; } fIncrement(fZero); //      //   1 

That is bad luck ... We accidentally started a nuclear war. Let's try again, but this time we will not return a number. Instead, return a function that, someday, will return a number:

 // fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) {   return () => f() + 1; } fIncrement(zero); //   [Function] 

Now you can breathe easy. The catastrophe is prevented. We continue the study. Thanks to these two functions, we can create a whole bunch of “possible numbers”:

 const fOne   = fIncrement(zero); const fTwo   = fIncrement(one); const fThree = fIncrement(two); //   … 

In addition, we can create a set of functions whose names will begin with f (let's call them f*() functions), designed to work with “possible numbers”:

 // fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) {   return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) {   return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) {   return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); //    ,   . ! 

See what we did here? With “possible numbers” you can do the same thing as with ordinary numbers. Mathematicians call this isomorphism . A regular number can always be turned into a “possible number” by placing it in a function. You can get the "possible number" by calling the function. In other words, we have a mapping between ordinary numbers and “possible numbers.” It is, in fact, much more interesting than it might seem. Soon we will return to this idea.

The above technique using the wrapper function is a valid strategy. We can hide behind the functions as much as necessary. And, since we have not yet called any of these functions, all of them, theoretically, are pure. And no one starts a war. In the usual code (not related to missiles), we actually need side effects as a result. Wrapping everything we need in a function allows us to precisely control these effects. We ourselves choose the time of appearance of these effects.

It should be noted that it is not very convenient to use uniform constructions with heaps of brackets everywhere to declare functions. And creating new versions of each function is also not a pleasant thing. JavaScript has some nice built-in functions, like Math.sqrt() . It would be very nice if there was a way to use these ordinary functions with our “pending values”. Actually, we'll talk about it now.

Effect functor


Here we will talk about functors represented by objects containing our “deferred functions”. To represent the functor, we will use the Effect object. In such an object we put our function fZero() . But before doing so, we will make this function a bit safer:

 // zero :: () -> Number function fZero() {   console.log('Starting with nothing');   //  , ,     .   //       .   return 0; } 

Now we will describe the constructor function for creating objects of the Effect type:

 // Effect :: Function -> Effect function Effect(f) {   return {}; } 

There is nothing particularly interesting here, so let's work on this function. So, we want to use the usual fZero() function with an Effect object. To provide such a work scenario, we will write a method that takes a normal function and sometime applies it to our “pending value”. And we will do this without calling the Effect function. We call this function map() . It has such a name due to the fact that it creates a mapping between the normal function and the Effect function. It may look like this:

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       }   } } 

Now, if you closely monitor what is happening, you may have questions to the map() function. It looks suspiciously similar to the composition. We will return to this issue later, but for now let's try out what we have at the moment:

 const zero = Effect(fZero); const increment = x => x + 1; //   . const one = zero.map(increment); 

So ... Now we have no opportunity to observe what happened here. So let's modify the Effect in order, so to speak, to get the opportunity to “pull the trigger”:

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }   } } const zero = Effect(fZero); const increment = x => x + 1; //  . const one = zero.map(increment); one.runEffects(); //       //   1 

If needed, we can continue to call the map() function:

 const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero)   .map(increment)   .map(double)   .map(cube); eight.runEffects(); //       //   8 

Here, what is happening is already beginning to become more interesting. We call it a "functor." All this means that the Effect object has a map() function and it obeys some rules . However, these are not rules that prohibit anything. These rules are about what you can do. They are more like privileges. Since the Effect object is a functor, it obeys these rules. In particular, this is the so-called “composition rule”.

It looks like this:

If there is an Effect object with the name e , and two functions, f and g , then e.map(g).map(f) equivalent to e.map(x => f(g(x))) .

In other words, two map() methods executed in a row are equivalent to combining two functions. This means that an object of type Effect can perform actions similar to the following (remember one of the above examples):

 const incDoubleCube = x => cube(double(increment(x))); //       Ramda  lodash/fp      : // const incDoubleCube = compose(cube, double, increment); const eight = Effect(fZero).map(incDoubleCube); 

When we do what is shown here, we are guaranteed to get the same result that we would have if we used the variant of this code with a triple reference to map() . We can use this when refactoring code, and we can be sure that the code will work correctly. In some cases, by changing one approach to another, you can even achieve improved performance.

Now I propose to stop experimenting with numbers and talk about what looks more like the code used in real projects.

â–Ť Method of ()


The constructor of the Effect object takes, as an argument, a function. This is convenient since most of the side effects that we want to postpone are functions. For example, this is Math.random() and console.log() . However, sometimes you need to put in a Effect object some value that is not a function. For example, suppose we attached a certain object with configuration data to the global window object in the browser. , . , ( -, , , Haskell pure ):

 // of :: a -> Effect a Effect.of = function of(val) {   return Effect(() => val); } 

, , , -. , , . HTML- . , . . For example:

 window.myAppConf = {   selectors: {       'user-bio':     '.userbio',       'article-list': '#articles',       'user-name':    '.userfullname',   },   templates: {       'greet':  'Pleased to meet you, {name}',       'notify': 'You have {n} alerts',   } }; 

, Effect.of() , Effect :

 const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']); //   Effect('.userbio') 

â–Ť Effect


. , Effect . , getElementLocator() , Effect , . DOM, document.querySelector() — , . :

 // $ :: String -> Effect DOMElement function $(selector) {   return Effect.of(document.querySelector(s)); } 

, , map() :

 const userBio = userBioLocator.map($); //   Effect(Effect(<div>)) 

, , . div , map() , , . , innerHTML , :

 const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML)); //   Effect(Effect('<h2>User Biography</h2>')) 

, . userBio , . , , , . , , Effect('user-bio') . , , , :

 Effect(() => '.userbio'); 

— . :

 Effect(() => window.myAppConf.selectors['user-bio']); 

, map() , ( ). , , $ , :

 Effect(() => $(window.myAppConf.selectors['user-bio'])); 

, :

 Effect(   () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) ); 

Effect.of , :

 Effect(   () => Effect(       () => document.querySelector(window.myAppConf.selectors['user-bio'])   ) ); 

, , , . Effect .

â–Ť join()


? , Effect . , , .

Effect .runEffect() . . , - , , , , . , . join() . Effect , runEffect() , . , .

 // 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);       }   } } 

, :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .map($)   .join()   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

â–Ť chain()


, .map() , .join() , . , , . , , Effect . , .map() .join() . , , Effect :

 // 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();       }   } } 

chain() - , , Effect ( , ). HTML- :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .chain($)   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

-. . , flatMap . , , — , , join() . Haskell, , bind . , - , , chain , flatMap bind — .

â–Ť Effect


Effect , . . , DOM, , ? , , , . , . — .

 // 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   ); }); 

. :

 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}'); 

, . . ( name pattern ) Effect . tpl() , , Effect .
, map() Effect tpl() :

 pattern.map(tpl); //   Effect([Function]) 

, . map() :

 map :: Effect a ~> (a -> b) -> Effect b 

:

 tpl :: String -> Object -> String 

, map() pattern , ( , tpl() ) Effect .

 Effect (Object -> String) 

pattern Effect . . Effect , . ap() :

 // 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) {            //  -  ap,    ,   eff   (  ).           //    map  ,    eff       (  'g')           //   g,     f()           return eff.map(g => g(f()));       }   } } 

.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') 

, … , , .ap() . , , map() , ap() . , , .

. , . , , , Effect , ap() . , :

 // liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) {   return y.ap(x.map(f));   //      :   // return x.map(f).chain(g => y.map(g)); }); 

liftA2() , , . liftA3() :

 // 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))); }); 

, liftA2() liftA3() Effect . , , ap() .

liftA2() :

 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') 

?


, , , . ? , Effect ap() . , ? ?

: « , , ».

:


â–Ť


— . , , , . const pattern = window.myAppConfig.templates['greeting']; , , , :

 const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting')); 

— , , , , . . — , , . , , , , , , . , . — . , , , . , .

. .

â–Ť Effect


, , . - Facebook Gmail . ? .

, . . CSV- . . , , , . , . , . , , , .

, . , map() reduce() , . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .

, , . , . Nothing like? , , , . . , .

TensorFlow , .

TensorFlow, , . «». , , :

 node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2) 

Python, JavaScript. , Effect , add() , ( sess.run() ).

 print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) #  node3:  Tensor("Add_2:0", shape=(), dtype=float32) #  sess.run(node3):  7.0 

, (7.0) , sess.run() . , . , , , .

Results


, . , . Effect .
, , , , , . , , . Effect , , , . , .

— . , . , , . . . , .



Dear readers! ?

Source: https://habr.com/ru/post/422589/


All Articles