We all know that JavaScript can be used as an object oriented or functional programming language, but functional programming usually involves side effects, function composition, and currization. In fact, it does not. If you learn more, you will find that functional programming also includes many advanced features. Functor, Monad, etc. There’s a professor (Frisby) at Egghead who does a great job with functional programming based on JavaScript, The advanced topics related to functional programming such as box, Semigroup, Monoid, Functor, Applicative Functor, Monad and isomorphism are mainly introduced. The whole course is about 30 sections, this article is mainly the translation and summary of the course, have the energy to strongly recommend everyone to watch the original course Professor Frisby Introduces Composable Functional JavaScript. There’s a little hands-on project at the end of the course where you can get some hands-on experience with this different approach to programming. It is important to note that advanced features such as Monad introduced in this course may not be applicable to all projects, but they can be used to broaden your knowledge and help you learn pure functional programming such as Haskell

1. Use containers (BoxCreate a linear data stream

An ordinary function looks like this:

function nextCharForNumberString (str) {
  const trimmed = str.trim();
  const number = parseInt(trimmed);
  const nextNumber = number + 1;
  return String.fromCharCode(nextNumber);
}

const result = nextCharForNumberString('64');
console.log(result); // "A"
Copy the code

With Array, you can implement this:

const nextCharForNumberString = str= >
    [str]
    .map(s= > s.trim())
    .map(s= > parseInt(s))
    .map(i= > i + 1)
    .map(i= > String.fromCharCode(i));

const result = nextCharForNumberString('64');
console.log(result); // ["A"]
Copy the code

Here we put the data STR into a box (array), and then call the map method of the box multiple times to process the data inside the box. There is already something magical about this implementation. Here’s another implementation with the same basic idea, except instead of using arrays, we’ll implement the box ourselves:

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  toString: (a)= > `Box(${x}) `
});

const nextCharForNumberString = str= >
    Box(str)
    .map(s= > s.trim())
    .map(s= > parseInt(s))
    .map(i= > i + 1)
    .map(i= > String.fromCharCode(i));

const result = nextCharForNumberString('64');
console.log(String(result)); // "Box(A)"
Copy the code

So far we’ve implemented a box ourselves. Using maps continuously allows you to combine a set of operations to create a linear flow of data. Boxes can hold not only data, but also functions, and don’t forget that functions are first-class citizens:

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  toString: (a)= > `Box(${x}) `
});

const f0 = x= > x * 100; // think fo as a data
const add1 = f= > x => f(x) + 1; // think add1 as a function
const add2 = f= > x => f(x) + 2; // think add2 as a function
const g = Box(f0)
.map(f= > add1(f))
.map(f= > add2(f))
.fold(f= > f);

const res = g(1);
console.log(res); / / 103
Copy the code

Here, when you call map on a function container, you’re doing a combination of functions.

2. UseBoxRefactor imperative code

We use the same Box as in the previous section:

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  toString: (a)= > `Box(${x}) `
});
Copy the code

Imperative moneyToFloat:

const moneyToFloat = str= >
    parseFloat(str.replace(/\$/g.' '));
Copy the code

MoneyToFloat Box type:

const moneyToFloat = str= >
    Box(str)
    .map(s= > s.replace(/\$/g.' '))
    .fold(r= > parseFloat(r));
Copy the code

We’ve refactored moneyToFloat here using Box, which is good at turning nested expressions into maps one by one. It’s not very complicated, but it’s a good practice.

PercentToFloat:

const percentToFloat = str= > {
  const replaced = str.replace(/\%/g.' ');
  const number = parseFloat(replaced);
  return number * 0.01;
};
Copy the code

PercentToFloat Box type:

const percentToFloat = str= >
    Box(str)
    .map(str= > str.replace(/\%/g.' '))
    .map(replaced= > parseFloat(replaced))
    .fold(number= > number * 0.01);
Copy the code

We’re refactoring percentToFloat here using Box, and that’s a much cleaner implementation of the data flow.

Imperative applyDiscount:

const applyDiscount = (price, discount) = > {
  const cost = moneyToFloat(price);
  const savings = percentToFloat(discount);
  return cost - cost * savings;
};
Copy the code

Refactoring applyDiscount is a little trickier because it has two streams, but we can use closures:

ApplyDiscount Box type:

const applyDiscount = (price, discount) = >
    Box(price)
    .map(price= > moneyToFloat(price))
    .fold(cost= >
        Box(discount)
        .map(discount= > percentToFloat(discount))
        .fold(savings= > cost - cost * savings));
Copy the code

Now you can look at the output of this set of code:

const result = applyDiscount('$5.00'.'20%');

console.log(String(result)); / / "4"
Copy the code

If we don’t unbox (fold) in moneyToFloat and percentToFloat, then applyDiscount doesn’t need to be boxed (Box) before data conversion:

const moneyToFloat = str= >
    Box(str)
    .map(s= > s.replace(/\$/g.' '))
    .map(r= > parseFloat(r)); // here we don't fold the result out

