Slide the address

B: Talk is cheap! Show me the … MONEY!

The following content mainly refers to the Professor Frisby Introduces Composable Functional JavaScript

4.1. Box

Suppose you have a function that accepts a numeric string from user input. We need to preprocess it, remove excess whitespace, convert it to a number and increment it by one, and return the corresponding letter of the value. The code looks something like this…

const nextCharForNumStr = (str) = >
  String.fromCharCode(parseInt(str.trim()) + 1)

nextCharForNumStr('64') // "A"
Copy the code

Because of the lack of thinking hall, this code nested is too compact, see many “old broad pain”, hurriedly reconstruct a…

const nextCharForNumStr = (str) = > {
  const trimmed = str.trim()
  const number = parseInt(trimmed)
  const nextNumber = number + 1
  return String.fromCharCode(nextNumber)
}

nextCharForNumStr('64') // 'A'
Copy the code

It is clear that this revised code is not Pointfree at a glance after the previous content has been fumbled…

In order to use only one of these intermediate variables to think about or to look up the translation, is also easy to “old broad pain”, change again change ~

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

nextCharForNumStr('64') // ['A']
Copy the code

This time, using the array map method, we split the necessary four steps into four small functions.

This way you never have to think about the names of the intermediate variables, and everything you do at each step is so clear that you can see at a glance what the code is doing.

We put the original string variable STR in an array into [STR], as if in a container.

Does the code feel good door~~?

But let’s take this one step further and create a new type, Box. We’ll define the map method to do the same.

const Box = (x) = > ({
  map: f= > Box(f(x)),        // Return the container for the chain call
  fold: f= > f(x),            // Take the element out of the container
  inspect: (a)= > `Box(${x}) `.// See what's in the container
})

const nextCharForNumStr = (str) = > Box(str)
  .map(s= > s.trim())
  .map(i= > parseInt(i))
  .map(i= > i + 1)
  .map(i= > String.fromCharCode(i))
  .fold(c= > c.toLowerCase()) // You can easily continue calling new functions

nextCharForNumStr('64') // a
Copy the code

To create a container, you can also use the static method of, in addition to passing arguments directly like a function.

As a general convention of functional programming, functors have an of method that generates new containers.

Box(1) === Box.of(1)
Copy the code

The Box is a functor because it implements the map function. You can call it Mappable or whatever.

But in keeping with the categorical definition of names, let’s stand on the shoulders of giants and stop inventing new ones.

Functor is a container type that implements a map function and follows certain rules.

So what are those specific rules?

  1. Rule one:
fx.map(f).map(g) === fx.map(x= > g(f(x)))
Copy the code

This is really just a combination of functions…

  1. Rule 2:
const id = x= > x

fx.map(id) === id(fx)
Copy the code

4.2. Either/Maybe

Suppose you now have a requirement to get the hexadecimal RGB value for the color and return the uppercase value with # removed.

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

const redColor = findColor('red')
  .slice(1)
  .toUpperCase() // FF4444

const greenColor = findColor('green')
  .slice(1)
  .toUpperCase()
// Uncaught TypeError:
// Cannot read property 'slice' of undefined
Copy the code

The above code works fine when you enter a key value for an existing color, but will report an error once you pass in another color. Do how?

Never mind the conditional judgment and the clever error handling. Let’s look at the functional solution first

The function abstracts error handling into Either, which consists of two child containers, Right and Left.

// Either consists of Right and Left

const Left = (x) = > ({
  map: f= > Left(x),            // Ignore the f function passed in
  fold: (f, g) = > f(x),         // Use the function on the left
  inspect: (a)= > `Left(${x}) `.// See what's in the container
})

const Right = (x) = > ({
  map: f= > Right(f(x)),        // Return the container for the chain call
  fold: (f, g) = > g(x),         // Use the function on the right
  inspect: (a)= > `Right(${x}) `.// See what's in the container
})

// Let's test ~
const right = Right(4)
  .map(x= > x * 7 + 1)
  .map(x= > x / 2)

right.inspect() / / Right (14.5)
right.fold(e= > 'error', x => x) / / 14.5

