Maybe 타입 알아보기

2017년 03월 31일


함수 재사용

Sanctuary의 Maybe가 멋진 점은 기존의 Ramda에서 사용하던 함수에 호환이 된다는 것입니다. 덕분에 새로운 함수를 많이 배우지 않고도 Maybe의 강력한 기능을 온전히 사용할 수 있습니다.

Maybe에 map 적용하기

Maybe는 여러가지 관점에서 바라볼 수 있습니다. 값이 있는 경우와 없는 경우로 나눠진다는 것을 다르게 해석하면 Maybe는 원소를 최대 1개까지 가질 수 있는 배열로 이해할 수 있습니다.

배열의 관점으로 보면 Just는 원소가 하나 있는 배열을, Nothing은 비어있는 배열로 바꿔서 생각할 수 있습니다.

배열처럼 생각할 수 있기 때문에 배열에 호환되는 함수를 그대로 사용할 수 있습니다. 가장 대표적인 함수 R.map은 다음과 같이 사용 가능합니다.

import * as R from 'ramda';
import * as S from 'sanctuary';

const a = S.Just(41);
const b = [41];
const c = S.Nothing;
const d = [];

R.map(R.inc, a); // Just(42)
R.map(R.inc, b); // [42]
R.map(R.inc, c); // Nothing
R.map(R.inc, d); // []

Maybe에 대한 map의 성질

Maybe도 배열과 비슷하게 사이드 이펙트가 없는 경우에만 드러나는 유용한 성질 몇 가지가 있습니다. 이러한 성질들 중 일부는 배열을 통해 이해할 수 있습니다.

Just, Nothing을 보존한다

Just는 원소를 하나 갖는 배열로, Nothing은 비어있는 배열로 생각할 수 있습니다.

각각의 배열(Just, Nothing)에 map을 적용해도 길이는 보존되기 때문에 Just와 Nothing은 보존됩니다.

import * as R from 'ramda';
import * as S from 'sanctuary';

const f = R.pipe(R.map(???), R.map(???), R.map(???));

const xs = f([1]);
const ys = f([]);
xs.length; // 1
ys.length; // 0

const a = f(S.Just(42));
const b = f(S.Nothing);
a.isJust;    // true
b.isNothing; // true

이어서 호출하는 경우 계산을 하나로 합칠 수 있다

Maybe의 경우에는 별로 유용하지 않지만, 배열과 마찬가지로 계산하는 방식을 바꿀 수 있습니다.

계산을 하나로 합치면 Just를 반환하고 다시 map을 적용하는 과정 없이, 한 번의 map 호출로 결과값을 얻을 수 있습니다.

import * as R from 'ramda';
import * as S from 'sanctuary';

const f = R.pipe(R.map(R.multiply(10)), R.map(R.add(5)));
const g = R.map(R.pipe(R.multiply(10), R.add(5)));
const m = S.Just(1);

f(m); // Just(15)
g(m); // Just(15)

배열과 마찬가지로 중간에 임시객체가 발생하지 않지만 성능 면에서 크게 의미가 있는 것은 아닙니다. 같은 성질을 공유한다는 것이 중요합니다.

Maybe에 lift 적용하기

lift는 값을 받아 값을 반환하는 함수를 받아 배열을 받아 배열을 반환하는 함수를 만드는 고차함수였습니다.

사실 lift로 생성된 함수는 배열 말고도 다양한 타입으로 호출할 수 있습니다. 이 중 한 가지로 Maybe가 있습니다.

import * as R from 'ramda';
import * as S from 'sanctuary';

const f = (x, y) => `(${x}, ${y})`;
const g = R.lift(f);

const xs = [1, 2]
const ys = [1, 2]
const zs = []

g(xs, ys); // ['(1, 1)', '(1, 2)', '(2, 1)', '(2, 2)']
g(xs, zs); // []
g(ys, zs); // []

const x = [1]
const y = [2]
const z = []

g(x, y); // ['(1, 2)']
g(x, z); // []
g(y, z); // []

const a = S.Just(1);
const b = S.Just(2);
const c = S.Nothing;

g(a, b); // Just('(1, 2)')
g(a, c); // Nothing
g(b, c); // Nothing

lift된 함수를 Maybe로 호출하면 모든 인자가 Just인 경우에만 Just를 반환하고 Nothing이 하나라도 섞여있으면 Nothing을 반환합니다.

이러한 동작은 계산 도중 값이 비어있으면 에러를 발생시켜야 하는 경우 유용합니다.

import * as R from 'ramda';

const xs = [{ id: 30, data: 20 }, { id: 31, data: 97 }];
const ys = [{ id: 15, data: 12 }, { id: 29, data: 32 }];
const zs = [{ id: 78, data: 33 }, { id: 92, data: 22 }];
const fn = (x, y, z) => x.data + y.data + z.data;

const a = R.find(R.propEq('id', 30), xs);
const b = R.find(R.propEq('id', 29), ys);
const c = R.find(R.propEq('id', 78), zs);

if (a && b && c) {
  const result = fn(a, b, c); // 85
  console.log(result);
} else {
  console.log('?!?! a b c');
}

const z = R.find(R.propEq('id', 42), zs);

if (a && b && z) {
  const result = fn(a, b, z); // can't be executed
  console.log(result);
} else {
  console.log('?!?! a b z');
}

Maybe와 lift를 이용하면 다음과 같이 표현할 수 있습니다. 결과로 얻은 값이 비어있는지 직접 확인할 필요가 없습니다.

import * as R from 'ramda';
import * as S from 'sanctuary';

const xs = [{ id: 30, data: 20 }, { id: 31, data: 97 }];
const ys = [{ id: 15, data: 12 }, { id: 29, data: 32 }];
const zs = [{ id: 78, data: 33 }, { id: 92, data: 22 }];
const fn = (x, y, z) => x.data + y.data + z.data;

const safeFind = R.pipe(R.find, S.toMaybe);

const ma = safeFind(R.propEq('id', 30), xs);
const mb = safeFind(R.propEq('id', 29), ys);
const mc = safeFind(R.propEq('id', 78), zs);

const safeFn = R.lift(fn);
safeFn(ma, mb, mc); // Just(85)

const mz = safeFind(R.propEq('id', 42), zs);
safeFn(ma, mb, mz); // Nothing