const percentToFloat = str= >
    Box(str)
    .map(str= > str.replace(/\%/g.' '))
    .map(replaced= > parseFloat(replaced))
    .map(number= > number * 0.01); // here we don't fold the result out

const applyDiscount = (price, discount) = >
    moneyToFloat(price)
    .fold(cost= >
        percentToFloat(discount)
        .fold(savings= > cost - cost * savings));

const result = applyDiscount('$5.00'.'20%');

console.log(String(result)); / / "4"
Copy the code

Use 3.EitherBranch control

Either means Either Right or Left. Let’s implement Right first:

const Right = x= > ({
  map: f= > Right(f(x)),
  toString: (a)= > `Right(${x}) `
});

const result = Right(3).map(x= > x + 1).map(x= > x / 2);
console.log(String(result)); // "Right(2)"
Copy the code

Instead of implementing the Right fold, we’ll implement the Left fold first:

const Left = x= > ({
  map: f= > Left(x),
  toString: (a)= > `Left(${x}) `
});

const result = Left(3).map(x= > x + 1).map(x= > x / 2);
console.log(String(result)); // "Left(3)"
Copy the code

The Left container is different from the Right container because the Left completely ignores the passed data conversion function and leaves the internal data of the container as is. With Right and Left, we can branch the flow of application data. Containers are usually of the unknown type RightOrLeft because exceptions are common in programs.

Next we implement the fold method for Right and Left containers. If the unknown container is Right, we unpack it using the second function argument g:

const Right = x= > ({
  map: f= > Right(f(x)),
  fold: (f, g) = > g(x),
  toString: (a)= > `Right(${x}) `
});
Copy the code

If the unknown container is Left, the first function argument f is used for unpacking:

const Left = x= > ({
  map: f= > Left(x),
  fold: (f, g) = > f(x),
  toString: (a)= > `Left(${x}) `
});
Copy the code

Test the Right and Left fold methods:

const result = Right(2).map(x= > x + 1).map(x= > x / 2).fold(x= > 'error', x => x);
console.log(result); / / 1.5
Copy the code
const result = Left(2).map(x= > x + 1).map(x= > x / 2).fold(x= > 'error', x => x);
console.log(result); // 'error'
Copy the code

Either allows you to branch your program, such as exception handling, null checking, and so on.

Here’s an example:

const findColor = name= >
    ({red: '#ff4444'.blue: '#3b5998'.yellow: '#fff68f'})[name];

const result = findColor('red').slice(1).toUpperCase();
console.log(result); // "FF4444"
Copy the code

Here, if we pass green to the function findColor, we get an error. So you can use Either for error handling:

const findColor = name= > {
  const found = {red: '#ff4444'.blue: '#3b5998'.yellow: '#fff68f'}[name];
  return found ? Right(found) : Left(null);
};

const result = findColor('green')
            .map(c= > c.slice(1))
            .fold(e= > 'no color',
                 c => c.toUpperCase());
console.log(result); // "no color"
Copy the code

Further, we can refine a Either container for NULL detection and simplify the findColor code:

const fromNullable = x= >x ! =null ? Right(x) : Left(null); // [!=] will test both null and undefined

const findColor = name= >
    fromNullable({red: '#ff4444'.blue: '#3b5998'.yellow: '#fff68f'}[name]);
Copy the code

4. The use ofchainTo solveEitherThe nesting problem of

Json. If the location file fails to be read, a default port 3000 is provided. The command code is as follows:

const fs = require('fs');

const getPort = (a)= > {
  try {
    const str = fs.readFileSync('config.json');
    const config = JSON.parse(str);
    return config.port;
  } catch (e) {
    return 3000; }};const result = getPort();
console.log(result); // 8888 or 3000
Copy the code

We use Either to refactor:

const fs = require('fs');

const tryCatch = f= > {
  try {
    return Right(f());
  } catch (e) {
    returnLeft(e); }};const getPort = (a)= >
    tryCatch((a)= > fs.readFileSync('config.json'))
    .map(c= > JSON.parse(c))
    .fold(
        e= > 3000,
        obj => obj.port
    );

const result = getPort();
console.log(result); // 8888 or 3000
Copy the code

Is it perfect after refactoring? Parse. If the config. JSON file format is incorrect, the application will report an error:

SyntaxError: Unexpected end of JSON input

Therefore, we need to do exception handling for JSON parsing failures, and we can continue to use tryCatch to solve this problem:

const getPort = (a)= >
    tryCatch((a)= > fs.readFileSync('config.json'))
    .map(c= > tryCatch((a)= > JSON.parse(c)))
    .fold(
        left= > 3000.// The first tryCatch fails
        right => right.fold( // The first tryCatch succeeded
            e => 3000./ / JSON parse failure
            c => c.port
        )
    );
Copy the code

In this reconstruction, we used tryCatch twice, so the box was covered with two layers, and finally needed to be unpacked twice. To solve this problem, we can add a chain to the Right and Left methods:

const Right = x= > ({
  chain: f= > f(x),
  map: f= > Right(f(x)),
  fold: (f, g) = > g(x),
  toString: (a)= > `Right(${x}) `
});