const left = Left(4)
  .map(x= > x * 7 + 1)
  .map(x= > x / 2)

left.inspect() // Left(4)
left.fold(e= > 'error', x => x) // error
Copy the code

Right and Left are similar to Box:

  • The big difference is thatfoldFunction, here we need to pass two callbacks, the one on the leftLeftUse the one on the rightRightUse.
  • The second isLeftmapThe function ignores the function passed in.

Now let’s go back to our previous question

const fromNullable = (x) = > x == null
  ? Left(null)
  : Right(x)

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

findColor('green')
  .map(c= > c.slice(1))
  .fold(
    e= > 'no color',
    c => c.toUpperCase()
  ) // no color
Copy the code

The advantage of using Either is that you can do anything with this type of data without having to carefully check parameters in each function.

4.3.Chain / FlatMap / bind / > > =

Suppose we now have a JSON file with a port in it, and we need to read the file to get the port, and return the default value 3000 if there is an error.

// config.json
{ "port": 8888 }

// chain.js
const fs = require('fs')

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

So easy, let’s use Either to reconstruct it.

const fs = require('fs')

const Left = (x) = >({... })const Right = (x) = >({... })const tryCatch = (f) = > {
  try {
    return Right(f())
  } catch (e) {
    return Left(e)
  }
}

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

Ah, routine operation, looks good

Good for you, snake head… !

Parse json. parse will fail when the JSON file is written incorrectly, so this step should also be wrapped with tryCatch.

But here’s the thing…

The return value might be Right(Right(“)) or Right(Left(e)) (think why not Left(Right(“)) or Left(Left(e))).

So what we have now is a two-tier container, like a Russian nesting doll…

To fetch the value of a container within a container, we need to fold twice… ! (A few more layers…)

I know I’m going to return a container, so I don’t want to use a container.

. const Left =(x) = >({... chain:f= > Left(x) As with map, return Left directly
})

const Right = (x) = >({... chain:f= > f(x),   // Return a layer without using the container
})

const tryCatch = (f) = >{... }const getPort = (a)= > tryCatch(
    (a)= > fs.readFileSync('config.json')
  )
  .chain(c= > tryCatch((a)= > JSON.parse(c))) // Use chain and tryCatch
  .fold(
    e= > 3000,
    c => c.port
  )
Copy the code

In this case, the Left and Right are monads because they implement the chain function.

Monad is a container type that implements the chain function and follows certain rules.

Before continuing with these specific rules, let’s define a join function:

// m refers to an instance of Monad
const join = m= > m.chain(x= > x)
Copy the code
  1. Rule one:
join(m.map(join)) === join(join(m))
Copy the code
  1. Rule 2:
// M refers to a Monad type
join(M.of(m)) === join(m.map(M.of))
Copy the code

This rule states that a map can be defined by chain and of.

m.map(f) === m.chain(x= > M.of(f(x)))
Copy the code

That is to say,MonadIt must beFunctor

Monad is very powerful, and we will use it later to deal with various side effects. But don’t get confused. The main purpose of a chain is to join two different types together.

4.4. Semigroup

Definition 1: For the non-empty set S, if the binary operation ○ is defined on S such that for any A, b ∈ S, there is a ○ B ∈ S, then {S, ○} is called a generalized group.

Definition 2: If {S, ○} is a generalized group and the operation ○ satisfies the associative law, that is, any A, b, c ∈ S with (a ○ b) ○ c = a ○ (b ○ C), {S, ○} is called a semigroup.

For example, objects in JavaScript that have concat methods are semigroups.

Strings and concat are semigroups
'1'.concat('2').concat('3') = = ='1'.concat('2'.concat('3'))

Arrays and concat are semigroups
[1].concat([2]).concat([3= = = [])1].concat([2].concat([3]))
Copy the code

Although in theory it fits the definition of a semigroup for

:
,>

  • Adding numbers still returns numbers (broad group)
  • Addition satisfies the associative law (semigroup)

But there is no concat method for numbers

Ok, let’s implement the semigroup Sum of

.
,>

const Sum = (x) = > ({
  x,
  concat: ({ x: y }) = > Sum(x + y), // Use deconstruction to get values
  inspect: (a)= > `Sum(${x}) `,
})

