함수자

프로그래밍에서 에러 핸들링이라는 또 다른 중요한 개념을 살펴본다.

함수자(functor) 라는 새로운 개념을 살펴본다. 이 개념은 순수하게 함수형 방법으로 에러를 다룰 수 있도록 도와준다.
함수자의 아이디어를 이해한 후 두 가지의 실제 함수자(MayBe, Either) 를 구현해본다.

함수자

함수자란 기본적인 객체로, 객체 내의 각 값을 실행할 때 새로운 객체를 실행하는 map 함수를 구현한다.

함수자는 컨테이너다.

간단하게 함수자는 값을 갖고 있는 컨테이너다. 함수자가 기본적인 객체라는 정의에서 이를 살펴봤다. 값을 갖는
간단한 컨테이너를 생성해보며, Container 를 호출해본다.

1
2
3
4
5
6
7
8
9
10
11
12
const Container = function (val) {
this.value = val;
}

let testValue = new Container(3);
=> Container(value: 3)

let testObj = new Container({a: 1});
=> Container(value: {a: 1})

let testArray = new Container([1, 2]);
=> Container(value: [1, 2])

Container는 값을 내부에 저장하기만 한다. 자바스크립트의 모든 데이터형을 전달할 수 있으며, Container 는 이를 저장한다.
Container 프로토타입에서 of 라고 불리는 유용한 메서드를 생성할 수 있는데, new 키워드를 사용하지 않아도
새로운 Container 를 생성할 수 있게 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Container.of = function(value) {
return new Container(value);
}

let testValue = Container.of(3);
=> Container(value: 3)

let testObj = Container.of({a: 1});
=> Container(value: {a: 1})

let testArray = Container.of([1, 2]);
=> Container(value: [1, 2])

Container.of(Container.of(3))
/**
Container {
value: Container {
value: 3
}
}
*/

map 구현

map 함수가 필요한 이유는 현재 Container에 저장된 값에 대한 함수를 호출할 수 있다.

map 함수는 Container 의 값을 받고 해당 값에 전달된 함수를 적용한 후 결과를 다시 Container 에 넣는다.

1
2
3
4
5
6
Container.prototype.map = function(fn) {
return Container.of(fn(this.value));
}

let double = x => x + x;
console.log(Container.of(3).map(double)) // 6

map 함수는 Container 에 전달된 함수의 결과를 다시 반환하며, 이는 결합 연산을 가능케한다.

1
2
3
Container.of(3).map(double)
.map(double)
.map(double)

MayBe 함수자

MayBe 함수자는 좀 더 함수적인 방법으로 코드의 에러를 핸들링할 수 있다.

MayBe 구현

MayBe는 함수자의 한 형태로, map 함수를 다른 방식으로 구현한다.

1
2
3
4
5
6
7
const MayBe = function(val) {
this.value = val;
}

MayBe.of = function(val) {
return new MayBe(val);
}

Container 구현과 유사하다. map 을 구현해본다.

1
2
3
4
5
6
7
MayBe.prototype.isNothing = function() {
return (this.value === null || this.value === undefined);
}

MayBe.prototype.map = function(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));
}

map 함수는 Container 의 map 함수와 유사하다. MayBe 의 map 은 전달된 함수에 isNothing 함수를
적용해 컨테이너 값이 null 인지 undefined 인지 먼저 확인한다.

간단한 사용자 케이스

MayBe 는 map 에 전달된 함수를 적용하기 전에 null 과 undefined 를 확인한다.
이는 에러 핸들링을 다루는 가장 강력한 추상화다.

1
MayBe.of("string").map(x => x.toUpperCase());

위 코드에서 x 가 null 또는 undefined 일때 에러가 발생한다. 하지만 MayBe 에서 래핑하므로, 에러가 발생하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
MayBe.of("George")
.map(x => x.toUpperCase())
.map(x => `Mr. ${x}`);

// MayBe { value: "Mr. GEORGE" }

MayBe.of("George")
.map(() => undefined)
.map(x => `Mr. ${x}`);

// MayBe { value: null }

모든 map 함수가 null/undefined 를 받는 것과 상관없이 호출된다. 이 처럼 MayBe 는 모든 undefined, null 에러를 쉽게 다룰 수 있게 도와준다.

Either 함수자

Either 함수자를 만들어 분기 문제를 해결한다.

