Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

신나는 개발...

Functional Programming in JavaScript, Part 2: The Monoid 본문

weekly

Functional Programming in JavaScript, Part 2: The Monoid

벽돌1 2020. 4. 8. 22:41

https://marmelab.com/blog/2018/04/18/functional-programming-2-monoid.html

 

Functional Programming in JavaScript, Part 2: The Monoid

What do number addition, string concatenation, array concatenation, and function composition have in common? They are all monoids, and they have very interesting properties.

marmelab.com

What is the similarity between number addition, string concatenation, array concatenation, and function composition?

They are allmonoids. And monoids are super useful.

 

The term Monoid comes from category theory. It describes a set of elements which has 3 special properties when combined with a particular operation, often namedconcat:

a set of : 하나의 타입 string, number....

a set of elements :

 

concat이라는 특졍한 연산과 함께 반드시 함쳐져야한다 두개의 밸류를 세번

  • The operation must combine two values of the set(set:type의 2가지 value) into a third value of the same set(같은 타입의 third value). If a and b are part of the set, then concat(a, b) must also be part of the set. In category theory, this is called amagma.
    • int1, int2, int3 : int1 int2를 combine하면 int(third value)가 나와야한다
  • The operation must be associative : concat(x, concat(y, z)) must be the same asconcat(concat(x, y), z) where x,y, and z are any value in the set. No matter how you group the operation, the result should be the same, as long as the order is respected.
    • 어떻게 그룹지던가에 같은 결과를 return한다. ----> 결합법칙!
    • as long as the order is respected : 순서가 보장되는 한
  • The set must possess aneutral element(0으로 생각하면 편하다)in regard to the operation. If that neutral element is combined with any other value, it should not change it. concat(element, neutral) == concat(neutral, element) == element
    • concat(1,0) === concat(0,1) === 1

 

Now that you know what a monoid is, what if I told you that you use it all the time?

Tip: This article is the second one in a series aboutfunctional programming in JavaScript. You can read this one first, or start with the previous one, describing theunit.

Number Addition Is A Monoid

When you add two numbers, you manipulate a set of JavaScriptNumberinstances. The addition operation takes two numbers, and always return a number. Therefore, it is amagma.

The addition isassociative:
(1 + 2) + 3 == 1 + (2 + 3); // true
Finally, the number 0 is the neutral element for the addition operation:

x + 0; // x
So according to the definition, numbers form a monoid under the addition operation.

Tip: You see that the concat function doesn't have to be named concat for a monoid to exist. Here, it's just +.

Let's see another example.

String Concatenation Is a Monoid

In JavaScript, concat is a method of String instances. But it's easy to extract it as a pure function:

const concat = (a, b) => a.concat(b);

 

This function operates on two strings and returns a string. So it's a magma.

It's also associative:

concat("hello", concat(" ", "world")); // "hello world"
concat(concat("hello", " "), "world"); // "hello world"

And it has a neutral element, the empty string (''):

concat("hello", ""); // 'hello'
concat("", "hello"); // 'hello'

So according to the definition, strings form a monoid under the concatenation operation. Do you want another example?

 

Function Composition Is A Monoid

You probably know the compose function:

Compose takes two functions as arguments, and returns a function. So it's a magma.

Now let's see associativity and neutrality:

 

 

Reducing Function Arguments

In the previous post of this series, I explained that when we manipulate functions, we need a way to turn a function that takes two arguments into a function that takes any number of arguments.

 

Monoids are the solution to that problem when used in combination with Array.reduce.

monoids는 Array.reduce와 함께 합체되었을 때 그 문제의 해답이 될 수 있다.

 

Let's start with number addition. The addition operation takes two arguments. How can I apply it on an array of arbitrary size? Using Array.reduce:

arbitrary: 임의의

const add = (a, b) => a + b;
const addArray = arr => arr.reduce(add, 0); // 0 : neutral element
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
addArray(numbers); // 55

 

This works with other monoids, too:

 

const concat = (a, b) => a.concat(b);
const concatArray = arr => arr.reduce(concat, "");
const strings = ["hello", " ", "world"];
concatArray(strings); // 'hello world';

const compose = (f1, f2) => arg => f1(f2(arg));
const composeArray = arr => arr.reduce(compose, x => x);
const resultIs = a => `result: ${a}`;
const add5 = a => a + 5;
const double = a => a * 2;
const functions = [resultIs, add5, double];
const myOperation = composeArray(functions);
myOperation(2); // result: 9

Let's generalize: when you have a monoid, you can transform a function taking two arguments to a function taking an array of arguments by calling:

 

[value1, value2, value3, ...].reduce(concat, neutral);

이것이 일반화 된 패턴이다.

 

Splitting Computation In Chunks : chunks로 계산을 쪼개기

As the operation of a monoid is associative, you can cut an array of values into smaller chunks, apply the concat operation on each array, then recombine the results using the concat operation:

쪼개고 그걸 다시 concat을로 결합을 시켜도 결과는 동일하다.

const concat = (a, b) => a + b;
const bigArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
bigArray.reduce(concat, 0); // 55
const result1 = bigArray.slice(0, 5).reduce(concat, 0); // 15
const result2 = bigArray.slice(5).reduce(concat, 0); // 40
concat(result1, result2); // 55

The interest is to distribute large computation across many computation units (cores). This is possible with any monoid, so that explains why parallel programming is made easier by functional paradigms.

 

Async Composition

Using the same logic, I can create a function that composes any number of asynchronous functions (or any function that returns a promise). I just need to form a monoid

const fetchJoke = async number => fetch(`http://api.icndb.com/jokes/${number}`);
const toJson = async response => response.json();
const parseJoke = json => json.value.joke;

const getJoke = async number => parseJoke(await toJson(await fetchJoke(number))
getJoke(23).then(console.log); // "Time waits for no man. Unless that man is Chuck Norris."

// the getJoke() function is a pain to write. Let's use composition to make it easier
const asyncCompose = (func1, func2) => async x => func1(await func2(x));

// asyncCompose() is associative
asyncCompose(parseJoke, asyncCompose(toJson, fetchJoke))(23).then(console.log);
// "Time waits for no man. Unless that man is Chuck Norris."

asyncCompose(asyncCompose(parseJoke, toJson), fetchJoke)(23).then(console.log);
// "Time waits for no man. Unless that man is Chuck Norris."

// asyncCompose() has a neutral element - the identity function
const neutralAsyncFunc = x => x;
asyncCompose(a => Promise.resolve(a + 1), neutralAsyncFunc)(5) // Promise(6)
asyncCompose(neutralAsyncFunc, a => Promise.resolve(a + 1))(5) // Promise(6)

// so async functions form a monoid under the asyncCompose operation
// hurray, we can use Array.reduce!
const asyncComposeArray = functions => functions.reduce(asyncCompose, x => x);
// let's make it a function that takes an arbitrary number of arguments instead
const asyncComposeArgs = (...args) => args.reduce(asyncCompose, x => x);

// now, writing getJoke() becomes much easier
const getJoke2 = asyncComposeArgs(parseJoke, toJson, fetchJoke);
getJoke2(23).then(console.log); // "Time waits for no man. Unless that man is Chuck Norris."

Thanks to monoids, I managed to write an asynchronous version of compose() working with an arbitrary number of arguments in no time. This is not magic, this is math.

manage to : 위기를 극복할 수 있었다. 할 수 있다.

in no time : 시간을 들이지 않고, 무리없이

 

Async Flow

There is just one problem: this reads backward.

뒤에서부터 읽어야하는게 문제다

const getJoke2 = asyncComposeArgs(parseJoke, toJson, fetchJoke);

So instead of using compose(), let's use flow(), which does exactly the same thing, but the other way around:

const fetchJoke = async number => fetch(`http://api.icndb.com/jokes/${number}`);
const toJson = async response => response.json();
const parseJoke = json => json.value.joke;

const flow = (func1, func2) => async x => func2(await func1(x));
// func1 is executed before func2
const flowArray = functions => functions.reduce(flow, x => x);
// let's make it a function that takes an arbitrary number of arguments instead
const flowArgs = (...args) => args.reduce(flow, x => x);

// now, writing getJoke() becomes much easier
const getJoke2 = flowArgs(fetchJoke, toJson, parseJoke);
getJoke2(23).then(console.log); // "Time waits for no man. Unless that man is Chuck Norris."

That's much more readable: I can use a sequential execution model of the functions passed as arguments to flowArgs().

I love this flowArgs() pattern, it has many advantages in terms of testability, encapsulation, and delayed execution.

delayed execution : 반만 실행하고

a = fn(1)

b = a(2)

 

Conclusion

So monoids are pretty common, and their link with Array.reduce makes them super useful. Don't be frightened by the academic articles on monoids, which make them look harder than they actually are!

Any time you need to compose an arbitrary number of items (e.g. when flattening a hierarchy of objects, or when implementing a workflow of async operations), look for a monoid! That's another great learning from functional programming.

In the next post in this series, I'll talk about the superpowers hidden in Array.map. It will be the time for the Functor, and no this is not the name of a villain in Power Rangers.

 

category : 같은 타입의 a 와 b를 더했을 때 같은 타입이 나온다면 category

[int, string].push(long)

같은 array가 되었기 땜시 같은 카테고리

 

1.add(2).double()

같은 카테로그이고 -> 같은 카테고리를 연산해도 같은 카테고리가 리턴되면 associative가 보장되고 이게 monoid다

카테로리 안에 element가 존재하고 element에 concat을 적용할 수 있는 타입을 카테고리라고 부름...

 

identity

same set

associative

이게 만족되어야 monaid