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 3: Introduction to Functors and Monads 본문

weekly

Functional Programming in JavaScript, Part 3: Introduction to Functors and Monads

벽돌1 2020. 4. 28. 22:52

https://marmelab.com/blog/2018/09/26/functional-programming-3-functor-redone.html

[

Functional Programming in JavaScript, Part 3: Introduction to Functors and Monads

`Functors` and `Monads` may sound frightening, they are powerful concepts that can help developers on a day-to-day basis.

marmelab.com

](https://marmelab.com/blog/2018/09/26/functional-programming-3-functor-redone.html)

Exchanging The Function And Value Roles

placeholder패턴이 functor

What if instead of passing the value to the functions, we passed the functions to the value? Or rather to a placeholder for the value.

값을 함수로 전달하는 대신에 함수를 값으로 전달하면 어떨까? 또는 ...?

I mean, a value already knows some important things about the functions that will be called with it:

  • If I am null, there is no need to execute the function.
  • If I am not of the correct type, I should skip all functions passed to me(value).
  • If I am an error, I should not execute this function, but I should execute this other one instead.
  • If I am asynchronous, I should call this function once I arrive
    • 내(value)가 비동기(promise)라면 promise가 resolve(once I arrive)됐을 때만 실행한다.

In short, the value knows best what to do with the function.

This is the basic idea behind a pattern calledFunctor: a placeholder for a value, so that you pass the function to the value, and not the other way around. We are in functional programming after all: we want to manipulate functions, not values.

이건 functor라는 패턴 뒤에 숨어있는 기본 아이디어다. : 값을 위한 placeholder가 functor다.

우리는 결국에 FP안쪽에서 생각하는 중이다 : 우리는 값이 아닌 함수를 조작하길 원한다.

Let's write a placeholder for a number value: aNumberBox.

리턴이 this가 아닌 새로운 NumberBox가 된다.....

const NumberBox = number => ({
  applyFunction: fn => NumberBox(fn(number)),
  value: number,
});

NumberBox(5)
  .applyFunction(v => v * 2)
  .applyFunction(v => v + 1).value; // 11

For now, this code still suffers from the same problem on non-number arguments. So let's enhance the placeholder to check that it holds a numeric value before applying the function.

const NumberBox = number => ({
  applyFunction: fn => {
    if (typeof number !== "number") {
      return NumberBox(NaN);
    }
    return NumberBox(fn(number));
  },
  value: number,
});

NumberBox(5)
  .applyFunction(v => v * 2)
  .applyFunction(v => v + 1).value; // 11
NumberBox({ v: 5 })
  .applyFunction(v => v * 2)
  .applyFunction(v => v + 1).value; // NaN

ThisapplyFunctionmethod looks a lot like another function that we often use, doesn't it?

[1, 2, 3].map(v => v + 1);

That's right,Array.map()works the same: it takes values from an array instead of a box, but it applies the function all the same.

From now on let's renameapplyFunctiontomap, as we map a function over a value.

const NumberBox = number => ({
  map: fn => {
    if (typeof number !== "number") {
      return NumberBox(NaN);
    }
    return NumberBox(fn(number));
  },
  value: number,
});

And, just like that, I have created afunctor, which is a value placeholder offering amapfunction to execute functions on it.

So to recapitulate, I have a box that holds a value and can apply a function to it thanks to amapmethod. This allows adding custom logic to the function application. This way I manipulate a function, not a value. And with this pattern, I can do a lot more.

Using The Functor Pattern For Type Checking

Let's generalize theNumberBoxfunction to handle any type checking, not just numbers. I'll create aTypeBoxfunction, that generates a functor based on a test (orpredicate, to use a more exact word):

const TypeBox = (predicate, defaultValue) => {
  const TypePredicate = value => ({
    map: fn =>
      predicate(value) ? TypePredicate(fn(value)) : TypePredicate(defaultValue),
    value,
  });
  return TypePredicate;
};

So now I can recreate theNumberBoxby passing a number check as predicate, andNaNas default value:

const NumberBox = TypeBox(value => typeof value === "number", NaN);
NumberBox(5)
  .map(v => v * 2)
  .map(v => v + 1).value; // 11
NumberBox({ v: 5 })
  .map(v => v * 2)
  .map(v => v + 1).value; // NaN

I can also create aStringBox, a functor for strings:

const StringBox = TypeBox(value => typeof value === "string", null);
StringBox("world")
  .map(v => "Hello " + v)
  .map(v => "**" + v + "**").value; // '**Hello, world**'
StringBox({ v: 5 })
  .map(v => "Hello " + v)
  .map(v => "**" + v + "**").value; // null

And you can imagine how this applies to all kinds of type checks.

Tip

Themapmethod has an interesting property: It allows to compose functions. Said otherwise:

const double = v => v * 2;
const increment = v => v + 1;
NumberBox(5)
  .map(double)
  .map(increment).value; // 11
// will have the same result as :
NumberBox(5).map(v => increment(double(v))).value; // 11

In fact, anymapimplementation must make sure to have this property. In a functional paradigm, we want to compose small functions into more complex ones.

map구현은 이 속성(순서대로 조합가능한 특성)을 보장해야한다.

There is another thing that I must make sure of:mapmust not have anyside effects. It must only change the functorvalue, and nothing else. How can I make sure of this? By testing that mapping the identity function (v => v) returns the exact samefunctor. This is called the identity law.

어떻게 보장할래? : 정확히 동일한 functor를 리턴하는 identity function mapping(monad의 identity)테스트를 함으로써.

아래 코드가 mapping

NumberBox(5).map(v => v)) == NumberBox(5)