1
2
3
4
5
MayBe.of("George")
.map(() => undefined)
.map(x => `Mr. ${x}`);

// MayBe { value: null }

원하는 결과가 출력됐다. 하지만 어떤 분기 (map 호출 두개) 가 undefined 및 null 값에 부합하지 않는지 알 수 없다.
어떤 분기에서 발생된 문제 인지 찾으려면 MayBe 의 분기를 일일이 파고 들어야 한다. 이것이 Either 가 필요한 이유다.

Either 구현

Either 함수자 부분 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Nothing = function(val) {
this.value = val;
}

Nothing.of = function(val) {
return new Nothing(val);
}

Nothing.prototype.map = function(f) {
return this;
}

const Some = function(val) {
this.value = val;
}

Some.of = function(val) {
return new Some(val);
}

Some.prototype.map = function(fn) {
return Some.of(fn(this.value));
}

위 구현에는 Some 과 Nothing 이라는 두 함수가 있다. Some 함수는 Container 를 이름만 바꿔 복사한 것이다.
Nothing 의 map 은 주어진 함수를 실행하지 않고 오히려 반환한다. 다시 말해 Some 에서는 함수를 실행하는데,
Nothing 에서는 실행하지 않는다.

1
2
3
4
Some.of("test").map(x => x.toUpperCase());
// Some {value: "TEST"}
Nothing.of("test").map(x => x.toUpperCase());
// Nothing {value: "test"}

Some 과 Nothing 두 객체를 Either 객체로 감싼다.

1
2
3
4
5
6
7
const Either = {
Some,
Nothing
}

Either.Some.of("Super").map(x => `${x}star`)
Either.Nothing.of("Super").map(x => `${x}star`)

Either 는 MayBe 와 다르게 예외 상황이 발생한 경우 null 값이 반환되지 않는다.

1
2
3
4
const books = [
{ id: 'book1', title: 'coding with javascript', author: 'Chris Minnick, Eva Holland' },
{ id: 'book2', title: 'speaking javaScript', author: 'Axel Rauschmayer' },
];

위와 같이 데이터가 있다고 할때, author 가 Axel 을 포함하고 있을 경우 console.log, 포함하고 있지 않을 경우 console.error 를 실행하고자 한다.
console.error 를 실행할때 MayBe 같은 경우 null 값이 반환되기 때문에 어떤 값에서 에러가 발생하는지 알 수 없다.
이 문제를 Either 를 사용하여 해결하고자 한다.

우선 Some 과 Nothing 에 다음과 같은 프로토타입을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Some.prototype.isNothing = function() {
return false
}
Some.prototype.isSome = function() {
return true
}

Nothing.prototype.isNothing = function() {
return true
}
Nothing.prototype.isSome = function() {
return false
}
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
const findBookById = curry((id, books) => {
return books.find((book) => book.id === id);
});

const validateBookAuthor = (book) => {
return book.author.indexOf('Axel') === -1
? Either.Nothing.of(book)
: Either.Some.of(book);
}

const logByEitherStatus = (eitherBook) => {
return eitherBook.isNothing
? console.error(`Author: ${eitherBook.value.author}`)
: console.log(`Author: ${eitherBook.value.author}`)
}

const logBookAuthor = (bookId, books) => {
return pipe(
findBookById(bookId),
validateBookAuthor,
logByEitherStatus
)(books)
};


logBookAuthor('book1', books);
logBookAuthor('book2', books);

validateBookAuthor 함수 부분에서 Axel 이 포함되어 있지 않은 경우 Nothing.of 를 실행하여 분기 처리를 해준다.

모나드

모나드(Monad)

  1. chain 메소드를 구현한 객체다.
  2. chain 메소드는 모나드가 가진 값에 함수를 적용해서 새로운 모나드(chain 을 갖고 있는)를 반환해야 한다.

모나드는 chain 메서드를 갖는 함수자다. 즉 이것이 모나드다.

1
2
3
4
5
6
7
Maybe.prototype.join = function() {
return this.isNothing() ? MayBe.of(null) : this.value;
}

MayBe.prototype.chain = function(f){
return this.map(f).join()
}

MayBe 함수자를 chain 을 추가해 모나드로 만들었다. 반복적인 map 이 중첩된 값에 매우 효율적이다.

참조: 함수형 자바스크립트 입문 2/e
참조: nakta.log

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.