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
관리 메뉴

히바리 쿄야 와 함께 하는 Developer Cafe

[4일차] DO IT 타입 스크립트 프로그래밍/p294~p338/ 모나드 본문

TypeScript

[4일차] DO IT 타입 스크립트 프로그래밍/p294~p338/ 모나드

TWICE&GFRIEND 2021. 4. 12. 15:05

모나드는 순서가 있는 연산을 처리하는데 사용하는 디자인 패턴이다. 모나드는 순수 함수형 프로그래밍 언어에서 부작용을 관리하기 위해 광범위하게 사용되며 복합 체계 언어에서도 복잡도를 제어하기 위해 사용된다.

모나드는 타입으로 감싸 빈 값을 자동으로 전파하거나(Maybe 모나드) 또는 비동기 코드를 단순화(Continuation 모나드) 하는 등의 행동을 추가하는 역할을 한다.

모나드를 고려하고 있다면 코드의 구조가 다음 세가지 조건을 만족해야 한다.

  1. 타입 생성자 – 기초 타입을 위한 모나드화된 타입을 생성하는 기능. 예를 들면 기초 타입인 number를 위해 Maybe<number> 타입을 정의하는 것.
  2. unit 함수 – 기초 타입의 값을 감싸 모나드에 넣음. Maybe 모나드가 number 타입인 값 2를 감싸면 타입 Maybe<number>의 값 Maybe(2)가 됨.
  3. bind 함수 – 모나드 값으로 동작을 연결하는 함수.

다음의 TypeScript 코드가 이 함수의 일반적인 표현이다. M은 모나드가 될 타입으로 가정한다.

