본문 바로가기

책 리뷰/함수형 자바스크립트

함수형 자바스크립트 [4장 - (1)]

※ 함수형 자바스크립트 라는 책을 한 장씩 리뷰해보도록 하겠습니다. 4장은 내용이 길어서 두개의 포스팅으로 나눠서 작성하겠습니다.

 

Chapter 4. 재사용 가능한, 모듈적인 코드로

 

"잘 작동하는 복잡한 시스템은 십중팔구 잘 작동했던 단순한 시스템에서 진화한 경우가 많다"

 

모듈성 이란, 프로그램을 더 작고 독립적인 부분으로 나눌 수 있는 정도를 뜻합니다. 모듈적 프로그램은 자신을 구성하는 부속들로부터 자신의 의미를 도출할 수 있다는 특별한 점이 있습니다. 부속 (하위 프로그램) 은 다른 시스템에 그대로 혹은 더 잘게 쪼개서 통합할 수 있는 재사용 가능 컴포넌트이며 개발자의 생산성 및 유지보수, 가독성을 향상시키는데 큰 도움이 됩니다.

 

4.1 메서드 체인 대 함수 파이프 라인

 

3장에서 본 메서드 체인과 별개로 파이프라이닝 은 함수를 연결하는 또 다른 기법입니다. 함수는 입력과 출력을 기준으로 서술하는 것이 유용하며, 아래와 같은 하스켈 언어 표기법을 많이 씁니다.

 

  • <function-name>               ::                          <Inputs*>         ->       <Output>
  •        함수명       형식을 알려주는 연산자     0개 이상의 입력형식          단일 출력 형식

예를 들어 문자열을 받아 불리언을 반환하는 isEmpty 함수는 아래와 같습니다.

- isEmpty :: String -> Boolean  //하스켈 함수 표기법
- const isEmpty = s => !s || !s.trim();  //람다 표현식

함수 체이닝과 파이프라이닝을 이해하려면 함수를 형식 간의 매핑으로 봐야합니다.

  • 메서드를 체이닝 (단단한 결합, 제한된 표현성)
  • 함수 파이프라인을 배열 (느슨한 결합, 유연성)

4.1.1 메서드를 여럿 체이닝

 

먼저 로대시를 활용한 메서드 체이닝 예시를 보겠습니다.

_.chain(names)
  .filter(isValid)
  .map(s => s.replace(/_/, ' '))
  .uniq()
  .map(_.startCase)
  .sort()
  .value();

명령형 코드에 비해 분명 구조적으로 향상됐고 가독성도 좋아졌지만, 자신을 소유한 객체에 매여있어 체인에서 실행 가능한 메서드 수가 줄고 코드의 표현성에도 제약이 있습니다. 예시에선 로대시를 사용하고있기때문에, 해당 라이브러리가 제공하는 연산외의 다른 라이브러리 함수를 연결하기는 쉽지 않습니다.

 

4.1.2 함수를 파이프라인에 나열

 

파이프라인 이란 한 함수의 출력이 다음 함수의 입력이 되게끔 느슨하게 배열한, 방향성 함수 순차열입니다.

- 입력 A형 --> (f :: A -> B) --> B형 --> (g :: B -> C) --> C형 출력

이러한 형태는 객체지향 디자인 패턴중 '파이프 및 필터'와 동등한 패턴입니다. 객체 메서드를 통해 함수들을 단단히 결합하는 체이닝과 달리 파이프라인은 함수 입출력을 서로 연결 지어 느슨하게 결합된 컴포넌트를 만듭니다.

 

4.2 함수 호환 요건

 

함수형 프로그래밍에선 파이프라인이 프로그램을 구축하는 유일한 수단이며, 각 함수는 두 가지 측면에서 입력과 출력이 서로 호환되어야 합니다.

  • 형식 : 한 함수의 반환 형식과 수신 함수의 인수 형식이 일치
  • 항수 : 수신 함수는 앞 단계 함수가 반환한 값을 처리하기 위해 적어도 하나 이상의 매개변수 선언

4.2.1 형식이 호환되는 함수

 

함수 파이프라인을 설계할 때에는 한 함수가 반환하는 것과 다른 함수가 받는 것이 반드시 호환되어야 합니다.

자바스크립트는 동적 파견 체제때문에 형식과 무관하게 객체에서 속성과 메서드를 가져올 수 있는 유연한 구조이지만, 함수가 어떤 형식의 값을 기대하는지 명확히 정의하면 프로그램을 이해하는데 더 쉬워집니다.

