Combating dirty side effects in pure, functional JavaScript code

Original author: James Sinclair
  • Transfer
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 -> StringfunctionlogSomething(something) {
    const dt = (newDate())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 function logSomething()has two problems that do not allow it to be recognized as clean: it creates an object Dateand 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 -> *functionlogSomething(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 = newDate();
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 an logobject method cnslwould result in an I / O statement. 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. Look at the features of the function logSomething(). 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 consoleor 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 :: () -> StringfunctiongetUserNameFromDOM() {
    returndocument.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, because it documentis a global object that can change at any time. One way to make such a function clean is to pass a global object documentas a parameter to it. However, it can still pass the function querySelector(). It looks like this:

// getUserNameFromDOM :: (String -> Element) -> StringfunctiongetUserNameFromDOM($) {
    return $('#username').value;
}
// qs :: String -> Elementconst 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, by this we do not get rid, only moving the reference to 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 a function getUserNameFromDOM(). 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 identifierusername. 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 of 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 function getUserNameFromDOM()has now become completely predictable. If we give it a function qsStub(), it will always return mhatter. "Unpredictability" we moved to a small function qs().

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:

functionapp(doc, con, ftch, store, config, ga, d, random) {
    // Тут находится код приложения
 }
app(document, console, fetch, store, config, ga, (newDate()), 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 :: () -> NumberfunctionfZero() {
    console.log('Launching nuclear missiles');
    // Тут будет код для запуска ядерных ракет
    return0;
}

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 functionfZero()to another function that just returns it. Let's say it will be something like a security wrapper:

// fZero :: () -> NumberfunctionfZero() {
    console.log('Launching nuclear missiles');
    // Тут будет код для запуска ядерных ракет
    return0;
}
// returnZeroFunc :: () -> (() -> Number)functionreturnZeroFunc() {
    return fZero;
}

You can call the function as many times as you like returnZeroFunc(). 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 explore the function in more detail returnZeroFunc(). So, the function is pure under the following conditions:

  • No observable side effects.
  • Reference transparency. That is, calling such a function with the same input values ​​always leads to the same results.

Let's analyze the function returnZeroFunc().

Does it have side effects? We just found out that the call returnZeroFunc()does not lead to the launch of rockets. 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 function returnZeroFunc()is not yet completely clean. It refers to a variable outside its own scope. To solve this problem, we rewrite the function:

// returnZeroFunc :: () -> (() -> Number)functionreturnZeroFunc() {
    functionfZero() {
        console.log('Launching nuclear missiles');
        // Тут будет код для запуска ядерных ракет
        return0;
    }
    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 the function. This happens due to the fact that returnZeroFunc()there will always be a new function reference returned. 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 :: () -> NumberfunctionfZero() {
    console.log('Launching nuclear missiles');
    // Тут будет код для запуска ядерных ракет
    return0;
}

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 function returns fZero()and adds one to it:

// fIncrement :: (() -> Number) -> NumberfunctionfIncrement(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)functionfIncrement(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 variety of functions whose names will begin with f(let's call them f*()functions), designed to work with "possible numbers":

// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number)functionfMultiply(a, b) {
    return() => a() * b();
}
// fPow :: (() -> Number) -> (() -> Number) -> (() -> Number)functionfPow(a, b) {
    return() =>Math.pow(a(), b());
}
// fSqrt :: (() -> Number) -> (() -> Number)functionfSqrt(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 great built-in functions, likeMath.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 object Effect. In such an object we put our function fZero(). But before doing so, we will make this function a bit safer:

// zero :: () -> NumberfunctionfZero() {
    console.log('Starting with nothing');
    // Тут мы, определённо, не будем запускать никаких ракет.
    // Но чистой эта функция пока не является.
    return0;
}

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

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

There is nothing particularly interesting here, so let's work on this function. So, we want to use a regular function fZero()with an object Effect. 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 function Effect. We call this function map(). It has this name because it creates a mapping between a normal function and a function Effect. It may look like this:

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

Now, if you closely monitor what is happening, you may have questions to the function map(). 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. Therefore, let's modify Effectin order, so to speak, to get the opportunity to “pull the trigger”:

// Effect :: Function -> EffectfunctionEffect(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 function map():

constdouble = 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 an object Effecthas a function map()and it obeys certain rules . However, these are not rules that prohibit anything. These rules are about what you can do. They are more like privileges. Since the object Effectis a functor, it obeys these rules. In particular, this is the so-called “composition rule”.

It looks like this:

If there is an object Effectwith the name e, and two functions, fand g, then it is e.map(g).map(f)equivalent e.map(x => f(g(x))).

In other words, two consecutive methods are map()equivalent to the composition of two functions. This means that an object of typeEffect can perform actions like the following (remember one of the examples above):

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 get if we use 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 object constructor Effecttakes, as an argument, a function. This is convenient since most of the side effects that we want to postpone are functions. For example, this Math.random()and console.log(). However, sometimes you need to put in the object Effectsome value that is not a function. For example, suppose we attached windowa certain object with configuration data to the global object in the browser. We need data from this object, but such an operation is not allowed in pure functions. In order to simplify the execution of such operations, we can write a small auxiliary method (in different languages ​​this method is called differently, for example, I don’t know why, it is called in Haskell pure):

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

In order to demonstrate a situation in which a similar method might be useful, let's imagine that we are working on a web application. This application has some standard features, for example, it can display a list of articles and user information. However, the location of these elements in the HTML code varies among different users of our application. Since we are accustomed to making thoughtful decisions, we decided to store information about the location of elements in the global configuration object. Thanks to this, we can always turn to them. 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',
    }
};

Now, thanks to an auxiliary method Effect.of(), we can easily put the value we need into a wrapper Effect:

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

▍Creating structures from nested Effect objects and expanding nested structures


Mapping functions with side effects can get us quite far. But sometimes we are engaged in mapping functions that return objects Effect. Let's say this is a function getElementLocator()that returns an object Effectcontaining a string. If we need to find a DOM element, then we need to call document.querySelector()another function that is not clean. You can clear it like this:

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

Now, if we need to combine all this, we can use the function map():

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

It’s a bit awkward to work with us. If we need to access the corresponding element div, we have to call map()with a function that also performs mapping, which ultimately gives the desired result. For example, if we need it innerHTML, the code will look like this:

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

Let's try to take apart what we have. Let's start with userBio, and from here we go further. It will be boring to analyze it, but we need it in order to properly understand what is happening here. Here, in the course of the descriptions, we will use the constructions of the form Effect('user-bio'). In order not to get confused in them, we must bear in mind that if you write down such constructions as a code, they will look something like this:

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

Although this is also not quite correct. Rather, it will look like this:

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

Now, when we apply the function map(), it turns out to be similar to the composition of the internal function and another function (we have already seen this above). As a result, for example, when we perform mapping with a function $, it looks like this:

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

If you open this expression, you get the following:

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

And if we reveal now Effect.of, then we will see a clearer picture of what is happening:

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

Note that all the code that performs the actual actions is in the most deeply nested function. EffectIt does not fall into an external object .

Join Join () method


Why do we deal with all this? We do this in order to deploy all these nested objects Effect. If we are going to do this, then we must make sure that we do not introduce undesirable side effects into this process.

The way to get rid of nested constructions when working with objects Effectis to call .runEffect()for an external function. However, this may seem confusing. We have already gone through a lot, due to the fact that we needed to ensure that the behavior of the system in which the code containing side effects is not executed. Now we will create another function that solves the same problem. Let's call her join(). We will use it to expand nested structures from objects Effect, and the functionrunEffect()We will use when we need to run the code with side effects. This clarifies our intent, even when the code we run remains the same.

// Effect :: Function -> EffectfunctionEffect(f) {
    return {
        map(g) {
            return Effect(x => g(f(x)));
        },
        runEffects(x) {
            return f(x);
        }
        join(x) {
            return f(x);
        }
    }
}

We can use this function to extract from the nested structure an element with information about the user:

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

ChainMethod chain ()


