Author: Zhao Xiangtao

The concept of Functional Programming has become increasingly popular in both the front-end and back-end domains, and it is rare to find large applications that do not make extensive use of Functional Programming techniques, such as the popular React (Core idea data as view). Vue3.0’s Composition API, Redux, Lodash and other front-end frameworks and libraries are all full of functional thinking. In fact, functional programming is not a programming paradigm created in recent years, but at the beginning of computer science. Lambda calculus, published by Alonzo Church in the 1930s, can be said to be the preexistence of functional programming.

This series of articles is suitable for those who have a solid foundation in JavaScript and have some experience in functional programming. The purpose of this article is to explain some concepts and logic of category theory in the context of JavaScript language features in the practical application of programming.

The curse of the Black Pearl

Set sail!

First, let’s take a look at the code of the Double 11 promotion, both as a review of the concept of function composition and as the first step of the new journey:

const finalPrice = number= > {
    const doublePrice = number * 2
    const discount = doublePrice * 8.
    const price = discount - 50
    return price
}

const result = finalPrice(100)
console.log(result) / / = > 110
Copy the code

Take a look at the code of the simple Double 11 shopping carnival above. After a fancy promotion (discount (20% off) + coupon (50)) of the original price of 100 goods, everyone successfully got the sale price of 110. Good deal. Chop your hands off

If you’ve read another functional programming primer from our Cloud Music front end team, I’m sure you already know how to write functional programs: programs that pipe data between a series of pure functions. We also know that these procedures are declarative codes of behavior. Now use the idea of function composition again to keep the data pipeline operation and eliminate so many intermediate variables, keeping the style point-free:

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

    const double = x= > x * 2
    const discount = x= > x * 0.8
    const coupon = x= > x - 50

    const finalPrice = compose(coupon, discount, double)

    const result = finalPrice(100)
    console.log(result) / / = > 110
Copy the code

Well! Finally, something functional! At this point, we see that the argument 100 passed to finalPrice is pipelinedlike a factory widget by functions double, discount, and Coupon. 100 flows through pipes like water. Are we familiar with this scene? Array’s map and filter are completely similar concepts, aren’t they? So we can wrap our input parameters with Array:

const finalPrice = number= >
    [number]
        .map(x= > x * 2)
        .map(x= > x * 0.8)
        .map(x= > x - 50)

const result = finalPrice(100)
console.log(result) / / = > [110]
Copy the code

Now we put the number into the Array container and call map three times in a row to pipe the data. After careful observation, Array is just a container for our data. We just want to use Array’s map method. We don’t need other methods for the time being, so why not create a Box container?

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

const finalPrice = str= >
    Box(str)
        .map(x= > x * 2)
        .map(x= > x * 0.8)
        .map(x= > x - 50)

const result = finalPrice(100)

console.log(result) // => Box(110)
Copy the code

The reason for using the function Box instead of ES6’s Class is to avoid the “bad” new and this keywords (from You Don’t Know JS volume 1). New is mistaken for creating an instance of Class. There is no such thing as instantiation, just a simple attribute delegation mechanism (a combination of objects), and this introduces the problem of execution context and lexical scope, and I just want to create a simple object!

The purpose of the inspect method is to implicitly call it using console.log in Node.js so we can see the type of data; This method does not work in browsers and can be replaced with console.log(String(x)); Node. Js V12 API is subject to change, can use Symbol. For (‘ nodejs. Util. Inspect. Custom) alternative to inspect

The reason for using a chain of successive dot, dot, dot calls instead of the compose combination is to make it easier to understand that compose is more functional.

The sealed Black Pearl

The map in Box is the same as the famous map in array, except that the former operates on Box(x) and the latter on [x]. They’re used almost the same way, just drop a value into a Box and keep going map, map, map… :

Box(2).map(x= > x + 2).map(x= > x * 3);
// => Box(12)

Box('hello').map(s= > s.toUpperCase());
// => Box('HELLO')
Copy the code