다시말해서, 함수 f 와 g 가 있고 f 의 출력 형식과 g 의 입력 형식이 동등하면 두 함수의 형식은 호환됩니다.

 

4.2.2 함수와 항수 : 튜플

 

항수란 함수가 받는 인수의 개수이며 함수의 길이 라고도 합니다. 인수를 하나만 받는 함수는 서너 개 받는 함수보다 훨씬 단순하다고 할 수 있으며, 최대한 함수의 인수를 적게 해야합니다. 하지만 이런 단항 함수는 얻기 쉽지 않은데, 이 때 함수형 언어에서 지원하는 튜플 이라는 자료구조를 사용할 수 있습니다.

튜플은 유한 원소를 지닌 정렬된 리스트로, 형식이 다른 원소를 한데 묶어 다른 함수에 건네주는 일이 가능한 불변성 자료구조입니다. 객체 리터럴이나 배열 같은 임의 형식으로 반환하는 방법보다 튜플이 유리한 부분은 아래와 같습니다.

  • 불변성 : 튜플은 한번 만들어지면 내용을 바꾸지 못함
  • 임의 형식의 생성 방지 : 튜플은 전혀 무관한 값을 서로 연관짓기 가능.
  • 이형 배열의 생성 방지 : 배열은 동일한 형식의 객체를 담는 자료구조이므로 형식이 다른 원소가 배열에 들어가는                                   것을 방지

튜플을 사용하는 간단한 예시입니다.

const Status = Tuble(Boolean, String);

// trim :: String -> String
const trim = str => str.replace(/^\$*|\s*$/g, '');

// normalize :: String -> String
const normalize = str => str.replace(/\-/g, '');

// isValid :: String -> Status
const isValid = function(str) {
  if (str.length === 0) {
    return new Status(false, '잘못된 입력입니다');
  } else {
    return new Status(true, '성공');
  }
}

 isValid(normalize(trim('444-44-4444'))); // (true, '성공');

튜플은 일급 객체로 만들어두면 편리하며, ES6 부터 지원하는 해체 할당 과 조합하면 튜플 값을 변수로 깔끔하게 매핑 가능합니다.

const StringPair = Tuple(String, String);
const name = new StringPair('Barkley', 'Rosser');

[first, last] = name.values(); // 해체 할당
first; // -> 'Barkley'
last; // -> 'Rosser'

const fullname = new StringPair('J', 'Barkley', 'Rosser'); // 항수가 맞지 않아 에러

튜플로 함수 항수를 줄일 수 있지만, 이보다 더 나은 방안인 항수를 추상하는 동시에 모듈성, 재사용성을 높이는 함수 커링을 보도록 하겠습니다.

 

4.3 커리된 함수를 평가

 

자바스크립트에선 비커리된 일반 함수를 호출할 때 인수가 모자라도 그 값에 undefined 를 세팅해 이상없이 실행됩니다. 이런 자바스크립트 특성 탓에 언어 자체에서 커링을 기본 지원하지는 않지만, 커링이란 함수에 일부 인수만 넣어 호출하면 함수가 실행되는게 아닌 나머지 인수를 받는 새로운 함수를 반환하는 것입니다.

f(a,b,c) 라는 함수가 있을때,
f(a,b,c) 호출 -> 결과 반환
f(a,b) 호출 -> f(c) 반환
f(a) 호출 -> f(b,c) 반환

자바스크립트는 자동으로 함수를 커리할 수 없기때문에 수동 커리도 가능하지만, 인수가 늘어나면 자동으로 처리할 방법이 필요합니다. 이러한 경우 람다JS 라는 또 다른 함수형 라이브러리의 R.curry 함수를 사용해 인수 개수에 상관없이 순수 함수형 언어의 자동 커링 장치를 모방할 수 있습니다.

 

이러한 커링은 아래와 같은 두가지 경우에 사용합니다.

  • 함수 팩토리를 모방
  • 재사용 가능한 모듈적 함수 템플릿을 구현

4.3.1 함수 팩토리를 모방

 

함수형 프로그래밍의 커링을 사용해 보관된 학생 객체를 각각 저장소와 배열로 조회하는 함수를 작성해보겠습니다.

// fetchStudentFromDb :: DB -> (String -> Student)
const fetchStudentFromDb = R.curry(function(db, ssn) { // db 에서 조회
  return find(db, ssn);
});

