Image source: unsplash.com/photos/RPLw…

Author: Zhao Xiangtao

Functor and Applicative are Functor and Functor. They are Functor and Applicative. They are Functor and Applicative. Use Either to handle ubiquitous null and create composable try-catches; Use Applicative for highly flexible and extensible form validation; The core of Functor: map- applying a function to a wrapped value, and the core of Functor: ap- applying a wrapped function to a wrapped value.

Don’t forget a few remaining questions:

  • How to solve nestingtry-catch
  • A combination of asynchronous functions
  • Promise is also a Functor?

Solve the three problems one by one, starting with the first: nested try-catch.

This article builds on the previous two, so it is advisable to read the first two articles before reading this one, otherwise you may get confused with some concepts and nouns

Nested Array

Array.prototype.flatmap is a method that is used in Javascript Array, but is not used in Javascript Array. A flat map is literally a flat map, and it works exactly as it does in practice. Here’s a use case to compare it to a map:

const arr = [1.2.3.4]

arr.map(x= > [x * 2])  // => [[2], [4], [6], [8]]

arr.flatMap(x= > [x * 2])  // => [2, 4, 6, 8]
Copy the code

The differences between flatMap and Map are as follows:

  • mapIs to put the results of the function together and put them in a Box;
  • flatMapThe result is that theThe results of function execution are stripped of a layer of “wrapper”“And put them together in the Box

So flatMap is like map and then flat, just one more “unpack” operation!

Russian nesting doll

Array is also a specific implementation case of Box concept. What about other boxes? How do you use Either? To start with a simpler combination of functions, write a street name function that gets the user’s address:

const compose = (. fns) = > x= > fns.reduceRight((v, f) = > f(v), x)
const address = user= > user.address
const street = address= > address.street

const app = compose(street, address)

const user = {
    address: {
        street: 'long'}}const res = app(user) / / = > long river
Copy the code

The theory of function composition is also very simple, as long as the return type of the previous function can be used as the input parameter of the next function, you can safely and boldly combine.

Note that the value of the address attribute on the User object may be null, and that TypeError can occur if nothing is done to prevent it. Before this problem don’t worry, after all is ready to handle null/undefined Either functor, can use fromNullable packing the code above:

const address = user= > fromNullable(user).map(u= > u.address)
const street = address= > fromNullable(address).map(a= > a.street)

const app = user= >
    address(user)     // Either(address)                                       
        .map(street)  // Either(Either(street))

const res = app(user) // => Rirgt(Right)
Copy the code

Looking at the code above, the street function returns Either, but remember that the map method (map: F => Right(f(x))) rewraps the result of the function execution into a “box”, i.e., the final result is: Rirgt(Right(‘ long river ‘)).

This is obviously not what we want, we only want the street wrapped in one layer. The problem is that when the map method is used (the map will be wrapped twice), we can simply use the fold method to unpack the result of the street function execution from the box.

const app = user= >
        address(user)                              // Either(address)
       .map(s= > street(s).fold(x= > x, x= > x))   // Either(street)
       .fold(() = > 'default street'.x= > x)       // street
       
const res = app(user)  // => 'long river'
Copy the code

There is no doubt that there are a few times of packaging, you need a few “unpacking” operations, so there is no logical problem. However, this is similar to the common front end callback hell problem, such code is too difficult to maintain and read, you can not write a line to count the number of layers of packaging.

It’s a code version of a Russian nesting doll:

The reason for the double wrapping is that map rewraps the results of the function calculation into the Box, which is a bit redundant because it is immediately unpacked, much like Array flatMap (map then flat).

Because the result of the function execution is already wrapped, only one method (flatMap) is required to execute the function directly without doing anything else

const Right = x= > ({
  flatMap: f= > f(x),
});

const Left = x= > ({
  flatMap: f= > Left(x),
});

const app = user= >
    address(user)                             // Either(address)
        .flatMap(street)                      // Either(street)
        .flod(() = > 'default street'.x= > x)  // street
Copy the code

The difference between Map and flatMap is that the map method receives a function that simply transforms the values inside the container, so it needs to be rewrapped in Box. FlatMap, however, accepts a function that returns type Box and can be called directly.

The similarities between map and flatMap are obvious: they both return an instance of Box for subsequent chaining calls.

