What the Functional Programming???(feat. Promise) - adelakim5/fe-w4-martian GitHub Wiki

사실 나는 class만 만들어봤지 객체지향 프로그래밍다운 코드를 짜본 적은 없었고, 오히려 함수로 잘게 짜서 조합해나가는게 더 재밌던 그런 나 자신에 대해 어쩌면 함수형에 더 잘 맞는 사람인건가 싶었는데... 이번 미션을 하면서 선언적으로 코드를 짠다는 것이 꽤 낯설다는 것을 깨달았다. (그렇게 끔찍한 혼종이 되어가는 중인듯..)

따라서 이번 미션을 기회로 함수형 프로그래밍에 대해 이해하는 깊이를 조금 더 높이기로 해본다!

목차

  1. 이번 미션에서 내가 시도한 것들
  2. 함수형 프로그래밍은 뭐라고 생각하나?
  3. Promise에 대해 깨달은 것들

이번 미션에서 내가 시도한 것들

구현 과정에서 자질구레한 시도들도 많았지만 일단 나름 괄목(?)할만한 시도들을 추려보았다.

MyPromise

mdn의 Promise를 보고 진또배기스러운 프로미스를 만들어보고 싶었는데, 내 역량으론 현저히 부족한 것 같아서 ㅜㅜ

사실상 then(), catch()만 사용하는 허접 프로미스를 구현했다.

const pipe = (...fns) => (acc) => fns.reduce((a, f) => f(a), acc);
// resolve로 들어온 acc가 then으로 들어오는 callback함수들에 의해 새로운 값으로 쓰여지도록 하는 함수

export default class MyPromise {
  constructor(fn) {
    this.cbList = [];
    this.catchCb = (e) => {
      throw e;
    };
    // fulfilled, rejected
    this.state = "pending";

    setTimeout(() => {
      try {
        this.state = "fulfilled";
        fn(pipe(...this.cbList), this.catchCb); 
      } catch (e) {
        this.state = "rejected";
        this.catchCb(e);
      }
    }, 0);
  }

  then(callback) {
    this.cbList.push(callback);
    return this;
  }

  catch = (callback) => {
    this.catchCb = callback;
    return this;
  };
}
  1. MyPromise를 생성할 때 resolve에 들어온 리턴 값은 pipe함수를 통해 then에서 들어온 콜백함수들이 모인 cbList의 함수들을 순차적으로 거치며 리턴한다.
    • MyPromise의 상태는 fulfilled
  2. 반면 reject로 들어온 값(reason)은 catch함수를 통해 catchCb로 등록하고, 에러가 났을 때 호출한다.
    • MyPromise의 상태는 rejected

function adela

함수형 프로그래밍의 특징 중 하나는 사이드이펙트가 없도록, 순수함수들끼리의 조합으로 언제나 동일한 output을 내는 것인데, 내가 구현한 몇몇 함수들은 다른 함수를 의존하는 식으로 구성되어있었다.

그래서 원래 꿈은 함수를 합성시켜 새 함수를 내는 함수를 짜고, 이걸 토대로 내가 위에서 말한 그런 함수들을 생성해내는 로직을 짜고 싶었는데 아직은 넘나 원대한 꿈인 것 같았고..

그보다 좀 더 작은 단위의 합성함수(?)를 만들어보았다.

const adela = (f, ...fns) => (...args) =>
  args.length
    ? f(
        ...fns.reduce((acc, fn, i) => {
          acc.push(fn(args[i]));
          return acc;
        }, [])
      )
    : f(...fns);

이름을 뭘로 지을까 했는데, 이때 커링을 검색해서 하고 있던 터라.. 커리도 사람 이름인데, 나도 그냥 내 이름으로 함수를 만들었다 ㅋㅋㅋㅋ

저 함수는 다음과 같이 사용할 수 있다.

const findTarget = (elements, capital) => Object.entries(elements).find((item) => item[1].dataset.id === capital);
const getHTMLElements = (className) => document.querySelectorAll(`.${className}`);
const capital = (letter) => letter.toUpperCase();
// ...
const target = adela(findTarget, getHTMLElements, capital)("line__text", letter);

풀어쓰면 아래와 같다.

const elements = getHTMLElements("line__text");
const capital = capital(letter);
const target = findTarget(elements, capital); 
// === const target = adela(findTarget, getHTMLElements, capital)("line__text", letter);

이런식으로 함수 안에서 다른 함수를 다른 인자와 함께 호출해서 만든 인자를 받고 싶을 때 사용하는 함수이다. 조금 복잡한 느낌도 없지 않지만, 시도해보았다!

이렇게 함수 합성에 대한 열망이 불타오르다 보니, 함수형 프로그래밍을 공부하고 싶은 생각이 샘솟기 시작했다.

함수형 프로그래밍은 뭐라고 생각하나?

함수들의 조합? 함수들의 집합?

함수들 갖고 여기다 쓰고 저기다 쓰는데, 사실 인자만 정확하게 가져다 주면 내가 예상하는 결과값을 리턴하는 것?

따라서 함수들이 순수함수여야하고, 전역변수와 같은 참조가 사실상 함수 외부에는 존재하지 않은 채로 프로그래밍을 쿵작쿵작 하는 게 함수형 프로그래밍인것 같다. (그래서 선언적이라고 말하는 것 같고..)

얄팍한 코딩사전의 말을 빌리면,

  • 명령형: 너는 이거하고, 너는 저거하고, 쟤는 저쪽에 있는거하고, 저 친구는 저어어어어기에 있는거 하도록 해
  • 선언형: 이거는 이거야.

이렇다보니, 함수를 "값"으로 바라볼 수 있어야 한다.

값으로 바라본다? 그럼 다른 함수에 "인자"로 넣어줄 수 있다. 물론 리턴도 가능~ 함수 안에서 새로운 함수 만들어내는 것도 가능~

이런 내용들을 알아가다보니.. 내가 감각하고 있던 Promise에 문제가 있었다는 것을 깨닫게 되었다.

Promise에 대해 깨달은 것들

그 전에 모나드라는 개념에 대해 아주 살짝 알게되었다.

유인동님 말씀에 의하면,

  • 함수형 프로그래밍에서도 모나드를 중시하는 프로그래밍과 모나드가 없는 프로그래밍이 있다.
  • 모나드는 메서드를 가진 "값"
  • 좀 더 타입을 중시하는 함수형 프로그래밍에서 사용하는 "값"

함수형 프로그래밍은 함수를 합성하며 프로그래밍을 하게 되는데,

f(g(x)) = f(g(x)) // 수학의 세계에선 무조건 이것만 가능
f(g(x)) = x // 현실 세계에선 가끔 이런 경우 존재

이런 이유로 안전한 함수합성을 위해서 모나드가 존재한다.

모나드?

[1] // 박스 안에 있는 "값"

이게 모나드.

모나드를 합성한다는 말은

const g = a => a + 1;
const f = a => a * a;

const m = [1].map(g).map(f); // 1에 g를 실행하고, f를 실행하는 모나드
//

Promise === future 모나드

const m = [1].map(g).map(f).forEach(a => console.log(a));
const p = Promise.resolve(1).then(g).then(f).then(a => console.log(a));
// Promise.resolve(1) === [1]
// Promise.reolve(1) === 모나드

만약 x값에 문제가 생겼다면?

// example
f(g(x)) = [] // 빈배열이 들어옴

이런 경우, 일반적으로는 그냥 흘러가게 두었다면,

프로미스는 다음과 같이 바꿔주기 위해 생겼다.

f(g(x)) = g(x)
  • g(x)를 했는데 에러 발생! => f는 실행 안하고 그냥 g(x)한것과 똑같게 돌아갈래!
  • 에러가 발생한 경우에는 저렇게 실행시킨다.
const fg = x => Promise.resolve(x).then(g).then(f);

fg(1).then(console.log); // 4 (정상적으로 동작)
fg("에러닷").catch(_ => "에러야...").then(console.log); // 에러 

프로미스를 콜백지옥을 해결하기 위해 정리해서 쓰는 "객체"라고 생각하면 잘못된 생각!

프로미스는 프로미스를 으로 다루기 위해 사용되는 것!

  • 어떤 일이 일어날지 모르는 효과를 감싸놓고 나는 이런 형태의 값이라고 해놓고
  • 비동기적인 상황과 성공과 실패를 값으로 다루는 모나드
  • "값"임

setTimeOut으로 동기적으로 실행시키는 것은 흐름이지 "값"이 아니다.

const delay = (time, val) => new Promise(resolve => setTimeout(() => resolve(a), time));

delay(100, 5).then(console.log);
const a = delay(100, 4); // 값에 할당 가능
if(a instanceof Promise) {}; // a가 무엇인지 확인 가능
if(true) a.then(fn); // 조건에 따라 동작하게 할 수 있음

프로미스를 만들어서 어떤 함수에 전달하거나 어떤 로직과 함께 다룰 수 있기에 의미가 있는 것이다.

const go1 = (a, f) => f(a);

go1(10, console.log); // 10;
go1(delay(100, 5), console.log); // 에러남

go1을 다음과 같이 수정할 수 있다.

const go1 = (a, f) => a instanceof Promsie ? a.then(f) : f(a); // a가 프로미스이면 then(f)를, 그렇지 않으면 f(a)를 반환 

참고자료

함수형 프로그래밍과 ES6+