The above pattern, which uses a method call .map()followed by a method call .join(), is quite common. In fact, so often that for its implementation it would be convenient to create a separate auxiliary method. As a result, we can use this method whenever we have a function that returns an object Effect. Thanks to its use, we will not have to constantly use the structure consisting of a sequence of methods .map()and .join(). Here is how, with the addition of this function, the constructor of objects of the type will look like 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();
        }
    }
}

We called this new function chain()because it allows to combine operations performed on objects Effect(in fact, we also called it because the standard prescribes to call such a function exactly like this). Now our code for getting the internal HTML code of the block with information about the user will look like this:

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

Unfortunately, in other programming languages, these functions are called differently. Such a variety of names can be confusing when reading documentation. For example, the name is sometimes used flatMap. This name has a deep meaning, since here the usual mapping is performed first, and then - the disclosure of what happened with the help join(). In Haskell, however, the same mechanism has a confusing name bind. Therefore, if you read any materials on functional programming in different languages, keep in mind that chain, flatMapand bindare the naming options for similar mechanisms.

▍ Combination Effect Objects


Here is another scenario of working with objects Effect, the implementation of which may be somewhat inconvenient. It consists in combining two or more such objects using one function. For example, what if we need to get the username from the DOM, and then insert it into the template provided by the application's configuration object? For this, for example, we might have a function for working with templates, like the following. Please note that we create a curried version of the function. If you have not met with currying before, take a look at this material.

// tpl :: String -> Object -> Stringconst tpl = curry(functiontpl(pattern, data) {
    returnObject.keys(data).reduce(
        (str, key) => str.replace(newRegExp(`{${key}}`, data[key]),
        pattern
    );
});

So far, everything looks fine. Now let's 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}');

So, we have a function for working with templates. It takes a string and an object and returns a string. However, a string and an object ( nameand pattern) wrapped in an object Effect. We need to bring the function tpl()to a higher level, making it work with objects Effect.
Let's start with an analysis of what happens when an map()object's method is called Effectwith a function passed to this method tpl():

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

What is happening will become clearer if you look at the types. The type signature for a method map()looks like this:

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

Here is the signature of the template function:

tpl :: String -> Object -> String

It turns out that when we call the map()object method pattern, we get a partially applied function (remember that we curried the function tpl()) inside the object Effect.

Effect (Object -> String)

Now we want to transfer a value from a patterntype object Effect. However, we do not yet have the necessary mechanism to perform this action. Therefore, we will create a new object method Effectthat allows this. Call it ap():

// Effect :: Function -> EffectfunctionEffect(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()));
        }
    }
}

Now you can call .ap()to work with the template and get the final result:

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

We have reached the goal, but I have to admit something ... The fact is that I discovered that the method is .ap()sometimes a source of confusion. Namely, it is difficult to remember that you first need to use the method map(), and then call ap(). Further, you can forget about the order in which the parameters are applied.

However, these troubles can be dealt with. The fact is that usually when using this design, I try to raise ordinary functions to the level of applicatives. In other words, I have the usual functions, and I need them to be able to work with objects Effectthat have a method ap(). We can write a function that automates all of this:

// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c)const liftA2 = curry(functionliftA2(f, x, y){
    return y.ap(x.map(f));
    // Ещё можно было бы написать так:
    //  return x.map(f).chain(g => y.map(g));
});

This function is named liftA2()because it works with a function that takes two arguments. Similarly, you can write a function liftA3():

// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d)const liftA3 = curry(functionliftA3(f, a, b, c){
    return c.ap(b.ap(a.map(f)));
});

Note that in the functions liftA2()and the liftA3()object type Effectis not even mentioned. In theory, they can work with any objects that have a compatible method ap().

The above example using the function liftA2()can be rewritten as:

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

Why is this all about?


Now you may think that in order to avoid side effects, considerable effort is required. What does it change? You may have a feeling that the packing of entities in objects Effectand the romp with the method ap()involve doing too much complicated work. Why this all, if the usual code and so works quite normally? And do you really need something like that in the real world?

I will allow myself to quote here one famous quotation from John Hughes from this article: “A functional programmer looks like a medieval monk who rejects the pleasures of life in the hope that this will make him virtuous.”