const Left = x= > ({
  chain: f= > Left(x),
  map: f= > Left(x),
  fold: (f, g) = > f(x),
  toString: (a)= > `Left(${x}) `
});
Copy the code

When we use map and don’t want to add another layer of boxes after the data transformation, we should use chain:

const getPort = (a)= >
    tryCatch((a)= > fs.readFileSync('config.json'))
    .chain(c= > tryCatch((a)= > JSON.parse(c)))
    .fold(
        e= > 3000,
        c => c.port
    );
Copy the code

5. Imperative code usageEitherImplementation example

const openSite = (a)= > {
  if (current_user) {
      return renderPage(current_user);
    }
    else {
      returnshowLogin(); }};const openSite = (a)= >
    fromNullable(current_user)
    .fold(showLogin, renderPage);
Copy the code
const streetName = user= > {
  const address = user.address;
  if (address) {
    const street = address.street;
    if (street) {
      returnstreet.name; }}return 'no street';
};

const streetName = user= >
    fromNullable(user.address)
    .chain(a= > fromNullable(a.street))
    .map(s= > s.name)
    .fold(
        e= > 'no street',
        n => n
    );
Copy the code
const concatUniq = (x, ys) = > {
  const found = ys.filter(y= > y ===x)[0];
  return found ? ys : ys.concat(x);
};

const cancatUniq = (x, ys) = >
    fromNullable(ys.filter(y= > y ===x)[0])
    .fold(null= > ys.concat(x), y => ys);
Copy the code
const wrapExamples = example= > {
  if (example.previewPath) {
    try {
      example.preview = fs.readFileSync(example.previewPath);
    }
    catch (e) {}
  }
  return example;
};

const wrapExamples = example= >
    fromNullable(example.previewPath)
    .chain(path= > tryCatch((a)= > fs.readFileSync(path)))
    .fold(
        (a)= > example,
        preview => Object.assign({preview}, example)
    );
Copy the code

6. Semigroup

A semigroup is a type with a concat method that satisfies the associative law. For example Array and String:

const res = "a".concat("b").concat("c");
const res = [1.2].concat([3.4].concat([5.6])); // law of association
Copy the code

We define a Sum semigroup of Sum of type Sum:

const Sum = x= > ({
  x,
  concat: o= > Sum(x + o.x),
  toString: (a)= > `Sum(${x}) `
});

const res = Sum(1).concat(Sum(2));
console.log(String(res)); // "Sum(3)"
Copy the code

To continue with the custom All semigroup, the All type is used to cascade Boolean types:

const All = x= > ({
  x,
  concat: o= > All(x && o.x),
  toString: (a)= > `All(${x}) `
});

const res = All(true).concat(All(false));
console.log(String(res)); // "All(false)"
Copy the code

To continue defining First semigroups, the concat method of type First does not change its initial value:

const First = x= > ({
  x,
  concat: o= > First(x),
  toString: (a)= > `First(${x}) `
});

const res = First('blah').concat(First('ice cream'));
console.log(String(res)); // "First(blah)"
Copy the code

Examples of semigroups

This is a placeholder and we’ll fill it later.

const acct1 = Map({
  name: First('Nico'),
  isPaid: All(true),
  points: Sum(10),
  friends: ['Franklin']});const acct2 = Map({
  name: First('Nico'),
  isPaid: All(false),
  points: Sum(2),
  friends: ['Gatsby']});const res = acct1.concat(acct2);
console.log(res);
Copy the code

8. monoid

If a semigroup also has identity (identity), it is a monoid. Elements, when combined with other elements, do not change those elements and can be expressed as follows:

E dot a is a dot e is a

To upgrade the semigroup Sum to monoid, we simply need to implement an empty method and call the modified method to get the monoid’s identity:

const Sum = x= > ({
  x,
  concat: o= > Sum(x + o.x),
  toString: (a)= > `Sum(${x}) `
});

Sum.empty = (a)= > Sum(0);

const res = Sum.empty().concat(Sum(1).concat(Sum(2)));
// const res = Sum(1).concat(Sum(2)).concat(Sum.empty());
console.log(String(res)); // "Sum(3)"
Copy the code

We then proceed to implement the All upgrade as monoid:

const All = x= > ({
  x,
  concat: o= > All(x && o.x),
  toString: (a)= > `All(${x}) `
});

All.empty = (a)= > All(true);

const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"
Copy the code

If we tried to upgrade semigroup First to monoid, it would not work, such as First(‘hello’).concat(…). Is always hello, but first.empty ().concat(First(‘hello’)) is not necessarily hello, so we cannot upgrade semigroup First to monoid. This also shows that monoids must be semigroups, but semigroups need not be Monoids. Semigroups must satisfy associative laws. Monoids must not only satisfy associative laws, but also have identity.

9. Monoid, for example

Sum:

const Sum = x= > ({
  x,
  concat: o= > Sum(x + o.x),
  toString: (a)= > `Sum(${x}) `
});

Sum.empty = (a)= > Sum(0);
Copy the code

Product:

const Product = x= > ({
  x,
  concat: o= > Product(x * o.x),
  toString: (a)= > `Product(${x}) `
});

Product.empty = (a)= > Product(1);

const res = Product.empty().concat(Product(2)).concat(Product(3));
console.log(String(res)); // "Product(6)"
Copy the code

Any (returns true as long as one of them is true, false otherwise) :

const Any = x= > ({
  x,
  concat: o= > Any(x || o.x),
  toString: (a)= > `Any(${x}) `
});

Any.empty = (a)= > Any(false);

const res = Any.empty().concat(Any(false)).concat(Any(false));
console.log(String(res)); // "Any(false)"
Copy the code

All (returns true for All cases, false otherwise) :

const All = x= > ({
  x,
  concat: o= > All(x && o.x),
  toString: (a)= > `All(${x}) `
});

All.empty = (a)= > All(true);

const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"
Copy the code

Max:

const Max = x= > ({
  x,
  concat: o= > Max(x > o.x ? x : o.x),
  toString: (a)= > `Max(${x}) `
});

Max.empty = (a)= > Max(-Infinity);

const res = Max.empty().concat(Max(100)).concat(Max(200));
console.log(String(res)); // "Max(200)"
Copy the code

Min (minimize) :

const Min = x= > ({
  x,
  concat: o= > Min(x < o.x ? x : o.x),
  toString: (a)= > `Min(${x}) `
});

Min.empty = (a)= > Min(Infinity);

const res = Min.empty().concat(Min(100)).concat(Min(200));
console.log(String(res)); // "Min(100)"
Copy the code

Use 10.foldMapSum up the set

Suppose we need to Sum up a set of Sum, we can do this:

const res = [Sum(1), Sum(2), Sum(3)]
	.reduce((acc, x) = > acc.concat(x), Sum.empty());

console.log(res); // Sum(6)
Copy the code

Given the generality of this operation, we can draw a function fold. Install immutable and immutable using node. Immutable-ext provides a fold method:

const {Map, List} = require('immutable-ext');
const {Sum} = require('./monoid');

const res = List.of(Sum(1), Sum(2), Sum(3))
	.fold(Sum.empty());

console.log(res); // Sum(6)
Copy the code

You might think that a fold takes a function, because this is what a fold does in the previous sections, such as Box and Right:

Box(3).fold(x= > x); / / 3
Right(3).fold(e= > e, x => x); / / 3
Copy the code

Yeah, but the essence of a fold is unpacking. Before unboxing the Box and Right types, the value is taken out; Now unboxing the set is to get the sum of the set out. To aggregate multiple values from a collection into a single value, you pass in the initial value sum.empty (). So when you see a fold, you should think of it as being evaluated from a type, which could be either a single-value type (such as Box, Right) or a monoid collection.

Let’s move on to another set, Map:

const res = Map({brian: Sum(3), sara: Sum(5)})
	.fold(Sum.empty());

console.log(res); // Sum(8)
Copy the code

The Map here is a monoid set. If it is a common data set, the set can be converted into a Monoid set using the Map method of the set first:

const res = Map({brian: 3.sara: 5})
	.map(Sum)
	.fold(Sum.empty());

console.log(res); // Sum(8)
Copy the code
const res = List.of(1.2.3)
	.map(Sum)
	.fold(Sum.empty());

console.log(res); // Sum(6)
Copy the code

FoldMap: foldMap: foldMap: foldMap: foldMap: foldMap: foldMap: foldMap: foldMap

const res = List.of(1.2.3)
	.foldMap(Sum, Sum.empty());

console.log(res); // Sum(6)
Copy the code

11. UseLazyBoxDelay is evaluated

Let’s review the Box example:

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  toString: (a)= > `Box(${x}) `
});

const res = Box('64')
            .map(s= > s.trim())
            .map(s= > parseInt(s))
            .map(i= > i + 1)
            .map(i= > String.fromCharCode(i))
            .fold(x= > x.toLowerCase());

console.log(String(res)); // a
Copy the code

There’s a series of data conversions going on here, and finally a. Now we can define a LazyBox that delays the execution of the series of data conversion functions until the trigger is pulled:

const LazyBox = g= > ({
  map: f= > LazyBox((a)= > f(g())),
  fold: f= > f(g())
});

const res = LazyBox((a)= > '64')
			.map(s= > s.trim())
            .map(s= > parseInt(s))
            .map(i= > i + 1)
            .map(i= > String.fromCharCode(i))
            .fold(x= > x.toLowerCase());

console.log(res); // a
Copy the code

The argument to LazyBox is a function with an empty argument. A call to map on LazyBox does not immediately execute the incoming data conversion function. Each call to Map adds another function to the queue until the fold pulls the trigger, and all the previous data conversion functions are executed one after the other. This pattern helps to implement pure functions.

In 12.TaskCapture side effects in

This section still discusses Lazy, but based on the data.task library, which can be installed through NPM. Suppose we want to implement a function for launching rockets. If we implement it this way, then the function is clearly not a pure function:

const launchMissiles = (a)= >
	console.log('launch missiles! '); // Use console.log to simulate launching rockets