interface M<T> { } function unit<T>(value: T): M<T> { // ... } 
function bind<T, U>(instance: M<T>, 
transform: (value: T) => M<U>): M<U> { // ... }
interface M<T> { } 
function unit<T>(value: T): M<T> { // ... } 
function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> { // ... }

Note: 여기에서의 bind 함수는 Function.prototype.bind 함수와 다르다. 후자의 bind는 ES5부터 제공하는 네이티브 함수로 부분 적용한 함수를 만들거나 함수에서 this 값을 바꿔 실행할 때 사용하는 함수다.

JavaScript와 같은 객체지향 언어에서는 unit 함수는 생성자와 같이 표현될 수 있고 bind 함수는 인스턴스의 메소드와 같이 표현될 수 있다.

interface MStatic<T> { 
   new(value: T): M<T>; 
} 

interface M<T> { 
   bind<U>(transform: (value: T) => M<U>):M<U>; 
}

또한 여기에서 다음 3가지 모나드 법칙을 준수해야 한다.
  1. bind(unit(x), f) ≡ f(x)
  2. bind(m, unit) ≡ m
  3. bind(bind(m, f), g) ≡ bind(m, x => bind(f(x), g))

먼저 앞 두가지 법칙은 unit이 중립적인 요소라는 뜻이다. 세번째 법칙은 bind는 결합이 가능해야 한다는 의미로 결합의 순서가 문제가 되서는 안된다는 의미다. 이 법칙은 덧셈에서 확인할 수 있는 법칙과 같다. 즉, (8 + 4) + 2의 결과는 8 + (4 + 2)와 같은 결과를 갖는다.

아래의 예제에서는 화살표 함수 문법을 사용하고 있다. Firefox (version 31)는 네이티브로 지원하고 있지만 Chrome (version 36)은 아직 지원하지 않는다.

Identity 모나드

identity 모나드는 가장 단순한 모나드로 값을 감싼다. Identity 생성자는 앞서 살펴본 unit과 같은 함수를 제공한다.

function Identity(value) { this.value = value; } 
Identity.prototype.bind = function(transform) { return transform(this.value); }; 
Identity.prototype.toString = function() { return 'Identity(' + this.value + ')'; };

다음 코드는 덧셈을 Identity 모나드를 활용해 연산하는 예시다.

var result = new Identity(5).bind(value => new Identity(6).bind(value2 => new Identity(value + value2)));

Maybe 모나드

Maybe 모나드는 Identity 모나드와 유사하게 값을 저장할 수 있지만 어떤 값도 있지 않은 상태를 표현할 수 있다.

Just 생성자가 값을 감쌀 때 사용된다.

function Just(value) { 
  this.value = value; 
} 

Just.prototype.bind = function(transform) { 
  return transform(this.value); 
}; 


Just.prototype.toString = function() { 
  return 'Just(' + this.value + ')'; 
};

Nothing은 빈 값을 표현한다.

var Nothing = { 
 bind: function() { 
 return this; 
}, toString: function() { 
 return 'Nothing'; } 
};

기본적인 사용법은 identity 모나드와 유사하다.

var result = new Just(5).bind(value => new Just(6).bind(value2 => new Just(value + value2)));

Identity 모나드와 주된 차이점은 빈 값의 전파에 있다. 중간 단계에서 Nothing이 반환되면 연관된 모든 연산을 통과하고 Nothing을 결과로 반환하게 된다.

다음 코드에서는 alert가 실행되지 않게 된다. 그 전 단계에서 빈 값을 반환하기 때문이다.

var result = new Just(5).bind(value => Nothing.bind(value2 => new Just(value + alert(value2))));

이 동작은 수치 표현에서 나타나는 특별한 값인 NaN(not-a-number)과도 유사하다. 결과 중간에 NaN 값이 있다면 NaN은 전체 연산에 전파된다.

var result = 5 + 6 * NaN;

 

var url; var user = getMb(); if (user !== null) { 
   var avatar = user.getMb(); if (avatar !== null) { 
   url = vatar.url; } 
}



function getUser() { 
   return new Just({ getMb: function() { 
   return Nothing;} }); 
} 
 var url = getUser() .bind(user => user.getMb()) 
 .bind(avatar => avatar.url); if(url instanceof Just) { 
 print('URL has value: ' + url.value); } 
 else { 
 print('URL is empty'); 
 }

List 모나드

List 모나드는 값의 목록에서 지연된 연산이 가능함을 나타낸다.

이 모나드의 unit 함수는 하나의 값을 받고 그 값을 yield하는 generator를 반환한다. bind 함수는 transform 함수를 목록의 모든 요소에 적용하고 그 모든 요소를 yield 한다.

function* unit(value) { yield value; } 
function* bind(list, transform) { 
  for (var item of list) { 
     yield* transform(item); 
  } 
}
function* unit(value) { 
   yield value; 
} 
function* bind(list, transform) { 
   for (var item of list) 
{ 
   yield* transform(item); 
} 

}

배열과 generator는 이터레이션이 가능하며 그 반복에서 bind 함수가 동작하게 된다. 다음 예제는 지연을 통해 각각 요소의 합을 만드는 목록을 어떻게 작성하는지 보여준다.

var result = bind([0, 1, 2], function (element) { 
   return bind([0, 1, 2], function* (element2) { 
   yield element + element2; }); 
}); 
   for (var item of result) { 
   print(item); 
}

Continuation 모나드

Continuation 모나드는 비동기 일감에서 사용한다. ES6에서는 다행히 직접 구현할 필요가 없다. Promise 객체가 이 모나드의 구현이기 때문이다.

  1. Promise.resolve(value) 값을 감싸고 pormise를 반환. (unit 함수의 역할)
  2. Promise.prototype.then(onFullfill: value => Promise) 함수를 인자로 받아 값을 다른 promise로 전달하고 promise를 반환. (bind 함수의 역할)
다음 코드에서는 Unit 함수로 Promise.resolve(value)를 활용했고, 
Bind 함수로 Promise.prototype.then을 활용했다.

var result = Promise.resolve(5).then(function(value) { 
   return Promise.resolve(6).then(function(value2) { 
   return value + value2; }); 
}); 
result.then(function(value) { 
  print(value); });

Promise는 기본적인 continuation 모나드에 여러가지 확장을 제공한다. 만약 then이 promise 객체가 아닌 간단한 값을 반환하면 이 값을 Promise 처리가 완료된 값과 같이 감싸 모나드 내에서 사용할 수 있다.

두번째 차이점은 에러 전파에 대해 거짓말을 한다는 점이다. Continuation 모나드는 연산 사이에서 하나의 값만 전달할 수 있다. 반면 Promise는 구별되는 두 값을 전달하는데 하나는 성공 값이고 다른 하나는 에러를 위해 사용한다. (Either 모나드와 유사하다.) 에러는 then 메소드의 두번째 콜백으로 포착할 수 있으며 또는 이를 위해 제공되는 특별한 메소드 .catch를 사용할 수 있다.

 

Comments