This is the first container on functional programming, we call it the Box, and the data like captain jack black pearl, in a jar, we can only through the map method to operate the value, and the Box like a virtual barrier, also can say to a certain extent, to protect the values in the Box are not random access and operation.

Why use this idea? Because we can manipulate the values inside the container without leaving the Box. Once we pass the values in the Box to the map function, we can do whatever we want; After the operation is complete, it is put back into the Box where it belongs in case of accidents. As a result, we can continuously call map and run any function we want. You can even change the type of the value, as in the last example above.

A map is an efficient and safe way to transform values in a container using lambda expressions.

Wait, if we can always call map.map.map, can we call this Type Mappable Type? There’s no problem with that!

Map knows how to map function values in context. It first opens the container, then maps the value to another value through the function, and finally wraps the resulting value into a new container of the same type. This method of transforming values in a container (map) is called a Functor.

Functor is a concept in category theory. What is category theory?? I don’t understand!!

Don’t panic! We will continue briefly with the concept and theory of category theory and Functor, but let’s forget that strange name for a moment and skip the concept.

Let’s keep calling it Box, which we all understand.

The Redemption of the Black Pearl

Similar to Box(2).map(x => x + 2) we can already wrap any type of value in a Box and go map, map, map… .

Another question, how do we get our values out? I want to get 4 instead of Box of 4!

What good is the Black Pearl if it can’t be released from its bottle? Let Captain Jack Sparrow seize Blackbeard’s sword and free the Black Pearl!

It’s time to add another method to our most primitive Box.

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

Box(2)
    .map(x= > x + 2)
    .fold(x= > x)  / / = > 4
Copy the code

Well, see the difference between a fold and a map?

mapIs to rewrap the result of function execution into Box and return a new Box type, whilefoldReturn the result of the function.

Practical use of Box

Try-Catch

JavaScript errors occur in many cases, especially when communicating with the server or when trying to access the properties of a NULL object. We always have to be prepared for the worst, and most of this is done by try-catch.

For example:

const getUser = id= >
    [{ id: 1.name: 'Loren' }, { id: 2.name: 'Zora' }]
        .filter(x= > x.id === id)[0]

const name = getUser(1).name
console.log(name) // => 'Loren'

const name2 = getUser(4).name
console.log(name2) // => 'TypeError: Cannot read property 'name' of undefined'
Copy the code

So now the code is reporting an error, using a try-catch can solve the problem to some extent:

try {
    const result = getUser(4).name
    console.log(result)
} catch (e) {
    console.log('error', e.message) // => 'TypeError: Cannot read property 'name' of undefined'
}
Copy the code

As soon as an Error occurs, JavaScript immediately terminates execution and creates a call stack trace of the function that caused the problem and stores it in an Error object. Catch acts as a haven for our code. But will try-catch solve our problem properly? Try-catch has the following disadvantages:

  • Reference transparency is violated because throwing an exception causes another exit from the function call, so a single predictable return value cannot be guaranteed.
  • Can cause side effects because exceptions can cause unexpected effects on the stack outside of the function call.
  • It violates the principle of locality, because the code used to recover the exception is further removed from the original function call, and when an error occurs, the function leaves the local stack and environment.
  • Instead of just caring about the return value of a function, the caller is responsible for declaring the exception matching type in the catch block to manage specific exceptions; It’s hard to combine or link with other functions, and you can’t have the next function in the pipeline handle an error thrown by the previous one.
  • Nested exception handling blocks occur when there are multiple exception conditions.

Exceptions should be thrown in one place, not everywhere.

As you can see from the description and code above, try-catch is a completely passive solution and very non-functional. How nice would it be to be able to easily handle and even contain errors? Let’s use the Box concept to optimize these problems

To the left? To the right?

A close analysis of the try-catch block logic shows that our code exits either in a try or a catch. The way our code is designed, we want our code to go through the try branch. Catch is one of our backstop schemes, so we can use the analogy of “Right” for the normal branch and “Left” for the exception branch. So let’s extend our Box to Left and Right, and look at the code:

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

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