Copy the code

If you use data.task, you can use its Lazy feature to delay execution:

const Task = require('data.task');

const launchMissiles = (a)= >
	new Task((rej, res) = > {
      console.log('launch missiles! ');
      res('missile');
	});
Copy the code

Clearly launchMissiles are pure functions. We can continue to combine other logics based on it:

const app = launchMissiles().map(x= > x + '! ');

app
.map(x= > x + '! ')
.fork(
	e= > console.log('err', e),
  	x => console.log('success', x)
);

// launch missiles!
// success missile!!
Copy the code

Fork is the trigger that pulls the trigger, executing the previously defined Task and a set of data conversion functions. Without fork, the console.log operation in the Task will not execute.

13. The use ofTaskProcessing asynchronous Tasks

Suppose we want to read the file, replace the file, and then write the file, the command code is as follows:

const fs = require('fs');

const app = (a)= >
	fs.readFile('config.json'.'utf-8', (err, contents) => {
      if (err) throw err;
      const  newContents = contents.replace(/8/g.'6');
      fs.writeFile('config1.json', newContents,
      	(err, success) => {
        if (err) throw err;
        console.log('success'); })}); app();Copy the code

The app implemented here throws an exception internally, not a pure function. We can use Task to refactor as follows:

const Task = require('data.task');
const fs = require('fs');

const readFile = (filename, enc) = >
	new Task((rej, res) = >
    	fs.readFile(filename, enc, (err, contents) =>
        	err ? rej(err) : res(contents)));

const writeFile = (filename, contents) = >
	new Task((rej, res) = >
    	fs.writeFile(filename, contents, (err, success) =>
        	err ? rej(err) : res(success)));

const app = (a)= >
	readFile('config.json'.'utf-8')
	.map(contents= > contents.replace(/8/g.'6'))
	.chain(contents= > writeFile('config1.json', contents));

app().fork(
	e= > console.log(e),
  	x => console.log('success'));Copy the code

The app implemented here is a pure function that calls app().fork to perform a sequence of actions. Data. task reads two files in sequence:

const fs = require('fs');
const Task = require('data.task');

const readFile = path= >
    new Task((rej, res) = >
        fs.readFile(path, 'utf-8', (error, contents) =>
            error ? rej(error) : res(contents)));

const concatenated = readFile('Task_test_file1.txt')
                    .chain(a= >
                        readFile('Task_test_file2.txt')
                        .map(b= > a + b));

concatenated.fork(console.error, console.log);
Copy the code

14. Functor

Functor is a type that has a map method and needs to satisfy two conditions:

fx.map(f).map(g) == fx.map(x => g(f(x)))

fx.map(id) == id(fx), where const id = x => x

The Box type is used as an example.

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});

const res1 = Box('squirrels')
			.map(s= > s.substr(5))
			.map(s= > s.toUpperCase());
const res2 = Box('squirrels')
			.map(s= > s.substr(5).toUpperCase());
console.log(res1, res2); // Box(RELS) Box(RELS)
Copy the code

Obviously Box satisfies the first condition. Note that s = > s.substr(5).toupperCase () is essentially the same as g(f(x)).

const f = s= > s.substr(5);
const g = s= > s.toUpperCase();
const h = s= > g(f(s));

const res = Box('squirrels')
			.map(h);
console.log(res); // Box(RELS)
Copy the code

Let’s see if the second condition is met:

const id = x= > x;
const res1 = Box('crayons').map(id);
const res2 = id(Box('crayons'));
console.log(res1, res2); // Box(crayons) Box(crayons)
Copy the code

The second condition is obviously satisfied as well.

Use 15.ofMethod puts the value into the conservative Functor

A conservative functor is a functor with the method of, which can be understood as filling a functor with an initial value. Take Box as an example:

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});
Box.of = x= > Box(x);

const res = Box.of(100);
console.log(res); // Box(100)
Copy the code

Here’s another functor example, IO functor:

const R = require('ramda');

const IO = x= > ({
  x, // here x is a function
  map: f= > IO(R.compose(f, x)),
  fold: f= > f(x) // get out x
});

IO.of = x= > IO(x);
Copy the code

IO is a function container. If you’re careful, you’ll notice that this is the Box container whose value is function. With IO functor, we can handle some I/O operations purely functionally, because reads and writes seem to be queued up until a function inside the I/O is called. Try this:

const R = require('ramda');
const {IO} = require('./IO');

const fake_window = {
    innerWidth: '1000px'.location: {
        href: "http://www.baidu.com/cpd/fe"}};const io_window = IO((a)= > fake_window);

const getWindowInnerWidth = io_window
.map(window= > window.innerWidth)
.fold(x= > x);

const split = x= > s => s.split(x);

const getUrl = io_window
.map(R.prop('location'))
.map(R.prop('href'))
.map(split('/'))
.fold(x= > x);

console.log(getWindowInnerWidth()); // 1000px
console.log(getUrl()); // [ 'http:', '', 'www.baidu.com', 'cpd', 'fe' ]
Copy the code

