// logSomething :: String -> String function logSomething(something) { const dt = (new Date())toISOString(); console.log(`${dt}: ${something}`); return something; }
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. // logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) { const dt = d.toIsoString(); return cnsl.log(`${dt}: ${something}`); }
const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something); // "Curiouser and curiouser!"
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.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!"
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. // getUserNameFromDOM :: () -> String function getUserNameFromDOM() { return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; // "mhatter"
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"
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.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}`);
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. function app(doc, con, ftch, store, config, ga, d, random) { // } app(document, console, fetch, store, config, ga, (new Date()), Math.random);
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
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; }
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(); // .
returnZeroFunc()
in more detail. So, the function is pure under the following conditions:returnZeroFunc()
analyze the returnZeroFunc()
function.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. zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true
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; }
===
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.fZero()
: // fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
fZero()
function returns and adds one to it: // fIncrement :: (() -> Number) -> Number function fIncrement(f) { return f() + 1; } fIncrement(fZero); // // 1
// fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) { return () => f() + 1; } fIncrement(zero); // [Function]
const fOne = fIncrement(zero); const fTwo = fIncrement(one); const fThree = fIncrement(two); // …
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); // , . !
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
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; }
Effect
type: // Effect :: Function -> Effect function Effect(f) { return {}; }
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))); } } }
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);
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
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
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”.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)))
.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);
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.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); }
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
. , 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
.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>')
.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
, . . , 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()
. , ? ?Effect
, ?const pattern = window.myAppConfig.templates['greeting'];
, , , : const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting'));
Facebook
Gmail
. ? .map()
reduce()
, . . , . , , , . 4 (, , 8, 16, ). , , . , . , - . node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2)
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
sess.run()
. , . , , , .Effect
.Effect
, , , . , .Source: https://habr.com/ru/post/422589/
All Articles