I will not check these laws for the other examples, but feel free to check them for yourself.

Maybe Another Example

Sometimes, making a function robust isn't about checking the input type, it's about checking that the input exists. Like when a function accesses a property:

const street = user.address.street;

To handle the case where user or user.address are not set, I need to add existence checks:

const street = user && user.address && user.address.street;

I can use the functor pattern to handle such cases. The idea is to create a placeholder to hold sometimes a value, and sometimes nothing. Let's call this functor Maybe.

이 idea는 반쯤은 value를 잡고 반쯤은 nothing을 잡는 placeholder를 생성한다.

const isNothing = value => value === null || typeof value === "undefined";

const Maybe = value => ({
  map: fn => (isNothing(value) ? Maybe(null) : Maybe(fn(value))),
  value,
});

Now it's safe to call properties on a non-object:

const user = {
  name: "Holmes",
  address: { street: "Baker Street", number: "221B" },
};
Maybe(user)
  .map(u => u.address)
  .map(a => a.street).value; // 'Baker Street'
const homelessUser = { name: "The Tramp" };
Maybe(homelessUser)
  .map(u => u.address)
  .map(a => a.street).value; // null

Tip: Calling the value property at the end isn't very natural in functional programming. I can augment the Maybe functor with a getter that accepts a default value as argument:

const isNothing = value => value === null || typeof value === 'undefined';

const Maybe = value => ({
    map: fn => isNothing(value) ? Maybe(null) : Maybe(fn(value)),
    getOrElse = defaultValue => isNothing(value) ? defaultValue : value,
});

As for accessing object properties, it can get even better using a get helper:

const get = key => value => value[key];

const getStreet = user =>
  Maybe(user)
    .map(get("address"))
    .map(get("street"))
    .getOrElse("unknown address");

getStreet({
  name: "Holmes",
  firstname: "Sherlock",
  address: {
    street: "Baker Street",
    number: "221B",
  },
}); // 'Baker Street'

getStreet({
  name: "Moriarty",
}); // 'unknown address'

getStreet(); // 'unknown address'

Either Right or Wrong

What about error handling? If the function mapped on a functor throws an error, the functor should be able to handle it.

For instance, let's consider the code required to validate a user email. To be precise, I want to validate an email address only if it is given, otherwise, I ignore it and return null.

const validateEmail = value => {
  if (!value.match(/\S+@\S+\.\S+/)) {
    throw new Error("The given email is invalid");
  }
  return value;
};

validateEmail("foo@example.com"); // 'foo@example.com'
validateEmail("foo@example"); // throw Error('The given email is invalid')

I'm not fond of that throw, because it interrupts the execution flow. What I'd like is a validateEmail function that doesn't throw an error, but may return either a value or an error. How to do this with functors?