// fetchStudentFromArray :: Array -> (String -> Student)
const fetchStudentFromArray = R.curry(function(arr, ssn) { // 배열에서 조회
  return arr[ssn];
});

// 위처럼 커리를 해놨기때문에, 아래 findStudent 메서드를 평가하는 부분과 함수 정의 부분을 떼어놓을 수 있다.
const findStudent = useDb ? fetchStudentFromDb(db) : fetchStudentFromArray(arr);

findStudent('444-44-4444');

 

4.3.2 재사용 가능한 함수 템플릿 구현

 

예시는 생략하고, 이러한 커링 기법을 사용하면 재사용성이 향상되며 다인수 함수를 단항 함수로 바꾸는 중요한 의의를 두고 있습니다. 왜 중요하냐 하면 위에서 언급했던 것처럼 항수가 적어질수록 해당 프로그램을 파악하기 쉬워집니다.

 

4.4 부분 적용과 매개변수 바인딩

 

부분 적용은 함수의 일부 매개변수 값을 처음부터 고정시켜 항수가 더 작은 함수를 생성하는 기법입니다. 위에서 봤던 커링은 매 번 인자를 1개로 고정한 연속적인 부분 적용이라고도 볼 수 있습니다.

 

커링과 부분 적용의 주된 차이점은 아래와 같습니다.

  • 커링은 부분 호출할 때마다 단항 함수를 중첩 생성하며, 내부적으로는 이들을 단계별로 합성하여 최종 결과를 냄. 개발자가 평가 시점과 방법을 변경 가능
  • 부분 적용은 함수 인수를 미리 정의된 값으로 묶은 후 인수가 적은 함수를 새로 만듬. 함수는 클로저에 고정된 매개변수를 갖고 있으며 이미 평가를 마친 상태
// 커링
const curriedFn = function(a) {
  return function(b) {
    return function(c) {
      return a + ', ' + b + ', ' + c;
    };
  };
};

// 부분 적용
const partialAppliedFn = function(a) {
  return function(b, c) {
    return a + ', ' + b + ', ' + c;
  };
};

 

4.4.1 언어의 핵심을 확장

 

부분 적용은 String, Number 같은 핵심 자료형을 확장하여 언어의 표현성을 풍부하게 할 목적으로 사용 가능합니다.

단, 이 경우 플랫폼 업그레이드 시 언어에 추가된 새 메서드와 충돌 가능성이 있어 이식성은 떨어집니다. 아래 예시)

// 처음 N 개의 문자를 얻음
String.prototype.first = _.partial(String.prototype.substring, 0, _);
'Functional Programming'.first(3); // -> 'Fun'

// 성명을 '성, 이름' 형식으로 바꿈
String.prototype.asName = _.partial(String.prototype.replace, /(\w+)\s(\w+)/, '$2, $1');
'Alonzo Church'.asName(); // -> 'Church, Alonzo'

이렇게 부분 적용을 사용할 경우 기존 언어가 갖고있는 메서드와 겹치진 않는지 확인해야합니다.

 

4.4.2 지연된 함수에 바인딩

 

브라우저에서 setTimeout, setInterval 같은 함수의 this 레퍼런스는 전역 컨텍스트인 window 객체를 가리켜야 잘 작동합니다. 이러한 경우엔 부분 적용을 사용할 수 없어, 함수 바인딩을 사용해야 합니다. 아래 예시)

const Scheduler = (function() {
  const delayedFn = _.bind(setTimeout, undefined, _, _);
  
  return {
    delay5 : _.partial(delayedFn, _, 5000),
    delay10 : _.partial(delayedFn, _, 10000),
    delay : _.partial(delayedFn, _, _)
  };
})();

Scheduler.delay5(function() {
  console.log('5초 후 실행');
});

부분 적용, 커링 모두 유익한 기법입니다. 어느 기법을 사용하든지, 함수를 여러 단항 함수들로 몸집을 줄임과 동시에 자신의 스코프 밖에 위치한 객체에 맘대로 접근하지 못하도록 적정한 개수의 인수를 공급하는 효과가 있습니다. 또한 필요한 데이터를 얻는 로직을 분리하면 재사용 가능한 함수로 만들 수 있으며, 함수의 합성을 단순화 하는 장점이 있습니다.

 

4장의 나머지 부분은 다음 포스팅에서 이어가도록 하겠습니다.

 

 

출처 : 함수형 자바스크립트(책)