const resultLeft = Left(4).map(x= > x + 1).map(x= > x / 2)
console.log(resultLeft)  // => Left(4)

const resultRight = Right(4).map(x= > x + 1).map(x= > x / 2)
console.log(resultRight)  / / = > Right (2.5)
Copy the code

The difference between Left and Right is that Left automatically skips functions passed by the map method, while Right, like the most basic Box, executes the function and rewraps the return value into the Right container. Left and Right are exactly like Reject and Resolve in promises. The result of a Promise is either Reject or Resolve, and structures with Right and Left branches, We can call it Either, Either left or right. It makes sense, right? The above code shows the basic usage of Left and Right. Now apply our Left and Right to getUser.

const getUser = id= > {
    const user = [{ id: 1.name: 'Loren' }, { id: 2.name: 'Zora' }]
        .filter(x= > x.id === id)[0]
    return user ? Right(user) : Left(null)}const result = getUser(4)
    .map(x= > x.name)
    .fold((a)= > 'not found', x => x)

console.log(result) // => not found
Copy the code

Unbelievable! We are now able to handle errors linearly and even give a not found warning (provided by the fold), but if you think about it a little more carefully, maybe our original getUser function would return undefined or a normal value, Can we just wrap the return value of this function?

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

const getUser = id= >
    fromNullable([{ id: 1.name: 'Loren' }, { id: 2.name: 'Zora' }]
            .filter(x= > x.id === id)[0])

const result = getUser(4)
    .map(x= > x.name)
    .fold((a)= > 'not found', c => c.toUpperCase())

console.log(result) // => not found
Copy the code

Now that we’ve successfully handled the possibility of null or undefined, what about try-catch? Can it also be wrapped by Either?

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

const jsonFormat = str= > JSON.parse(str)

const app = (str) = >
    tryCatch((a)= > jsonFormat(str))
        .map(x= > x.path)
        .fold((a)= > 'default path', x => x)

const result = app('{"path":"some path..." } ')
console.log(result) // => 'some path... '

const result2 = app('the way to death')
console.log(result2) // => 'default path'
Copy the code

Now our try-catch does not interrupt our combination of functions, and the Error is properly controlled without throwing an Error object at random.

It is recommended to open netease cloud music to listen to a “left to right”! Relax and reflect on our Right and Left.

What is a Functor? How to use Functor? Why use Functor?

What is a Functor?

Above we defined a simple Box, which is essentially a type with map and fold methods. Let’s slow down a bit and take a closer look and think about our map: Box(a) -> Box(b), which essentially maps a Box(a) to Box(b) through a function a -> b. This is similar to the function knowledge in middle school algebra, might as well review the definition of function in algebra textbook:

Suppose A and B are two sets, and if, by some correspondence rule, any element of A has A unique correspondence in B, the correspondence is called A function from set A to set B.

The above sets A and B can be compared to String, Boolean, Number, and more abstract objects in our programming world. In general, we can think of A data type as A Set of all possible values. Boolean, for example, is a set of [true,false], Number is a set of all real numbers, and all sets, with sets as objects and mappings between sets as arrows, form a category:

A is a set of strings (String), b is a set of real numbers (Number), and C is a set of Boolean numbers (Number). G => str.length and f => number >=0? True: false, then we can map from the string category to the real number category by using function G, and then from the real number category to the Boolean category by using function F.

Now let’s go back to that obscure name we skipped earlier: Functor is the arrow that maps categories to categories! This arrow is usually implemented using the map method in conjunction with a transform function (i.e. STR => str.length), which is pretty easy to understand, right?

If we have the function g and f, then we must be able to derive the function h = f·g, which is const h = compose(f,g), and that is the transformation of a -> c in the bottom part of the graph. We’re all math majors. We all know how to do that