I need to return a different functor based on a try/catch: either a functor that allows me to continue to map ---> Right, or a functor that would not change its value anymore (if it's an error) ---> Left. I'll call these two functors Left and Right. The validateEmail function would look like the following:

const validateEmail = value => {
  if (!value.match(/\S+@\S+\.\S+/)) {
    return Left(new Error("The given email is invalid"));
  }
  return Right(value);
};

The Left functor should never change its value anymore, whatever function is passed to its map:

const Left = value => ({
  map: fn => Left(value),
  value,
});

Left(5).map(v => v * 2).value; // 5

As for the Right functor, it simply applies a function to its value, transparently. You could call it the Identify functor, too.

const Right = value => ({
  map: fn => Right(fn(value)),
  value,
});

Right(5).map(v => v * 2).value; // 10

And now I can chain map calls on my email value:

validateEmail("foo@example.com") // valid email, return Right functor
  .map(v => "Email: " + v).value; // 'Email: foo@example.com'
validateEmail("foo@example") // invalid email, return Left functor
  .map(v => "Email: " + v).value; // Error('The given email is invalid')

The Left and Right functors form what we call an Either.

Catching ErrorsThe validateEmail returns either a result or an error, but doesn't allow me to do something specific in case of an error. For instance, I may want to catch the Error and log its message property.

I'll add a catch() function to both the Left and Right functors to allow me to do just that:

// on a Right, catch() should do nothing
const Right = value => ({
  map: fn => Right(fn(value)),
  catch: () => Right(value),
  value,
});
// catch is ignored on a right
Right(5).catch(error => error.message).value; // 5

// on a Left, catch() should apply the function, and return a Right to allow further mapping
const Left = value => ({
  map: fn => Left(value),
  catch: fn => Right(fn(value)),
  value,
});
Left(new Error("boom")).catch(error => error.message).value; // 'boom'

For better reusability, I combine these Left and Right functors into a generic tryCatch function:

const tryCatch = fn => value => {
  try {
    return Right(fn(value)); // everything went fine we go right
  } catch (error) {
    return Left(error); // oops there was an error let's go left.
  }
};

And now, I can use this new tryCatch function to decorate the initial implementation of validateEmail, and get either a Left or a Right functor in return.

const validateEmail = tryCatch(value => {
  if (!value.match(/\S+@\S+\.\S+/)) {
    throw new Error("The given email is invalid");
  }
  return value;
});

validateMail("foo@example.com")
  .map(v => "Email: " + v)
  .catch(get("message")).value; // 'Email: foo@example.com'
validateMail("foo@example")
  .map(v => "Email: " + v)
  .catch(get("message")).value; // 'The given email is invalid'

Catching Errors

The validateEmail returns either a result or an error, but doesn't allow me to do something specific in case of an error. For instance, I may want to catch the Error and log its message property.

I'll add a catch() function to both the Left and Right functors to allow me to do just that:

// on a Right, catch() should do nothing
const Right = value => ({
  map: fn => Right(fn(value)),
  catch: () => Right(value),
  value,
});
// catch is ignored on a right
Right(5).catch(error => error.message).value; // 5

// on a Left, catch() should apply the function, and return a Right to allow further mapping
const Left = value => ({
  map: fn => Left(value),
  catch: fn => Right(fn(value)),
  value,
});
Left(new Error("boom")).catch(error => error.message).value; // 'boom'

For better reusability, I combine these Left and Right functors into a generic tryCatch function:

const tryCatch = fn => value => {
  try {
    return Right(fn(value)); // everything went fine we go right
  } catch (error) {
    return Left(error); // oops there was an error let's go left.
  }
};

And now, I can use this new tryCatch function to decorate the initial implementation of validateEmail, and get either a Left or a Right functor in return.

const validateEmail = tryCatch(value => {
  if (!value.match(/\S+@\S+\.\S+/)) {
    throw new Error("The given email is invalid");
  }
  return value;
});

validateMail("foo@example.com")
  .map(v => "Email: " + v)
  .catch(get("message")).value; // 'Email: foo@example.com'
validateMail("foo@example")
  .map(v => "Email: " + v)
  .catch(get("message")).value; // 'The given email is invalid'

Combining Functors

Now, let's try to validate not an email string, but a user object that maybe has an email. I do not need to validate the email if the user has no email property.

Let's use Maybe and combine it with Either:

const validateUser = user =>
  Maybe(user)
    .map(get("email"))
    .map(v => validateMail(v).catch(get("message")));

validateUser({
  firstName: "John",
  email: "foo@example.com",
}); // Maybe(Right('foo@example.com'))

validateUser({
  firstName: "John",
  email: "foo@example",
}); // Maybe(Left('The given email is invalid'))

validateUser({
  firstName: "John",
}); // Maybe(null)

I always get a Maybe, but its value is either a Left or a Right functor, or even a null value. So in order to get the value, I have to test Maybe value to know what to do with it.

const validateUserValue = user => {
  const result = validateUser(user).value;
  if (value === null || typeof value === "undefined") {
    return null;
  }
  return result.value;
};

This is bad: all the code I worked so hard to remove is coming back for revenge. You can imagine that it gets worse if I try to combine more functors, ending up with .value.value.value.value...
How can I handle this crisis?

Chain To The Rescue

I need a method allowing me to extract(추출하다) a value of a functor, even though this value is wrapped inside another functor.

값이 다른 functor로 감싸져있더라도 functor의 값을 추출해주는 메소드가 필요하다.

I need a flatten method that removes the inner functor, keeping only its value.

inner functor를 제거하는 flatten 함수가 필요하다.

const Maybe = value => ({
    // we could return the value, but then we would sometimes switch functor type.
    // This way Maybe.flatten will always return a Maybe
    flatten: () => isNothing(value) ? Maybe(null) : Maybe(value.value), //bug -> 이 다음 장에서 설명해준다
    ...,
});

const validateUser = user => Maybe(user)
    .map(get('email'))
    .map(v =>  validateMail(v)
        .catch(get('message'))
    )
    .flatten()
    // since now I will always have a simple Maybe, I can use getOrElse to get the value
    .getOrElse('The user has no mail');

validateUser({
    firstName: 'John',
    email: 'foo@example.com',
}); // 'foo@example.com'

validateUser({
    firstName: 'John',
    email: 'foo@example',
}); // 'The given email is invalid'

validateUser({
    firstName: 'John',
}); // 'The user has no mail'

So now I can call .getOrElse and get the value directly. But that's still not ideal. You see, in functional programming, you often need to map and then flatten, or to flatMap for short. But the term "flatMap" describes what the procedure does, not what it is used for. This operation is used to chain functors, so let's call it that: chain.

우리는 종종 map과 flatten이 필요하고 이걸 짧게 말해 flatMap이라고 한다.

flatMap은 procedure가 뭘 하는지는 설명하지만 어디에 쓰는지는 설명하지 않으므로 chain으로 부르자.

const Maybe = value => ({
    flatten: () => isNothing(value) ? Maybe(null) : Maybe(value.value), //bug
    // using the named function form instead of the fat arrow function
    // because I need access to this
    chain(fn) {
        return this.map(fn).flatten();
    },
    ...,
});

const validateUser = user => Maybe(user)
    .map(get('email'))
    .chain(v =>  validateMail(v)
        .catch(get('message'))
    )
    .getOrElse('The user has no mail');

validateUser({
    firstName: 'John',
    email: 'foo@example.com',
}); // 'foo@example.com'

validateUser({
    firstName: 'John',
    email: 'foo@example',
}); // 'The given email is invalid'

validateUser({
    firstName: 'John',
}).value; // 'The user has no mail'

Note that the two names are used in functional programming, so flatMap == chain.

Here Comes The Monad

By the way, a functor that can flatten itself with a chain method is called a Monad. So Maybe is a monad. Not so scary, right?

Like map, chain must respect a few laws to ensure that monads combine correctly.

functor가 chain을 지원하면 monad다~!!

functor는 placeholder였어여..... function을 value에게 전달하는 수단....

  • Left identity

    Monad(x).chain(f) === f(x);
    // f being a function returning a monad -> f는 monad를 리턴하는 함수이다

    Chaining a function to a monad is the same as passing the value to the function. This ensures that the encompassing monad is totally removed by chain.

    • chain하면 map하고 flat해서 Monad가 return 된다.
  • Right identity

    Monad(x).chain(Monad) === Monad(x);

    Chaining the monad constructor(생성자) should return the same monad. This ensures that chain has no side effect.

  • Associativity (결합법칙)

    monad.chain(f).chain(g) == monad.chain(x => f(x).chain(g));
    // f and g being functions returning a monad

    Chain must be associative: Using .chain(f).chain(g), is the same as using .chain(v => f(v).chain(g)).

This ensures that we can chain function that uses chain themselves.

Conclusion

You can play with the code of this tutorial in the CodeSandbox below. It uses a slightly modified example: try to understand why it's better than what was explained earlier.