16. Monad

Functor can apply a function to a wrapped value (” wrapped “means the value exists in a box, likewise) :

Box(1).map(x= > x + 1); // Box(2)
Copy the code

Applicative functor can apply an enclosed function to an enclosed value:

const add = x= > x + 1;
Box(add).ap(Box(1)); // Box(2)
Copy the code

Monod, on the other hand, can apply a function that returns the type of the box to a wrapped value without increasing the number of wrapping layers:

Let’s start with Boxfunctor:

const Box = x= > ({
  map: f= > Box(f(x)),
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});

const res = Box(1)
			.map(x= > Box(x))
			.map(x= > Box(x)); // Box(Box(Box(1)))
console.log(res); // Box([object Object])
Copy the code

Here we call map continuously and the return value of the function passed in when map is the Box type, obviously this will cause the Box packing layer to accumulate, we can add join method to Box to unpack:

const Box = x= > ({
  map: f= > Box(f(x)),
  join: (a)= > x,
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});

const res = Box(1)
			.map(x= > Box(x))
			.join()
			.map(x= > Box(x))
			.join();
console.log(res); // Box(1)
Copy the code

We define a JOIN here just to illustrate the unwrapping operation, but we could certainly use a fold to do the same:

const Box = x= > ({
  map: f= > Box(f(x)),
  join: (a)= > x,
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});

const res = Box(1)
			.map(x= > Box(x))
			.fold(x= > x)
			.map(x= > Box(x))
			.fold(x= > x);
console.log(res); // Box(1)
Copy the code

Considered. The map (…). As with.join(), we can add a chain method to Box to do both operations:

const Box = x= > ({
  map: f= > Box(f(x)),
  join: (a)= > x,
  chain: f= > Box(x).map(f).join(),
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});

const res = Box(1)
			.chain(x= > Box(x))
			.chain(x= > Box(x));
console.log(res); // Box(1)
Copy the code

17. Curry

This is a very simple one, a direct example, if you can understand these examples, you can understand the Currization:

const modulo = dvr= > dvd => dvd % dvr;

const isOdd = modulo(2); / / for odd number

const filter = pred= > xs => xs.filter(pred);

const getAllOdds = filter(isOdd);

const res1 = getAllOdds([1.2.3.4]);
console.log(res1); / / [1, 3]

const map = f= > xs => xs.map(f);

const add = x= > y => x + y;

const add1 = add(1);
const allAdd1 = map(add1);

const res2 = allAdd1([1.2.3]);
console.log(res2); / / [2, 3, 4]
Copy the code

18. Applicative Functor

Box is a functor, and we upgraded it to Applicative functor by adding an AP method:

const Box = x= > ({
  ap: b2= > b2.map(x), // here x is a function
  map: f= > Box(f(x)),
  fold: f= > f(x),
  inspect: (a)= > `Box(${x}) `
});

const res = Box(x= > x + 1).ap(Box(2));
console.log(res); // Box(3)
Copy the code

Here, inside Box is a unary function, we can also use the Currified multivariate function:

const add = x= > y => x + y;

const res = Box(add).ap(Box(2));
console.log(res); // Box([Function])
Copy the code

Applicative functor removes an argument with a single call to ap. Here res is still a function: y => 2 + y, but removes the argument x. We can call ap methods continuously:

const res = Box(add).ap(Box(2)).ap(Box(3));
console.log(res); // Box(5)
Copy the code

For applicative functor, we can find the following identity:

F(x).map(f) = F(f).ap(F(x))

That is, calling map(f) on a functor that holds the value x is equivalent to calling AP (f (x)) on a functor that holds the function f.

Then we implement a utility function liftA2 that handles applicative functor:

const liftA2 = (f, fx, fy) = >
	F(f).ap(fx).ap(fy);
Copy the code

But the specific functor type F needs to be known here, so by means of the previous identity, we proceed to define the following general form liftA2:

const liftA2 = (f, fx, fy) = >
	fx.map(f).ap(fy);
Copy the code

Try it on:

const res1 = Box(add).ap(Box(2)).ap(Box(4));
const res2 = liftA2(add, Box(2), Box(4)); // utilize helper function liftA2

console.log(res1); // Box(6)
console.log(res2); // Box(6)
Copy the code

We can also define similar liftA3 and liftA4 utility functions:

const liftA3 = (f, fx, fy, fz) = >
	fx.map(f).ap(fy).ap(fz);
Copy the code

19. Applicative Functor

First, define either:

const Right = x= > ({
  ap: e2= > e2.map(x), // declare as a applicative, here x is a function
  chain: f= > f(x), // declare as a monad
  map: f= > Right(f(x)),
  fold: (f, g) = > g(x),
  inspect: (a)= > `Right(${x}) `
});

const Left = x= > ({
  ap: e2= > e2.map(x), // declare as a applicative, here x is a function
  chain: f= > Left(x), // declare as a monad
  map: f= > Left(x),
  fold: (f, g) = > f(x),
  inspect: (a)= > `Left(${x}) `
});

