ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Functional] promise와 monad, kleisli composition
    programing/Language 2019. 8. 15. 17:20

    안녕하세요, Einere입니다.

    (ADblock을 꺼주시면 감사하겠습니다.)

     

    2019/06/25 - [programing/JavaScript] - [Functional] 평가와 일급, 고차함수

    2019/07/15 - [programing/JavaScript] - [Functional] 순회와 이터러블

    2019/07/15 - [programing/JavaScript] - [Functional] 제네레이터와 이터러블

    2019/07/15 - [programing/JavaScript] - [Functional] map, filter, reduce

    2019/07/18 - [programing/JavaScript] - [Functional] 코드를 값으로 다루어 표현력 높이기

    2019/07/22 - [programing/JavaScript] - [Functional] range,take, 느긋한 L.range, L.take

    2019/07/22 - [programing/JavaScript] - [Functional] 지연 평가와 L.map, L.filter

    2019/08/12 - [programing/JavaScript] - [Functional] join, find, flatten

     


    오늘은 promise와 monad, kleisli composition에 대해 알아보도록 하겠습니다.

     

     

     

    Monad

    기능 프로그래밍에서 모나드는 프로그램 로직에 필요한 상용구 코드를 자동화하면서 프로그램을 일반적으로 구성 할 수있는 디자인 패턴입니다. 이 패턴은 모나드가 자체 자료형을 제공함으로써 달성됩니다. 이 자료형은 모나드 내부의 기본 값을 감싸는 절차와 모나딕한 값을 출력하는 함수를 합성하는 절차, 특정 형태의 계산을 표현합니다. 이를 통해, 모나드는 잠재적 undefined값을 다루기(Maybe monad), 유연하고 잘 정의된 리스트에 값을 보관(List monad)과 같은 광범위한 문제를 간략화합니다.

    모나드라는 개념과 용어는 범주론에서 유래했으며, 추가적인 기능을 가진 함수자(functor)로서 정의됩니다.

    범주론은 모나드 법칙으로 알려진 몇 가지 공식적인 요구 사항을 제공합니다. 이 모나드 법칙은 모나드가 충족해야 하며, 모나드 코드를 확인하는 데 사용될 수 있습니다.

     

    뭔가 어렵네요.

    그럼 예제를 보면서 이해를 해봅시다.

     

    const f = val => val + 1;
    const g = val => val * val;
    
    console.log(g(f(1))); // 출력 : 4
    console.log(g(f())); // 출력 : NaN

    g(f(1))는 1을 인자로 받아, 적절한 결과인 4를 출력했습니다. 그러나 g(f())는 부적절한 결과인 NaN을 출력했습니다. 따라서, $(g\cdot f)(x)$는 안전하게 합성되지 않은 함수라고 할 수 있습니다.

    위 코드를 모나드하게 바꾸면 다음과 같습니다.

     

    [1].map(f).map(g).forEach(val => console.log(val)); // 출력 : 1
    [].map(f).map(g).forEach(val => console.log(val)); // 출력 : (아무것도 출력하지 않음)

    값을 저장하는 컨테이너인 배열을 이용하여, 함수 f를 적용한 뒤, 그 결과를 다시 g에 적용합니다. 최종 출력을 forEach로 순회하면서 터미널에 출력합니다.

    일반 코딩의 경우 NaN이 출력되는 반면, 위와 같은 노마드 코딩은 아무것도 출력하지 않습니다. 따라서, 안전하게 함수를 합성할 수 있습니다.

     

    함수형 프로그래밍에 더 가깝게 리팩토링 한다면, 다음과 같이 구현할 수 있습니다.

    Array.of(1).map(f).map(g).forEach(val => console.log(val)); // 출력 : 1

     

     

     

    Promise와 Monad

    함수를 안전하게 합성하기 위해 모나드라는 개념이 도입되었습니다. 모나드 구현체 중, 비동기 상황에서 함수를 안전하게 합성하기 위해 필요한 것이 promise입니다.

     

    위 예제 코드를 promise를 이용한다면 다음과 같이 구현할 수 있습니다.

    Promise.resolve(1).then(f).then(g).then(result => console.log(result));

     

    위 코드에서, promise의 특성인 비동기성을 적용하면 다음과 같이 구현할 수 있습니다.

    new Promise((res, rej) => {
    	setTimeout(()=> {
        	res(1);
        }, 2000);
    })
        .then(f)
        .then(g)
        .then(result => console.log(result));

    2초 후 값을 평가하는 프로미스를 생성하고, 해당 프로미스가 값을 평가한 후 f와 g를 차례대로 적용합니다.

     

     

     

    kleisli composition

    kleisli composition은 함수 합성 방법중 하나입니다. 오류가 발생할 수 있는 상황에서, 함수 합성을 안전하게 하는 규칙이라고 생각하시면 됩니다.

    예를 들어, 함수 f와 g가 있다고 가정합니다. 이 때, 언제 어디서든 g(f(x)) == g(f(x))이어야 합니다. (수학에서는 자명하겠지만, 프로그래밍에서는 상태에 따라서 결과값이 다를 수 있습니다.) 여기서, f(x)에서 오류가 발생했다고 가정했을 때, g(f(x)) == f(x)로 만들어주는 함수 합성 방법이 kleisli composition입니다. 자세히 말하자면, 오류가 발생하여 잘못된 값을 도출한 f(x)의 결과값을 인자로 받은 함수 g또한 동일한 결과값을 출력해야 한다는 뜻입니다. 코드를 예시로 들면 다음과 같습니다.

     

    const users = [
        {id:1, name:'aa'},
        {id:2, name:'bb'},
        {id:3, name:'cc'}
    ];
    
    // 특정 id의 유저를 찾는 함수
    const f = id => find(user => user.id === id, users);
    // 특정 유저 객체의 이름을 얻는 함수
    const g = ({name}) => name;
    // (g*f)(x) 합성 함수
    const gf = id => g(f(id));
    
    
    const result1 = gf(2);
    users.pop();
    users.pop();
    const result2 = gf(2);
    console.log(result1 === result1); // 출력 : true
    console.log(result1 === result2); // 출력 : error

    두 함수 f와 g는 외부 변수인 users에 의존하고 있기 때문에, users를 조작하기 전 결과인 result1과 result2는 서로 다른 값을 가지게 됩니다. 그럼 이 코드를 안전하게 합성해보도록 하겠습니다.

     

    const users = [
        {id:1, name:'aa'},
        {id:2, name:'bb'},
        {id:3, name:'cc'}
    ];
    
    // 특정 id의 유저를 찾는 함수
    const f = id => find(user => user.id === id, users) || Promise.reject('not found');
    // 특정 유저 객체의 이름을 얻는 함수
    const g = ({name}) => name;
    // (g*f)(x) 합성 함수
    const gf = id => Promise.resolve(id).then(f).then(g).catch(err => err);
    
    // case 1
    gf(2).then(result => console.log(result)); // 출력 : bb
    
    // case 2
    users.pop();
    users.pop();
    gf(2).then(result => console.log(result)); // 출력 : not found

    함수 f에서, 원하는 user를 찾지 못하면 reject 프로미스를 반환합니다. 그리고 합성함수 gf를 promise를 이용해 합성합니다. (catch를 이용해서 에러 핸들링도 해줍니다.) 그 결과, 안전하게 결과를 받는 것을 확인할 수 있습니다.

     

     

    참고

    마이클 포거스, 『함수형 자바스크립트 : 새롭고 올바른 자바스크립트 프로그래밍 기법』, 한빛미디어(2014).

    유인동, 인프런 - 함수형 프로그래밍과 JavaScript ES6+

    댓글

Designed by black7375.