Consider the above comments from two points of view:

  • Is functional programming really an important concept?
  • In what situations can something like a pattern implemented in an object Effectbe useful in the real world?

▍About the importance of functional code purity


Pure functions are important. If we consider a small function in isolation, then a certain proportion of structures in it that do not allow to call it clean do not really matter. Write something like const pattern = window.myAppConfig.templates['greeting'];faster and easier than writing, for example, like this:

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

And if this is all that you have ever done in programming, then the first option, and the truth, is simpler and better. Side effects in this case do not matter. However, this is just one line of code in an application that can contain thousands or even millions of lines. Functional cleanliness begins to take on much greater significance when you try to figure out why an application mysteriously, and, as it seems, for no reason at all, stops working. In such a situation, something unexpected happens, and you try to break the problem into parts and isolate its cause. The more code you can exclude from consideration, the better. If your functions are clean, this allows you to be sure that only what they are told affects their behavior. And this greatly narrows the range of possible causes of error,

In other words, it allows you to spend less effort on thinking. This is extremely important when debugging large and complex applications.

▍ Pattern Effect in the real world


So perhaps functional cleanliness is important if you are developing a large and complex application. It could be something like Facebookor Gmail. But what if you do not do such large-scale projects? Consider one very common scenario.

Let's say you have some data. And this data you have quite a lot. These can be millions of lines as text CSV files or huge database tables. You must process this data. Perhaps you are using them to train a neural network to build a model of inference. Perhaps you are trying to predict the next major movement in the cryptocurrency market. In fact, the goal of processing large amounts of data can be anything. The point here is that in order to achieve this goal, it is required to perform a huge amount of calculations.

Joel Spolsky convincingly proves that functional programming can help in solving such problems. For example, you can create alternative versions of methods map()andreduce()that support parallel data processing. This makes functional cleanliness possible. However, this is not all. Of course, using pure functions, you can write something interesting that performs parallel data processing. But there are only 4 cores on your system (or maybe 8, or 16 if you are lucky). Solving such problems, even using a multi-core processor, can still take a lot of time. But computations can be seriously accelerated by running them through multiple processors. Say, use a video card or some server cluster.

In order to make this possible, you first need to describe the calculations that you want to perform. But they need to be described, without performing a real run of such calculations. Nothing like? Ideally, after you create such a description, you pass it to a certain framework. This framework will then take care of reading the information from the repository and distributing the tasks among the data processing nodes. Then the same framework will collect the results and tell you what happened after performing the calculations.

It works so opensorsnaya library TensorFlow , designed to perform high-performance numerical calculations.

Using TensorFlow, do not use the usual data types of the programming language in which the application is written. Instead, create the so-called "tensors". Let's say if we need to add two numbers, it will look like this:

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

This code is written in Python, but it is not very different from JavaScript. And, which makes it related to the examples of using the class discussed above Effect, the code in add()will not be executed until we explicitly ask the system to do it (in this case, this is done using a construct 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

As you can see, we will not get the result (7.0) until we call sess.run(). It is easy to see that this is very similar to our deferred functions. Here we also plan in advance the actions of the system, and then, when everything is ready, we start the process of calculations.

Results


In this article, we looked at many issues related to functional programming. In particular, we have dismantled two ways to maintain the functional purity of the code. This is dependency injection and the use of a functor Effect.
The dependency injection mechanism works due to the removal of what violates the purity of the code, beyond the limits of this code, that is, beyond the limits of functions that must be clean. What is taken out of them, you need to pass them in the form of parameters. The functor Effect, on the other hand, is concerned with wrapping everything that is related to a function. In order to run the appropriate code, the programmer needs to make a deliberate decision.

Both approaches are a sort of scam. Both of them do not allow to get rid of constructions that violate the functional purity of the code. They only endure similar constructions, so to speak, to the periphery. But it is quite useful. This allows you to clearly separate the code into clean and unclean. The possibility of such separation can give a programmer real advantages in situations where he has to debug large and complex applications. Dear readers! Do you use functional programming concepts in your projects?




Also popular now: