자바스크립트의 함수형 프로그래밍 컨셉


원문
Thomas Collardeau, https://medium.com/@collardeau/intro-to-functional-programming-concepts-in-javascript-b0650773139c


대부분의 함수형 프로그래밍은 보통 함수들을 조립하고, 데이터들을 예측하기 쉽게 다루는 그런 것들을 의미한다.

간단한 함수들이 깔끔하게 합쳐지면서 복잡도는 증가할지라도, 신뢰할 수 있는 함수를 만들어낸다.

이 글은 함수형 프로그래밍 컨셉에 대한 여러 파트 중의 첫 번째 파트로, 람다(Ramda)라이브러리, composition, pointfree style 그리고 functor의 컨셉을 소개하려 한다.


## Getting Our Feet Wet

우선 "Doc Emmett Brown"이라는 사람이 있다 생각하고, 우리는 이 사람의 First name의 첫 이니셜을 얻어내는 일을 할 것인데, 결과적으로 단순하게 "E"를 만들어내면 된다.

아마 여러분은 하나의 책임만을 갖는 작은 단위의 함수에 익숙할 것이다. ES6 문법으로 구현을 시작해보자.

const getFirstName = person => person.split(" ")[1];
const getFirstLetter = string => string[0];   // first letter

const getFirstInitial = person => {
   return getFirstLetter(getFirstName(person));   // nested function!
}

// try it
getFirstInitial("Doc Emmett Brown");  // "E"

메인 함수는 getFirstInitial이다. 여기에서 functional programming(함수 반환)과 composition(중첩 함수)의 기본적인 측면을 볼 수 있다. 우리는 두 개의 작은 함수들(테스트와 디버깅이 쉬운)로 조금 더 복잡한 함수를 만들었으며, 이는 바람직한 방식이라고 확신한다.

역:
본문에서 functional programming - 함수 반환에 관련하여, 다음과 같이 표현하였다:
—> essential aspect of functional programming (we return a function)

그러나 실제 getFirstInitial메인 함수에서 보면 결국 중첩된 함수 호출의 결과를 반환할 뿐이지, 함수 자체를 반환하지는 않는다.
개인적으로 이 형태는 단순한 반환문일 뿐이지, 함수형 프로그래밍의 개념은 아니라고 생각한다.
return getFirstLetter(getFirstName(person));


아직 위의 중첩 함수가 그리 좋아 보이지 않는다는 것에(비록 ES6는 조금 더 괜찮게 만들 수 있지만) 동의할 것이다. 그렇지만, 이는 극히 일부분이기도 하고 일할 때 별로 볼 일도 없기도 하고, 나름 로직을 간단하게 보여주기도 한다.

return getFirstLetter(getFirstName(person));

위처럼 중첩시켜 호출하는 것은 완전히 이해하지 못할 정도는 아니지만, 복잡도는 우리의 적이니 싹부터 잘라버리도록 하자.


## In Comes Ramda

우리는 Ramda 라이브러리를 임포팅해서 함수적인 접근을 더 잘할 수 있다. 우리의 메인함수인 getFirstInitial을 변경해보도록 하자. 그리고 이름도 명사형인 firstInitial로 바꿀 것이다. (함수형 프로그래밍을 하는 사람들("Functioneers")은 데이터를 동사가 아닌 명사로 생각하길 좋아한다.)

const pipe = require("ramda").pipe;   // node or webpack/browserify

const firstInitial = pipe(getFirstName, getFirstLetter);

람다의 pipe는 함수들을 좀 더 직관적으로 중첩, 끼워넣기를 할 수 있게 해준다. 어떤 데이터든지 firstInitial에 들어가면, 첫 번째로 getFirstName을 거칠 것이고, 그 결과가 getFirstLetter를 거쳐서, 우리에게 최종 결과로 주어질 것이다!

이 코드는 이해하기에도 더 쉽고, 이런 방식으로 유지 보수하기도 쉽다. 이제 우리는 함수들을 쉽게 중첩하거나 composing할 수 있다. 사실, 람다에서 pipe와 정확히 같은 기능을 가지고 있는 compose함수도 있는데, 인자들이 함수들을 통과하는 순서가 반대이다.

compose(getFirstLetter, getFirstName);

나(저자)는 코드를 일반 영어처럼 읽는 것(왼쪽 -> 오른쪽)을 좋아하기 때문에 pipe를 더 선호한다. 이 외에도 람다는 더 많은 기능이 있다. (다음 파트에서 볼 수 있다.)


## PointFree Style

pipe, compose와 함께 또 다른 흥미로운 것이 있다. 우리의 첫 함수 getFirstInitial에서 우리는 person대한 참조를 함수 내부에 가지고 있다는 것이다.
아래를 보자.

// Standalone ES6 composition mentions "person" twice
const getFirstInitial = person => getFirstLetter(getFirstName(person))

// vs. Ramda's pipe composition
const firstInitial = pipe(getFirstName, getFirstLetter);

람다의 함수 정의는 저런 귀찮은 데이터 참조로부터 벗어난다. 보기에(이해하기에도) 좋지 않는가? 이를 Pointfree 스타일이라 부른다.

우리는 "person"이라는 함수의 입력값에 얽매일 필요가 없다(나중에 이름을 수정할 필요도 없다). 이렇게 하면 함수를 더욱 포괄적으로 만들 수 있다: 우리는 꼭 사람 이름일 필요 없이, 형식에 맞는 문자열이라면 함수에 넘길 수 있다.


## More Syntax Goodness

다른 함수를 이런 함수 조립 중간에 쉽게 끼워 넣을 수 있는데, 우리가 첫 번째 이름을 잘 뽑아내고 있는지 확인하기 위해 로그 함수를 끼워 넣어 보자.

pipe(getFirstName, log, getFirstLetter);

가장 처음 중첩 함수였다면 어땠을지 생각해보자.

return getFirstLetter(log(getFirstName(person)));

못생겼다. 내가 괄호는 맞게 썼나?


## The Real World

입력값이 null인 경우, 즉 firstInitial(null)은 어떤 일이 발생할까? 깨진다! 이는 getFirstName부터 시작되는데, String.split이 수행되면서 null 값을 확인하고 에러를 발생시킬 것이다.

하지만 괜찮다, 별일 아니다, 우리는 null 체크 로직을 함수 안에 추가해서 에러가 발생하지 않도록 피하면 된다.

const getFirstName = name =>  { 
  if(name === null) return null;  // new
  return name.split(" ")[1]; 
}

const getFirstLetter = word => { 
  if(word === null) return null;  // new
  return word[0]; 
}

이제 우리가 null 값을 입력해서 첫번째 이니셜을 얻고자 하면 null 값이 다시 반환될 것이고, 이건 꽤 합리적이다. 그리고 더욱 중요한 것은, 애플리케이션이 죽지 않는다.

그러나 이제 null뿐만 아니라 undefined도 체크하라는 새로운 요구사항이 뒤따른다! 우리는 뒤로 돌아가서 모든 null 체크 로직을 변경해야 한다. 이것은 일이고, 이상적이지 않다. 5분마다 우리의 구현이 엉망진창으로 변하길 원치 않는다.


## Do it, Maybe

유효성 체크를 어떻게 외부로 빼내서 독립시킬 수 있을까? 어떻게 한 곳에서 관리할 수 있을까? 우리가 본 코드들의 패턴은 거의 항상 같다. 값이 null이면 return null인 것 처럼.

if (name === null) return null;

여기서 중요한 추상화는 체크를 수행할 수 있는 컨테이너에 우리의 데이터를 감싸는 것에 있다. 그러고 나서 데이터를 바로 보내는 대신 컨테이너를 통해 보내도록 한다.

약간 서둘렀지만, 이제 위 컨테이너를 만들어보도록 하자.

자바스크립트에는 객체들이 있으니 사용해보도록 하자. 컨테이너(or wrapper)객체는 우리의 실제 값(person string 또는 null)을 저장할 뿐만 아니라, 그 값을 확인하고 새로운 값으로 감싸서 반환해주는 메서드를 가지고 있을 것이다!

이 컨테이너의 메서드를 fmap이라고 부를 것이다. 이는 자료구조의 안에 있는 원시값들을 새로운 구조로 만들어서 반환한다는 점에서 Array.map을 연상시킨다.

우리는 이 컨테이너들을 제공하는 컨테이너 팩토리를 만들 것이다. 가공된 값은 존재할 수도, 존재하지 않을 수도 있기 때문에, 이 팩토리를 Maybe라 부를 것이다.

코드를 통해 컨테이너의 컨셉을 보자.

const Maybe = val => ({
    val,
    fmap(f) {
        if (this.val === null) return Maybe(null);
        return Maybe(f(this.val));
    }
});

// lets create some containers with our Maybe factory
let user = Maybe("Slacker George McFly");
let noUser = Maybe(null);

console.log(user.val);    // "Slacker George McFly"
console.log(noUser.val);  // null

우리는 Maybe팩토리를 만들기 위해 ES6의 문법들을 사용하고 있는데, 이 팩토리는 값을 받아서 해당 값을 val프로퍼티로 가지고 있으면서 동시에 fmap메서드를 가지고 있는 새로운 객체를 반환한다.
fmapf라는 함수를 파라미터로 받아서, this.val을 체크하고 계산해서 새로운 값으로 감싸고 반환한다! ES6에 익숙하지 않다면, 여기 ES5 코드를 참고하자.

function Maybe(val){
  return {
    val: val,
    fmap: function(f){
      if(this.val === null) {
        return Maybe(null);
      }
      return Maybe(f(this.val));
    }
  }
}

재밌지 않나? Maybe는 실제로 하나의 functor를 반환하는데, 이는 대략 "mapped over"가 될 수 있는 자료 구조이다. "mapped over"는 단순하게 여러분이 원시 값을 기반으로 하여 만들고 있는(나타내고 있는) 새로운 값을 의미한다.


## Fixing the building blocks

이제 우리는 getFirstName, getFirstLetter가 일반 문자열(null값이 될 수도 있는!) 대신 사용할 Maybe 컨테이너를 다룰 수 있도록 변경해야 할 필요가 있다. 수정해보도록 하자.

const getFirstName = maybeName => maybeName.fmap(name => name.split(" ")[1]);
const getFirstLetter = maybeString => maybeString.fmap(string => string[0]);

이제 null 체크를 Maybe 컨테이너에 위임하였다! 훌륭한 점은 Maybe가 충분히 (우리가 만든) 우리의 인터페이스를 제공할 수 있어서 내부적으로 함께 쓰일 수 있다는 것이다. 이제 fmap메서드에 우리가 원하는 함수를 넘기기만 하면 알아서 내부의 값을 가지고 수행해 줄 것이다. 이렇게만 한다면 이 앱은 이제 죽지 않을 것이다.

undefined 체크를 포함한 우리의 코드를 정리해보자.

const Maybe = val => ({
  val,
  fmap(f) {
    if(this.val === null || this.val === undefined) return Maybe(null);
    return Maybe(f(this.val));
  }
});

const getFirstName = maybeName => maybeName.fmap(name => name.split(" ")[1]);
const getFirstLetter = maybeString => maybeString.fmap(string => string[0]);

const firstInitial = R.pipe(getFirstName, getFirstLetter);

// let's try this out 
const user = Maybe("Bully Biff Tannen");
const initial = firstInitial(user);
document.write(initial.val, "<br />");   // "B"

const noUser = Maybe(null);
const noInitial = firstInitial(noUser);
document.write(noInitial.val);           // null

위 코드들을 맘대로 포크해서 수정해도 괜찮다. http://codepen.io/collardeau/pen/JYpLEY

이제 우리는 Maybe라는 functor를 통해 더욱 쉽고 확실하게 코드를 구성할 수 있다.

part2를 주목하라. currying에 대해 배울 것이다. : Part2 link