Wait, what’s that ID arrow on a, B, and C? It maps to itself, right? Good job! For any Functor, fx.map(id) == id(FX) can be implemented by the function const id = x => x, and this is called Identity, the mathematical Identity.

That’s why we have to introduce category theory, the idea of Functor, not just mappale or something, Because then we can better understand the Composition and Identity of Functor, which come with mathematical principles, on the basis of keeping the same name, do not let this obscure name stop us.

The above introduction is just a way for front-end scumballs (compared to Haskell) to understand categories at some level. It’s not very rigorous, objects in categories may not be sets, arrows may not be maps… Stop!!!!! Hold on! If I keep talking, I’ll be an algebra teacher.

How to use Functor?

Now back in the world of code again, there is no doubt that Functor is an all too common concept. The vast majority of developers use Functor all the time without even realizing it. Such as:

  • An Array ofmapfilter.
  • jQuerycssstyle.
  • The PromisethencatchMethod (Promise is also a Functor? Yes!) .
  • The Rxjs observablesmapfilter(Combination of asynchronous functions? Relax!) .

Both return functors of the same type, so they can be called in a chain over and over again. In fact, these are extensions of the idea of Box:

[1.2.3].map(x= > x + 1).filter(x= > x > 2The $()"#mybtn").css("width"."100px").css("height"."100px").css("background"."red");

Promise.resolve(1).then(x= > x + 1).then(x= > x.toString())

Rx.Observable.fromEvent($input, 'keyup')
    .map(e= > e.target.value)
    .filter(text= > text.length > 0)
    .debounceTime(100)    
Copy the code

Why use Functor?

What is the point of putting a value into a container (Box, Right, Left, etc.) and then manipulating it only with a map? If we think about it in a different way, the answer is obvious: What is the advantage of having the container use the function itself? The answer is: abstractions, abstractions applied to functions.

The whole point of functional programming is to combine small functions into higher-level functions. Take an example of a combination of functions: what if I wanted to apply a unified map to any Functor? Partial Application:

const partial =
    (fn, ... presetArgs) = >(... laterArgs) => fn(... presetArgs, ... laterArgs);const double = n= > n * 2
const map = (fn, F) = > F.map(fn)
const mapDouble = partial(map, double)

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

The key is that mapDouble returns a function that waits to receive the second argument F (Box(1)); Once the second argument is received, f.map(fn) is executed directly, which is equivalent to Box(1).map(double). This expression returns Box(2), so you can continue with.fold and so on.

Summary and plan

conclusion

Through the example of double Eleven shopping carnival above, several basic concepts of functional programming (pure function, compose) were introduced, and the powerful concept of Box was gradually introduced, that is, the most basic Functor. Either can be used as a null package container. Either can be used as a null package container. The try-catch example shows how pure can handle errors. Either is not the only use of Either, but other advanced uses will be introduced later. Finally, it summarizes what is Functor, how to use Functor and what are the advantages of using Functor.

plan

Functor is one of the most basic concepts in the category theory we’ve introduced, but we’ve solved the simplest problems so far (better combinations (maps), more robust code (fromNullAble), purest error handling (try catch), but what about nested try-catches? How do you combine asynchronous functions? I will continue to introduce other concepts and practical usage examples in category theory through the case of the Double 11 shopping carnival. (Practical purpose: To continue to expose the tricks of dishonest merchants, and incidentally to become an algebra teacher, to get rid of the unwritten rules of elimination at 34; Doghead. JPG).

References and citations:

  • What is a functor?
  • So You Want to be a Functional Programmer
  • Two Years of Functional Programming in JavaScript: Lessons Learned
  • Master the JavaScript Interview: What is Functional Programming?
  • JavaScript Functional Programming Guide
  • You Don’t Know JS
  • Category theory for programmers

This article is published from netease Cloud music front end team, the article is prohibited to be reproduced in any form without authorization. We’re always looking for people, so if you’re ready to change jobs and you like cloud music, join us!