The flatMap method has the same logic as the FLOd method? Admittedly, they are very similar, but their usage scenarios are completely different! Flod is used to free a value from the Box; The purpose of flatMap is to apply a function that returns a Box to a Box so that chained calls can continue.

According to the specification, the flatMap method is followed by chain, which in other languages may also be called bind.

Nested Either, nested try-catch, nested Either, nested try-catch, nested Either, nested try-catch, nested Either, nested try-catch, nested Either

For example, if you want to read a configuration file from a file system and then read the contents (note that fs.readfilesync and json.parse are both error prone, so try-catch wraps are used) :

const readConfig = (filepath) = > {
    try {
        const str = fs.readFileSync(filepath);
        const config = JSON.parse(str);
        return config.version
    } catch (e) {
        return '0.0.0'}}const result = readConfig('/config.json');
console.log(result) / / = > '1.0.0'
Copy the code

Now rewrite the above code using the “box” concept + the “chain” function as follows:

const readConfig = (filepath) = >
    tryCatch(() = > fs.readFileSync(filepath))             // Either('')
        .chain(json= > tryCatch(() = > JSON.parse(json)))  // Either('') 
        .fold(() = > '0.0.0'.c= > c.version)

const result = readConfig('/config.json');
console.log(result) / / = > '1.0.0'
Copy the code

If a Functor implements the chain method, the Functor can be called a Monad. The concept of a good Monad is that simple; If you Google Monad, there are tons of articles about Monad, and one of the best explanations is:

“A monad is just A monoid in the category of endofunctors. What’s the problem?”

This quote is from brief — incomplete — and — mostly — wrong, and it’s all about making fun of Haskell, technically, but mostly.

And the exact definition of Monad is:

All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor. — Saunders Mac Lane

So do you understand this definition? (Don’t hit me) It doesn’t really matter if you don’t understand it, because it’s for professional math students. We just need to know that Monad is a chainable object that can be used to solve nested Box problems.

asynchronous

There’s no doubt that asyncrony is a major part of the JavaScript world, from onclick callbacks for buttons, to onload callbacks for AJAX requests, to readFile callbacks in Node.js. “Asynchronous non-blocking” implies a programming paradigm based on callback functions.

For the theory of asynchrony and event loops, please refer to another article by netease Cloud Music team: Talk about JavaScript concurrency, asynchrony and event loops

The callback and asynchronous

Starting with the simplest callback function, we’ll start with a typical Node.js-style callback:

const getUrl = url= > callback= > request(url, callback)
const getSomeJSON = getUrl('http://example.com/somedata.json')
getSomeJSON((err, data) = >{
  if(err){
    //do something with err
  }else {
    //do something with data}})Copy the code

This is a simple asynchronous HTTP request that is passed in a Currified URL and then a callback. What are the disadvantages of this style?

  • 1. The caller of a function has no direct control over the request and must place all subsequent operations oncallbackinside
  • 2. Functions cannot be combined becausegetSomeJSONNo results are returned after the call

Another key point is that the callback receives two arguments, an error message and a success data, forcing us to deal with both the error and data logic in a single function.

Instead of passing a function that takes two arguments (Err & Data), we can pass two functions (handleError & handleData) that take one argument each.

const getUrl = url= >
    (reject, resolve) = >
        request(url, (err, data) = >{
            err? reject(err) : resolve(data)
        })
Copy the code

Now that getUrl is called, we can continue passing handleError and handleData

const getSomeJSON = getUrl('http://example.com/somedata.json')
const handleError = err= > console.error('Error fetching JSON', err)
const handleData = compose(renderData, JSON.parse)

getSomeJSON(handleError, handleData) // Trigger the request
Copy the code

The logic for handleData and handleError is now completely separated, and the handleData functions can be combined as expected, while the (Reject, resolve) => {} functions are called fork, meaning two “branches.”

The Task and asynchronous

Now we have another problem. We always need to do json.parse in handleData, because converting strings to JSON is the first step in any data processing logic. It would be nice if we could combine getSomeJSON with the json. parse function; Now the question is clear: how do I combine an ordinary function with a fork?

This may seem like a tricky problem, but you can start with a simple problem. Suppose you have stringifyJson, how do you convert it to JSON, using the LazyBox concept introduced in the previous chapter:

const stringifyJson= '{"a":1}'

LazyBox(() = > stringifyJson).map(JSON.parse) // => LazyBox({ a: 1 })
Copy the code

We can wrap a function in A LazyBox, and then use map to keep combining functions until we call the fold function, which actually triggers the function call.

LazyBox is used to wrap synchronous functions in a box, so it is possible to wrap asynchronous fork functions in a box, and map ordinary f functions. For asynchronous logic, we can call it a Task (Task: some goal or result will be achieved in the future)

const Task = fork= > ({
    map: f= > Task((reject, resolve) = >          // return another Task, including a new fork.
            fork(reject, x= > resolve(f(x)))),   // when called,the new fork will run `f` over the value, before calling `resolve`
    fork,
    inspect: () = > 'Task(?) '
})
Copy the code

The new fork calls the previous fork, resolve if it is a correct branch, and pass reject if it is a failed branch.

If you haven’t looked deeply into how Promise works, this might be hard to understand, but stop and think about it for a moment.

Now rewrite the readConfig function using Task:

const readConfig = filepath= > Task((reject, resolve) = >
    fs.readFile(filepath, (err, data) = >
        err ? reject(err) : resolve(data)
    ))

const app = readConfig('config.json')
    .map(JSON.parse)

app.fork(() = > console.log('something went wrong'), json= > console.log('json', json))
Copy the code

Task.map is very similar to LazyBox’s map in that it is a combination of functions and does not actually call the function. LazyBox calls the fold and Task calls the fork.

Combination of Task and asynchronous functions

Now that you have an “elegant” readConfig function implemented through Task, what if you continue to modify the configuration file and save it locally? Let’s start with the writeConfig function, which is written exactly like the readConfig function:

const app = readConfig(readPath)
    .map(JSON.parse)
    .map(c= > ({ version: c.version + 1 }))
    .map(JSON.stringify)

const writeConfig = (filepath, contents) = >
    Task((reject, resolve) = > {
        fs.writeFile(filepath, contents, (err, _) = >
            err ? reject(err) : resolve(contents)
        )
    })
Copy the code

Since writeConfig returns a Task, it is obvious that a function like array.prototype. flatMap and Either. Chain is needed. Help us apply the function that returns Task to our app:

const Task = fork= > ({
    chain: f= > Task((reject, resolve) = >                   // return another Task
            fork(reject, x= > f(x).fork(reject, resolve)))  // calling `f` with the eventual value
})
Copy the code

Similar to the chain function in Either, f is called directly (return TaskB), and then the TaskB fork is called (reject, resolve) to handle the subsequent logic.

Now you can continue to compose the writeConfig function using chain smoothly

const app = readConfig(readPath)
    .map(JSON.parse)
    .map(c= > ({ version: c.version + 1 }))
    .map(JSON.stringify)
    .chain(c= > writeConfig(writeFilepath, c))

app.fork(() = > console.log('something went wrong'), 
    () = > console.log('read and write config success'))
Copy the code

For example, if you call two interfaces in a row, and the second interface relies on the return value of the first interface as a parameter, you can combine two asynchronous HTTP requests with the chain:

const httpGet = content= > Task((rej, res) = > {
    setTimeout(() = > res(content), 2000)})const getUser = (id) = > httpGet('Melo')
const getAge = name= > httpGet(name)

getUser('id')
    .chain(name= > getAge(name + '18'))
    .fork(console.error, console.log) // => 4000ms later, log: "Melo 18"
Copy the code

Monad VS Promise

The code implementation of Task is not as intuitive and understandable as Box, Either, LazyBox, but if you think about it carefully, you will find that Task and Promise are very, very similar. We can even think of a Task as a lazy-promise: promises are executed immediately when they are created, whereas tasks are executed after a fork is called.

As for reading the configuration file, modifying it, and then saving it locally, I think you can easily write out the implementation of the Promise version as a comparison to show example code:

const readConfig = filepath= > new Promise((resolve, reject) = >
    fs.readFile(filepath, (err, data) = >
        err ? reject(err) : resolve(data)
    ))

const writeConfig = (filepath, contents) = > new Promise((resolve, reject) = > {
    fs.writeFile(filepath, contents, (err, _) = >
        err ? reject(err) : resolve(contents)
    )
})

readConfig(readPath)
    .then(JSON.parse)
    .then(c= > ({ version: c.version + 1 }))
    .then(JSON.stringify)
    .then(c= > writeConfig(writeFilepath, c))
Copy the code

The readConfig and writeConfig implementations in both versions are very similar and will not be described; The key difference is that the Task version uses the map and chain functions, while the Promise version has always used THEN. So Promise looks very similar to Monad, so it begs the question, is Promise Monad?

Compare this to the simplest Box Monad:

const Box = x= > ({
    map: f= > Box(f(x)),
    chain: f= > f(x),
})

const box1 = Box(1)                          // => Box(1)
const promise1 = Promise.resolve(1)          // => Promise(1)
    
box1.map(x= > x + 1)                         // => Box(2)
promise1.then(x= > x + 1)                    // => Promise(2)

// -----------------

box1.chain(x= > Box(x + 1))                 // => Box(2)
promise1.then(x= > Promise.resolve(x + 1))  // => Promise(2)
Copy the code

You can see that if the function returns an unwrapped value, then and map behave similarly; If the function returns a wrapped value, then, like chain, removes a layer of wrapping. In this sense, Promise and Functor/Monad are both similar enough to meet their mathematical rules.

Here we go:

box1.map(x= > Box(x + 1))                   // => Box(Box(2))
promise1.then(x= > Promise.resolve(x + 1))  // => Promise(2)

box1.chain(x= > x + 1)                      / / = > 2
promise1.then(x= > x + 1)                   // => Promise(2)
Copy the code

If you pass a function that returns a wrapped value to THEN, you don’t get a wrapped value with two layers, as Functor does, but only one; Similarly, if we pass a function that returns a normal value to THEN, we still get a Promise, while the result of chain is to remove a layer of wrapping and get a value. In this way, Promise breaks both Functor and Monad’s mathematical rules. Therefore, Strictly speaking, Promise is not a Monad, but there is no denying that the design of Promise is definitely inspired by Monad.

The content of this section is relatively difficult to understand, mainly in the implementation principle of Task and the combination of asynchronous functions, which requires good mathematical thinking in logic. I hope you can think about it more, and there will be more harvest, after all, we just used a few lines of code. The enhanced Promise-> Lazy Promise-> Task is implemented.

Promises and Monads: Promises and Monads, Difference between Promises and Task

Apply functors and monads

Monad is better at dealing with situations that have a Context. In the getUser and getAge examples above, getAge must wait until the asynchronous execution of getUser is complete before it can be called. This is a ** vertical (serial)** link;

Applicative is better at dealing with horizontal (parallel) links, such as the form validation example in the previous chapter, where each field validation has no relationship at all.

Can tasks be parallelized asynchronously? The answer is yes! Assuming that getUser and getAge are not dependent on each other, the apply method of Applicative can be used to combine them.

Task
    .of(name= > age= > ({ name, age }))
    .ap(getUser)
    .ap(getAge)
    .fork(console.error, console.log) // 2000ms later, log: "{name: 'Melo', age: 18}"
Copy the code

Task.ap can refer to the principle of promise. all, and the specific implementation can refer to gist. Github.

conclusion

  • FunctorIt’s an implementationmapThe data type of the method
  • ApplicativeIt’s a realizationapplyThe data type of the method
  • MonadIt’s a realizationchainflatmapThe data type of the method

What are the differences between Functor, Applicative and Monad?

  • Functor: Applies a function to the value of the package, usingmap.
  • Applicative: Applies a wrapped function to the wrapped value, usingap
  • Monad: applies a function that returns the value of a package to the value of a package, usingchain

References and citations:

  • Array.prototype.flatMap
  • Monads and Gonads (YUIConf Evening Keynote)
  • Marvellously mysterious javascript maybe monad
  • Understanding Functor and Monad With a Bag of Peanuts
  • Functors, Applicatives, And Monads In Pictures
  • A Brief, Incomplete, and Mostly Wrong History of Programming Languages
  • The Little Idea of Functional Programming
  • Comparison to Promises
  • Functional Programming In JavaScript — With Practical Examples
  • Compose monads
  • Translation from Haskell to JavaScript of selected extras of the best introduction to monads I’ve ever read
  • The cat explains Monad
  • How to explain Monad in simple language?
  • “JavaScript Functional Programming”

This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!