Sum(1)
  .concat(Sum(2))
  .inspect() // Sum(3)
Copy the code

In addition,

also satisfies the definition of a semigroup ~
,>

const All = (x) = > ({
  x,
  concat: ({ x: y }) = > All(x && y), // Use deconstruction to get values
  inspect: (a)= > `All(${x}) `,
})

All(true)
  .concat(All(false))
  .inspect() // All(false)
Copy the code

Finally, let’s create a new semigroup for strings, First, which, as the name implies, ignores everything except the First argument.

const First = (x) = > ({
  x,
  concat: (a)= > First(x), // Ignore subsequent values
  inspect: (a)= > `First(${x}) `,
})

First('blah')
  .concat(First('yoyoyo'))
  .inspect() // First('blah')
Copy the code

Yi ah hey? Does it feel like this semigroup is a little different from the other semigroups, but I don’t know what it is… ?

I’ll leave that to the next section. Let me tell you what this thing is for.

const data1 = {
  name: 'steve'.isPaid: true.points: 10.friends: ['jame'],}const data2 = {
  name: 'steve'.isPaid: false.points: 2.friends: ['young'],}Copy the code

We can apply First to name, All to isPaid, Sum to points, and Sum to friends.

const Sum = (x) = >({... })const All = (x) = >({... })const First = (x) = >({... })const data1 = {
  name: First('steve'),
  isPaid: All(true),
  points: Sum(10),
  friends: ['jame'],}const data2 = {
  name: First('steve'),
  isPaid: All(false),
  points: Sum(2),
  friends: ['young'],}const concatObj = (obj1, obj2) = > Object.entries(obj1)
  .map(([ key, val ]) = > ({
    // concat Specifies the values of two objects
    [key]: val.concat(obj2[key]),
  }))
  .reduce((acc, cur) = >({... acc, ... cur })) concatObj(data1, data2)/* { name: First('steve'), isPaid: All(false), points: Sum(12), friends: ['jame', 'young'], } */
Copy the code

4.5. Monoid

Monoid is a semigroup with identity element (monoid).

We all know about semigroups, but what is the identity element?

Identity element: For semigroups <S, ○>, there exists e ∈ S such that any A ∈ S has A ○ E = e ○ A

For example, for the semigroup of Number addition, 0 is the identity element, so

constitutes a monoid. In the same way:
,>

  • for<Number, *>Let’s say the identity element is 1
  • for<Boolean, &&>Let’s say the identity element is true
  • for<Boolean, ||>The identity element is false
  • for<Number, Min>So the unit is Infinity
  • for<Number, Max>So the unit is minus Infinity

then<String, First>Is it monoid?

Obviously, we can’t find an identity element that satisfies e

First(e).concat(First('steve')) === First('steve').concat(First(e))

This is the reason why First is different from Sum and All.

Gee-gee-gee, is there any specific difference between the two?

In fact, the first response to monoids should be a default value or an initial value. For example, the second argument to reduce is passed an initial value or a default value.

// sum
const Sum = (x) = >({... }) Sum.empty =(a)= > Sum(0) Yuan / / unit

const sum = xs= > xs.reduce((acc, cur) = > acc + cur, 0)

sum([1.2.3])  / / 6
sum([])         // 0 instead of reporting an error!

// all
const All = (x) = >({... }) All.empty =(a)= > All(true) Yuan / / unit

const all = xs= > xs.reduce((acc, cur) = > acc && cur, true)

all([true.false.true]) // false
all([])                  // true instead of reporting an error!

// first
const First = (x) = >({... })const first = xs= > xs.reduce(acc, cur) => acc)

first(['steve'.'jame'.'young']) // steve
first([])                         // boom!!!
Copy the code

As you can see from the above code, monids are much more secure than semigroups,

4.6. FoldMap

1. The routines

In the previous section, if you pass in an instance of a monoid instead of a primitive type, you’ll see that it’s all the same thing…

const Monoid = (x) = >({... })const monoid = xs= > xs.reduce(
    (acc, cur) = > acc.concat(cur),  // Use concat to combine
    Monoid.empty()                  // Pass in the identity
)

monoid([Monoid(a), Monoid(b), Monoid(c)]) // Pass in an instance of a monoid
Copy the code

So for highly abstract functional thinking, such code will definitely need to be refactored and streamlined

2. The List, the Map

Before explaining how to refactor, let’s introduce two immutable data structures commonly used for stir-frying: List and Map.

As the name implies, this corresponds to the native Array and Object.

3. Use the List and Map to reconstruct data

Since lists and maps in the IMmutable library do not have empty attributes and fold methods, we extend List and Map~ first

import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.reduce((acc, cur) = > acc.concat(cur), empty)
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold

// from https://github.com/DrBoolean/immutable-ext
Copy the code

This simplifies the code from the previous section to this:

List.of(1.2.3)
  .map(Sum)
  .fold(Sum.empty())     // Sum(6)

List().fold(Sum.empty()) // Sum(0)

Map({ steve: 1.young: 3 })
  .map(Sum)
  .fold(Sum.empty())     // Sum(4)

Map().fold(Sum.empty())  // Sum(0)
Copy the code

4. Use foldMap for reconstruction

Note that the map and fold operations are logically one operation, so we can add a foldMap method to combine the two.

import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.foldMap(x= > x, empty)
  },
  foldMap (f, empty) {
    returnempty ! =null
      // Monoid puts calls to f in reduce to improve efficiency
      ? this.reduce(
          (acc, cur, idx) = >
            acc.concat(f(cur, idx)),
          empty
      )
      : this
        // F is called in map because it is empty
        .map(f)
        .reduce((acc, cur) = > acc.concat(cur))
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold
List.prototype.foldMap = derived.foldMap

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold
Map.prototype.foldMap = derived.foldMap

// from https://github.com/DrBoolean/immutable-ext
Copy the code

So the final version looks like this:

List.of(1.2.3)
  .foldMap(Sum, Sum.empty()) // Sum(6)
List()
  .foldMap(Sum, Sum.empty()) // Sum(0)

Map({ a: 1.b: 3 })
  .foldMap(Sum, Sum.empty()) // Sum(4)
Map()
  .foldMap(Sum, Sum.empty()) // Sum(0)
Copy the code

4.7. LazyBox

Now we’re going to implement a new container, LazyBox.

As the name suggests, the container is lazy…

You can keep assigning tasks to it with a map, but as long as you don’t call the fold method to urge it to execute (like deadline), it won’t execute…

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

const result = LazyBox((a)= > '64')
  .map(s= > s.trim())
  .map(i= > parseInt(i))
  .map(i= > i + 1)
  .map(i= > String.fromCharCode(i))
  // No fold is executed

result.fold(c= > c.toLowerCase()) // a
Copy the code

4.8. The Task

1. Basic introduction

With the basics of LazyBox in the previous section, let’s create a new type of Task.

First, the constructor of a Task can take a function to delay the computation, and of course it can create instances using the of method, as well as map, chain, concat, empty, and so on.

The difference is that it has a fork method (similar to the fold method in LazyBox, which is not executed until fork) and a Rejected method, similar to Left, which ignores subsequent operations.

import Task from 'data.task'

const showErr = e= > console.log(`err: ${e}`)
const showSuc = x= > console.log(`suc: ${x}`)

Task
  .of(1)
  .fork(showErr, showSuc) // suc: 1

Task
  .of(1)
  .map(x= > x + 1)
  .fork(showErr, showSuc) // suc: 2

/ / similar to the Left
Task
  .rejected(1)
  .map(x= > x + 1)
  .fork(showErr, showSuc) // err: 1

Task
  .of(1)
  .chain(x= > new Task.of(x + 1))
  .fork(showErr, showSuc) // suc: 2
Copy the code

2. Example

Next let’s do a program to launch missiles ~

const lauchMissiles = (a)= > (
  // Much like Promises, except that promises are implemented immediately
  // And the arguments are in the opposite position
  new Task((rej, res) = > {
    console.log('lauchMissiles')
    res('missile')}))// Continue to add subsequent actions to the previous mission (duang~ add special effects to missiles!)
const app = lauchMissiles()
  .map(x= > x + '! ')

// Then execute (launch missile)
app.fork(showErr, showSuc)
Copy the code

3. Theoretical significance

At first glance, the above code appears to be useless, but it is just a function wrapped around the code to be executed.

Remember the side effects from the previous chapter? While there are no side effects to using pure functions, there are all kinds of side effects that you must deal with in everyday projects.

So by wrapping up the code that has side effects, these new functions become pure functions, so that our entire application code is pure, and we can compose other functions continuously until the code is forked, adding various functions to our application. This way the entire application code flow will be very simple and beautiful.

4. Example of asynchronous nesting

The following code does three things:

  1. Read the data in config1.json
  2. Replace the 8 with the 6 in the content
  3. Write the new content to config2.json
import fs from 'fs'

const app = (a)= > (
  fs.readFile('config1.json'.'utf-8', (err, contents) => {
    if (err) throw err

    const newContents = content.replace(/8/g.'6')

    fs.writeFile('config2.json', newContents, (err, _) => {
      if (err) throw err

      console.log('success! ')})}))Copy the code

Let’s rewrite ~ as Task

import fs from 'fs'
import Task from 'data.task'

const cfg1 = 'config1.json'
const cfg2 = 'config2.json'

const readFile = (file, enc) = > (
  new Task((rej, res) = >
    fs.readFile(file, enc, (err, str) =>
      err ? rej(err) : res(str)
    )
  )
)

const writeFile = (file, str) = > (
  new Task((rej, res) = >
    fs.writeFile(file, str, (err, suc) =>
      err ? rej(err) : res(suc)
    )
  )
)

const app = readFile(cfg1, 'utf-8')
  .map(str= > str.replace(/8/g.'6'))
  .chain(str= > writeFile(cfg2, str))

app.fork(
  e= > console.log(`err: ${e}`),
  x => console.log(`suc: ${x}`))Copy the code

The code is self-explanatory, the tasks are done in a linear order, and requirements can be inserted or modified at will

4.9. Applicative Functor

1. Introduction of problems

Applicative Functor provides the ability for different functors to be applied to each other.

Why do we need mutual applications of functors? What are mutual applications?

Let’s start with a simple example:

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

add(Box.of(2))(Box.of(3)) // NaN

Box(2).map(add).inspect() // Box(y => 2 + y)
Copy the code

We now have a container whose internal values are the functions after partially applied. Then we want to apply it to Box(3), and finally get the desired result for Box(5).

When it comes to evaluating from a container, the first thing that comes to mind is the chain method, so let’s try it:

Box(2)
  .chain(x= > Box(3).map(add(x)))
  .inspect() // Box(5)
Copy the code

The problem with successful implementation of ~ BUT is the order in which monads are executed.

If we do this, we have to wait for Box(2) to complete before we can evaluate Box(3). If these are two asynchronous tasks, they cannot be executed in parallel at all.

Don’t panic. Take some medicine

2. Basic introduction

Here are the main characters: ap~ :

const Box = (x) = > ({
  // where box is another instance of box, x is the function
  ap: box= > box.map(x),
  ...
})

Box(add)
  // Box(y => 2 + y) Where have you seen it?
  .ap(Box(2))
  .ap(Box(3)) // Box(5)
Copy the code

algorithm

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

// That's why
Box(2).map(add) === Box(add).ap(Box(2))
Copy the code

3. Lift the family

Because there is too much template code to write code with ap directly, this is generally simplified by using the Lift family of functions.

// Where does F come from?
const fakeLiftA2 = f= > fx => fy= > F(f).ap(fx).ap(fy)

// Apply the operation rule to convert ~
const liftA2 = f= > fx => fy= > fx.map(f).ap(fy)

liftA2(add, Box(2), Box(4)) // Box(6)

/ / in the same way
const liftA3 = f= > fx => fy= > fz => fx.map(f).ap(fy).ap(fz)
constliftA4 = ... . const liftAN = ...Copy the code

4. Lift applications

  • Case 1
// Pretend to be a jQuery interface
const$=selector= >
  Either.of({ selector, height: 10 })

const getScreenSize = screen= > head => foot= >
  screen - (head.height + foot.height)

liftA2(getScreenSize(800($())'header'($())'footer')) // Right(780)
Copy the code
  • Case 2
// The cartesian product of List
List.of(x= > y => z= > [x, y, z].join(The '-'))
  .ap(List.of('tshirt'.'sweater'))
  .ap(List.of('white'.'black'))
  .ap(List.of('small'.'medium'.'large'))
Copy the code
  • Example 3
const Db = ({
  find: (id, cb) = >
    new Task((rej, res) = >
      setTimeout((a)= > res({ id, title: `${id}`}), 100))})const reportHeader = (p1, p2) = >
  `Report: ${p1.title} compared to ${p2.title}`

Task.of(p1= > p2 => reportHeader(p1, p2))
  .ap(Db.find(20))
  .ap(Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8

liftA2
  (p1= > p2 => reportHeader(p1, p2))
  (Db.find(20))
  (Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8
Copy the code

4.10. Traversable

1. Introduction of problems

import fs from 'fs'

/ / see 4.8.
const readFile = (file, enc) = > (
  new Task((rej, res) = >...). )const files = ['a.js'.'b.js']

// [Task, Task], we get an array of tasks
files.map(file= > readFile(file, 'utf-8'))
Copy the code

What we want, however, is a Task([file1, file2]) that contains an array so that we can call its fork method and see the result.

To solve this problem, functional programming is generally implemented with a method called traverse.

files
  .traverse(Task.of, file => readFile(file, 'utf-8'))
  .fork(console.error, console.log)
Copy the code

The first argument to the traverse method is the function to create the functor, and the second argument is the function to apply to the functor.

2. Implement

Actually the above code has a bug… Because Array does not have traverse methods. That’s ok. Let’s make it happen

Array.prototype.empty = []

// traversable
Array.prototype.traverse = function (point, fn) {
  return this.reduce(
    (acc, cur) = > acc
      .map(z= > y => z.concat(y))
      .ap(fn(cur)),
    point(this.empty)
  )
}
Copy the code

Looking a little dizzy?

The body of the code is a reduce, which is familiar, traversing elements from left to right, where the second argument is passed the empty element of the monoid.

The first argument is basically a call to the AP method via applicative functor and the result of its execution is merged into an array using the concat method.

So Task([foo, bar]) is returned, so we can call the fork method to execute it.

You can make Natural Transformations easily.

1. Basic concepts

A natural transformation is a function that accepts a functor and returns another functor. Take a look at the code to familiarize yourself with ~

const boxToEither = b= > b.fold(Right)
Copy the code

The boxToEither function is a natural transformation (nt) that converts the functor Box to another functor Either.

So can we use Left?

The answer is no!

Because a natural transformation doesn’t just convert one functor into another, it also satisfies the following rules:

nt(x).map(f) == nt(x.map(f))
Copy the code

For example:

const res1 = boxToEither(Box(100))
  .map(x= > x * 2)
const res2 = boxToEither(
  Box(100).map(x= > x * 2)
)

res1 === res2 // Right(200)
Copy the code

That is, changing functor A and converting it to functor B is equivalent to converting functor A to functor B and then changing it.

Obviously, Left does not satisfy this rule. So any function that satisfies this rule is a natural transformation.

2. Application scenarios

Example 1: Get twice the value of the last number in an array less than or equal to 100

const arr = [2.400.5.1000]
const first = xs= > fromNullable(xs[0])
const double = x= > x * 2
const getLargeNums = xs= > xs.filter(x= > x > 100)

first(
  getLargeNums(arr).map(double)
)
Copy the code

By nature, it is obviously equivalent to first(getLargeNums(arr)).map(double). But the latter obviously performs much better.

Let’s look at a slightly more complicated example:

Example 2: Find the ID of the user’s best friend whose ID is 3

/ / false API
const fakeApi = (id) = > ({
  id,
  name: 'user1'.bestFriendId: id + 1,})/ / false Db
const Db = {
  find: (id) = > new Task(
    (rej, res) = > (
      res(id > 2
        ? Right(fakeApi(id))
        : Left('not found'))))}Copy the code
// Task(Either(user))
const zero = Db.find(3)

/ / the first edition
// Task(Either(Task(Either(user)))) ???
const one = zero
  .map(either= > either
    .map(user= > Db
      .find(user.bestFriendId)
    )
  )
  .fork(
    console.error,
    either => either // Either(Task(Either(user)))
      .map(t= > t.fork( // Task(Either(user))
        console.error,
        either => either
            .map(console.log), // Either(user))))Copy the code

What the hell is this??

Definitely not…

// Task(Either(user))
const zero = Db.find(3)

/ / the second edition
const two = zero
  .chain(either= > either
    .fold(Task.rejected, Task.of) // Task(user)
    .chain(user= > Db
      .find(user.bestFriendId) // Task(Either(user))
    )
    .chain(either= > either
      .fold(Task.rejected, Task.of) // Task(user)
    )
  )
  .fork(
    console.error,
    console.log,
  )
Copy the code

The problem with the second version was redundant nested code.

// Task(Either(user))
const zero = Db.find(3)

/ / the third edition
const three = zero
  .chain(either= > either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .chain(user= > Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(either= > either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .fork(
    console.error,
    console.log,
  )
Copy the code

The problem with the third edition is redundant repetitive logic.

// Task(Either(user))
const zero = Db.find(3)

// This is a natural transformation
// Convert Either to Task
const eitherToTask = (e) = > (
  e.fold(Task.rejected, Task.of)
)

/ / the fourth edition
const four = zero
  .chain(eitherToTask) // Task(user)
  .chain(user= > Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(eitherToTask) // Task(user)
  .fork(
    console.error,
    console.log,
  )

/ / error
const error = Db.find(2) // Task(Either(user))
  // Task.rejected('not found')
  .chain(eitherToTask)
  // This is never called, it is skipped
  .chain((a)= > console.log('hey man'))... .fork(console.error, // not found
    console.log,
  )
Copy the code

4.12. Isomorphism

Isomorphism is a kind of mapping defined between mathematical objects, which can reveal the relationships between the properties or operations of these objects.

In simple terms, two different types of objects are transformed, preserving structure and not losing data.

How did you do that?

Isomorphism is a pair of functions: to and from, following the following rules:

to(from(x)) === x
from(to(y)) === y
Copy the code

This shows that both types can hold the same information in a lossless way.

1. For exampleString[Char]It’s isomorphic.

// String ~ [Char]
const Iso = (to, from) = > ({ to, from })

const chars = Iso(
  s= > s.split(' '),
  c => c.join(' '))const str = 'hello world'

chars.from(chars.to(str)) === str
Copy the code

What’s the use of that?

const truncate = (str) = > (
  chars.from(
    // We first convert it to an array using the to method
    // So you can use the array methods
    chars.to(str).slice(0.3)
  ).concat('... ')
)

truncate(str) // hel...
Copy the code

2. Let’s look at arrays with at most one argument[a]EitherIsomorphism relation of

// [a] ~ Either null a
const singleton = Iso(
  e= > e.fold((a)= > [], x => [x]),
  ([ x ]) => x ? Right(x) : Left()
)

const filterEither = (e, pred) = > singleton
  .from(
    singleton
      .to(e)
      .filter(pred)
  )

const getUCH = (str) = > filterEither(
  Right(str),
  x => x.match(/h/ig)
).map(x= > x.toUpperCase())

getUCH('hello') // Right(HELLO)

getUCH('ello') // Left(undefined)
Copy the code

The resources

  • JS functional programming guide
  • Pointfree programming Style Guide
  • Hey Underscore, You’re Doing It Wrong!
  • Functional Concepts with JavaScript: Part I
  • Professor Frisby Introduces Composable Functional JavaScript
  • Functional programming introductory tutorial
  • What are Functional Programming, Monad, Monoid, Applicative, Functor ??

Related articles

  • JavaScript Functional Programming (PART 1)
  • JavaScript Functional Programming (2)
  • JavaScript Functional programming (III) — this article
  • JavaScript Functional programming (4) In the pipeline…

To be continued…