const fromNullable = x= >x ! =null ? Right(x) : Left(null); // [!=] will test both null and undefined

const either = {
  	Right,
  	Left,
  	of: x= > Right(x),
  	fromNullable
};
Copy the code

Either is both monad and applicative functor.

Suppose we want to calculate the height of the page except header and footer:

const$=selector= >
	either.of({selector, height: 10}); // fake DOM selector

const getScreenSize = (screen, header, footer) = >
	screen - (header.height + footer.height);
Copy the code

If monod’s chain method is used, it can be implemented like this:

const res = $('header')
	.chain(header= >
    	$('footer').map(footer= >
        	getScreenSize(800, header, footer)));
console.log(res); // Right(780)
Copy the code

You can also use Applicative, but first you need to curryize getScreenSize:

const getScreenSize = screen= > header => footer= >
	screen - (header.height + footer.height);

const res1 = either.of(getScreenSize(800))
	.ap($('header'))
	.ap($('footer'));
const res2 = $('header')
	.map(getScreenSize(800))
	.ap($('footer'));
const res3 = liftA2(getScreenSize(800) and $('header') and $('footer'));
console.log(res1, res2, res3); // Right(780) Right(780) Right(780)
Copy the code

20. Applicative Functor之List

This section describes using Applicative functor to implement the following pattern:

for (x in xs) {
  for (y in ys) {
    for (z in zs) {
      // your code here}}}Copy the code

Use Applicative functor to refactor as follows:

const {List} = require('immutable-ext');

const merch = (a)= >
	List.of(x= > y => z= > `${x}-${y}-${z}`)
	.ap(List(['teeshirt'.'sweater']))
	.ap(List(['large'.'medium'.'small']))
	.ap(List(['black'.'white']));
const res = merch();
console.log(res);
Copy the code

21. Handle concurrent asynchronous events using Applicatives

Suppose we make two requests to read the database:

const Task = require('data.task');

const Db = ({
  find: id= >
  	new Task((rej, res) = >
    	setTimeOut((a)= > {
      		console.log(res);
        	res({id: id, title: `Project ${id}`})},5000))});const report = (p1, p2) = >
	`Report: ${p1.title} compared to ${p2.title}`;
Copy the code

If monad’s chain implementation is used, then two asynchronous events can only be executed sequentially:

Db.find(20).chain(p1= >
	Db.find(8).map(p2= >
    	report(p1, p2)))
	.fork(console.error, console.log);
Copy the code

Refactoring using Applicatives:

Task.of(p1= > p2 => report(p1, p2))
.ap(Db.find(20))
.ap(Db.find(8))
.fork(console.error, console.log);
Copy the code

22. [Task] => Task([])

Suppose we are going to read a set of files:

const fs = require('fs');
const Task = require('data.task');
const futurize = require('futurize').futurize(Task);
const {List} = require('immutable-ext');

const readFile = futurize(fs.readFile);

const files = ['box.js'.'config.json'];
const res = files.map(fn= > readFile(fn, 'utf-8'));
console.log(res);
// [ Task { fork: [Function], cleanup: [Function] },
// Task { fork: [Function], cleanup: [Function] } ]
Copy the code

Here res is an array of tasks, and we want something of the type Task([]), similar to promise.all(). We can use the traverse method to make tasks jump out of the array:

[Task] => Task([])

The implementation is as follows:

const files = List(['box.js'.'config.json']);
files.traverse(Task.of, fn => readFile(fn, 'utf-8'))
  .fork(console.error, console.log);
Copy the code

23. {Task} => Task({})

Suppose we are about to make a set of HTTP requests:

const fs = require('fs');
const Task = require('data.task');
const {List, Map} = require('immutable-ext');

const httpGet = (path, params) = >
	Task.of(`${path}: result`);

const res = Map({home: '/'.about: '/about'.blog: '/blod'})
.map(route= > httpGet(route, {}));
console.log(res);
// Map { "home": Task, "about": Task, "blog": Task }
Copy the code

Here res is a Map with a value of Task, and we want something of the type Task({}), similar to promise.all(). We can use the traverse method to make tasks jump out of the Map:

{Task} => Task({})

The implementation is as follows:

Map({home: '/'.about: '/about'.blog: '/blod'})
.traverse(Task.of, route => httpGet(route, {}))
.fork(console.error, console.log);
// Map { "home": "/: result", "about": "/about: result", "blog": "/blod: result" }
Copy the code

24. Type conversion

This section describes how one functor can be transformed into another. For example, convert either to Task:

const {Right, Left, fromNullable} = require('./either');
const Task = require('data.task');

const eitherToTask = e= >
	e.fold(Task.rejected, Task.of);

eitherToTask(Right('nightingale'))
.fork(
	e= > console.error('err', e),
  	r => console.log('res', r)
); // res nightingale

eitherToTask(Left('nightingale'))
.fork(
	e= > console.error('err', e),
  	r => console.log('res', r)
); // err nightingale
Copy the code

Convert Box to Either:

const {Right, Left, fromNullable} = require('./either');
const Box = require('./box');

const boxToEither = b= >
	b.fold(Right);

const res = boxToEither(Box(100));
console.log(res); // Right(100)
Copy the code

You may be wondering why boxToEither should be converted to Right and not Left, because the type conversions discussed in this section need to satisfy this condition:

nt(fx).map(f) == nt(fx.map(f))

Nt is the abbreviation of natural Transform, that is, natural type conversion. All functions that meet this formula are natural type conversion. Let’s see if boxToEither satisfies this formula if we convert it to Left:

const boxToEither = b= >
	b.fold(Left);

const res1 = boxToEither(Box(100)).map(x= > x * 2);
const res2 = boxToEither(Box(100).map(x= > x * 2));
console.log(res1, res2); // Left(100) Left(200)
Copy the code

Obviously, the above conditions are not satisfied.

Let’s look at the natural type conversion function first:

const first = xs= >
	fromNullable(xs[0]);

const res1 = first([1.2.3]).map(x= > x + 1);
const res2 = first([1.2.3].map(x= > x + 1));
console.log(res1, res2); // Right(2) Right(2)
Copy the code

The previous formula shows that for a functor, a natural cast followed by a map is equivalent to a map followed by a natural cast.

25. Example of type conversion

Take a look at a use case for First:

const {fromNullable} = require('./either');

const first = xs= >
	fromNullable(xs[0]);

const largeNumbers = xs= >
	xs.filter(x= > x > 100);

const res = first(largeNumbers([2.400.5.1000]).map(x= > x * 2));

console.log(res); // Right(800)
Copy the code

There is nothing wrong with this implementation, but instead of multiplying each value of large numbers by 2, we only need the first value, so using the natural type conversion formula we can change it to the following form:

const res = first(largeNumbers([2.400.5.1000])).map(x= > x * 2);

console.log(res); // Right(800)
Copy the code

Let’s look at a slightly more complicated example:

const {Right, Left} = require('./either');
const Task = require('data.task');

const fake = id= > ({
  id,
  name: 'user1'.best_friend_id: id + 1
}); // fake user infomation

const Db = ({
  find: id= >
  	new Task((rej, res) = >
    	res(id > 2 ? Right(fake(id)) : Left('not found')))});// fake database

const eitherToTask = e= >
	e.fold(Task.rejected, Task.of);
Copy the code

Here we simulate a database with some user information and assume that only users with ids greater than 2 can be found in the database.

Now we want to find information about a user’s best friends:

Db.find(3) // Task(Right(user))
.map(either= >
    either.map(user= > Db.find(user.best_friend_id))) // Task(Either(Task(Either)))
Copy the code

If we use chain here, let’s see what happens:

Db.find(3) // Task(Right(user))
.chain(either= >
	either.map(user= > Db.find(user.best_friend_id))) // Either(Task(Either))
Copy the code

There is also a problem with this call: the type of the container changes from Task to Either, which we don’t want Either. Let’s refactor this with a natural cast:

Db.find(3) // Task(Right(user))
.map(eitherToTask) // Task(Task(user))
Copy the code

To remove one layer of packaging, we use chain:

Db.find(3) // Task(Right(user))
.chain(eitherToTask) // Task(user)
.chain(user= >
	Db.find(user.best_friend_id)) // Task(Right(user))
.chain(eitherToTask)
.fork(
	console.error,
  	console.log
); // { id: 4, name: 'user1', best_friend_id: 5 }
Copy the code

26. Isomorphic rism

The isomorphism discussed here is not an isomorphism of “front-end isomorphism”, but a pair of functions that satisfy the following requirement:

from(to(x)) == x

to(from(y)) == y

If a pair of functions can be found that satisfy the above requirements, it means that one data type X has the same information or structure as another data type Y. In this case, we say that data type X and data type Y are isomorphic. For example, String and [char] are isomorphic:

const Iso = (to, from) = >({
  to,
  from
});

// String ~ [char]
const chars = Iso(s= > s.split(' '), arr => arr.join(' '));

const res1 = chars.from(chars.to('hello world'));
const res2 = chars.to(chars.from(['a'.'b'.'c']));
console.log(res1, res2); // hello world [ 'a', 'b', 'c' ]
Copy the code

How does that help? Let’s take an example:

const filterString = (str1, str2, pred) = >
  chars.from(chars.to(str1 + str2).filter(pred));

const res1 = filterString('hello'.'HELLO', x => x.match(/[aeiou]/ig));

console.log(res1); // eoEO

const toUpperCase = (arr1, arr2) = >
  chars.to(chars.from(arr1.concat(arr2)).toUpperCase());

const res2 = toUpperCase(['h'.'e'.'l'.'l'.'o'], ['w'.'o'.'r'.'l'.'d']);

console.log(res2); // [ 'H', 'E', 'L', 'L', 'O', 'W', 'O', 'R', 'L', 'D' ]
Copy the code

Here we use the Array filter method to filter the characters in the String; Use the toUpperCase method of String to handle the case conversion of character arrays. With isomorphism, we can convert and call methods between two different data types.

27. The actual combat

For practical examples of the last three sections of the course, see: practical.