Skip to content

Latest commit

 

History

History
229 lines (173 loc) · 9.45 KB

File metadata and controls

229 lines (173 loc) · 9.45 KB

4. 코드를 컬렉션으로 다루기

목차

  • pipe, go
  • match
  • 함수를 값으로 다루면서 원하는 시점에 평가하기
  • or, and

앞서 컬렉션을 다루는 함수들을 살펴보았습니다. 그런데 '코드를 컬렉션으로 다루기'란 무엇을 말하는 것일까요. 보통은 데이터베이스의 정보나, 앱의 상태 정보, 돔 엘리먼트 등을 컬렉션으로 다룹니다. 함수형 프로그래밍에서는 이것들 외에도 자주 사용하는 값이 하나 더 있습니다. 바로 함수입니다.

pipe, go

함수형 프로그래밍에서는 함수도 값으로 다룹니다. 다음은 함수를 값으로 다루는 함수 중 하나인 pipe입니다.

const { pipe } = Functional;

var f1 = pipe(
  _ => 1,
  a => a + 10,
  a => a + 100,
  a => a + 1000,
  console.log);
//1111

f1();

첫 번째 함수의 결과는 두 번째 함수의 인자로 전달됩니다. 두 번째 함수의 결과는 세 번째 함수의 인자로 전달됩니다. 반복되어 마지막 console.log에게 전달됩니다. 함수를 원소로 가진 컬렉션을 만들어서 pipe를 통해 함수를 합성했습니다.

const { go } = Functional;

go(2,
  a => a + 20,
  a => a + 200,
  a => a + 2000,
  console.log);
// 2222

go는 자바스크립트의 매력과 잘 어울립니다. 배열에 여러가지 타입의 값이 들어갈 수 있는 것을 활용합니다. go의 첫 번째 인자는 두 번째 인자인 함수를 적용할 인자입니다. 두 번째 함수의 결과는 세 번째 함수의 인자로 전달됩니다. 반복되어 마지막 console.log에게 전달됩니다.

gopipe 함수는 코드 라인들을 컬렉션(값)으로 다루면서 원하는 시점에 함수를 실행하면서 원하는 결과를 만들어가는 함수입니다. 이와 같이 함수를 값으로 다루는 함수를 고차 함수라고 합니다. 고차 함수는 함수를 리턴하거나, 함수를 실행하는 함수입니다. 앞서 확인했었던 map, reduce, find 등도 고차 함수 입니다.

match

match 함수는 복잡한 분기를 다루는 함수입니다. 함수들과 값들을 컬렉션으로 다루어서 분기를 구현합니다.

const { match } = Functional;

const a = 2;

const b = match (a)
  .case(1) (_=> '1이군요!')
  .case(2) (_=> '2네요?')
  .else    (_=> '1도 2도 아니군요.');

console.log(b);
// 2네요?

match의 괄호에 함수 컬렉션을 넣으면 pipe로 동작합니다.

const f2 = a =>
  match (a)
    .case(1) (
      a => a + 10,
      a => a + 100,
      a => a + 1000)
    .case(2) (
      a => a + 20,
      a => a + 200,
      a => a + 2000)
    .case(a => a < 5) (
      _=> '1도 2도 아니지만 5보다는 작군요.')
    .else (
      _=> '1도 2도 아니군요.');

console.log( f2(1) );
// 1111
console.log( f2(2) );
// 2222
console.log( f2(4) );
// '1도 2도 아니지만 5보다는 작군요.'
console.log( f2(10) );
// '1도 2도 아니군요.'

case는 내부적으로 isMatch처럼 동작합니다. isMatch는 두 번째 인자 값이 대상이며, 첫 번째 인자 값이 조건입니다. 객체라면 동일한 값이 내부에 있는지를 검사하고, 원시 값의 경우는 == 으로 비교합니다. 조건이 함수라면 함수를 실행하여 결과를 확인합니다. === 비교를 원하면 a => a === b 같은 함수 전달을 통해 선택할 수 있습니다.

const { isMatch } = Functional;

console.log( isMatch({ a: 1 }, { a: 1, b: 2 }) ); // true
console.log( isMatch({ a: 1, b: 2 }, { a: 1 }) ); // false
console.log( isMatch([2], [1, 2, 3]) ); // true
console.log( isMatch([1, 2, 3], [2]) ); // false
console.log( isMatch(5, 5) ); // true
console.log( isMatch("a", "a") ); // true

console.log( isMatch({ a: 1, b: 3 }, { a: 1, b: 2 }) ); // false
console.log( isMatch(5, 50) ); // false
console.log( isMatch("a", "aaa") ); // false

console.log( isMatch(b => b.includes("a"), "aaa") ); // true

match ({ a: 1, b: 2 })
  .case({ a: 2 }) (_=> console.log('---- 1'))
  .case({ b: 3 }) (_=> console.log('---- 2'))
  .case([4])      (_=> console.log('---- 3'))
  .case(4)        (_=> console.log('---- 4'))
  .case({ b: 2 }) (_=> console.log('---- 5'))
  .else           (_=> console.log('---- 6'));
  // ---- 5

match.case를 바로 실행하여 함수 리턴형으로 사용할 수 있습니다.

const mf = match
  .case({ a: 2 }) (_=> console.log('---- 1'))
  .case({ b: 3 }) (_=> console.log('---- 2'))
  .case([4])      (_=> console.log('---- 3'))
  .case(5)        (_=> console.log('---- 4'))
  .case({ b: 2 }) (_=> console.log('---- 5'))
  .else           (_=> console.log('---- 6'));

mf({ a: 2 });        // ---- 1
mf({ b: 3 });        // ---- 2
mf([4]);             // ---- 3
mf(5);               // ---- 4
mf({ b: 2, c: 10 }); // ---- 5
mf(10);              // ---- 6

함수를 값으로 다루면서 원하는 시점에 평가하기

순수 함수는 언제 평가해도 동일한 결과를 만듭니다. 이러한 법칙을 이용하여 함수형 프로그래밍에서는 비동기/동시성/병렬성/지연성 등을 훌륭하게 다룹니다. 고차 함수를 이용하여 함수 평가를 원하는 만큼 미루거나, 동시적으로 평가를 하거나, 원하는 순서대로 평가를 하면서 정확한 로직을 구현해갑니다.

함수를 원하는 시점에 평가하기는 함수형 프로그래밍의 최적화 기법입니다. 다양한 로직을 사람이 이해하고 외우기 좋은 많은 함수들로 만들고, 각 상황에 최적화된 함수를 선택하는 것이 함수형 프로그래밍이 가진 최적화 전략입니다. 함수들을 인자로 다루면서 목적에 따라 즉시 평가하기도, 미뤄서 평가하기도, 동시에 평가하기도, 필요 없는 경우엔 함수를 평가하지 않게도 합니다.

데이터와 도메인에 맞는 보조 함수를 만든 후 구현하고자 하는 로직에 최적화된 함수를 선택하는 것으로 자원 사용과 연산을 줄입니다. 함수를 값으로 다루는 것은 함수형 프로그래밍에서 가장 중요한 개념입니다.

[3. 컬렉션 중심 프로그래밍]에서 설명했던 some 함수는 컬렉션에 map을 적용한 다음, 하나라도 true로 평가될 수 있는 값이 있는지 확인하는 함수입니다. 아래와 같이 구현할 수 있습니다.

const { map, log } = Functional;

function some1(f, coll) {
  const bools = map(f, coll);
  for (const bool of bools) if (bool) return true;
  return false;
}

log( some1(a => a > 15, [10, 20, 30, 40]) );
// true

위 코드는 4개의 숫자 값을 모두 불리언으로 변경한 다음, true가 있는지 확인하고 있습니다. 위 코드는 정상적으로 동작하지만 f의 평가시점이 최적화 되어있지 않아 f가 4번 실행됩니다.

some1과 동일한 로직을 최적화하면 아래와 같습니다.

function some2(f, coll) {
  for (const val of coll) if (f(val)) return true;
  return false;
}

log( some2(a => a > 15, [10, 20, 30, 40]) );
// true

some2f를 평가하는 시점을 true가 있는지를 알아보는 곳으로 좀 더 미뤘습니다. 위 상황에서 some2는 2개의 숫자 값만 불리언으로 변경하게 됩니다. f를 두 번만 실행합니다.

some1some2는 동일한 일을 하지만 some2는 함수를 가장 알맞는 시점에 평가하여 자원 사용과 연산 비용을 줄였습니다.

[3. 컬렉션 중심 프로그래밍]에서 구현했던 some 역시 some2와 같이 평가시점을 최적화하도록 하면서, 함수조합을 통해 더욱 간결하게 구현되어있습니다.

const some = map(a => a !== undefined, find);

or, and

orand||&&의 함수 버전이며 코드 대신 함수를 받습니다. ||&&는 평가를 지연하여 효율적으로 처리합니다. 함수로 구현된 orand 역시 동일하게 효율적으로 동작합니다.

const { or, and } = Functional;

or(
  _=> null,  // (평가)
  _=> false, // (평가)
  _=> 10     // (평가)
)(); // 10

or(
  _=> null,  // (평가)
  _=> true,  // (평가)
  _=> 10     // (X)
)(); // true

and(
  _=> null,  // (평가)
  _=> 10,    // (X)
  _=> false, // (X)
  _=> 12     // (X)
)(); // null

and(
  _=> true,  // (평가)
  _=> 10,    // (평가)
  _=> false, // (평가)
  _=> 12     // (X)
)(); // false

and(
  _=> true,  // (평가)
  _=> 10,    // (평가)
  _=> 11,    // (평가)
  _=> 12     // (평가)
)(); // 12

'함수를 값으로 다루면서 원하는 시점에 평가하기'는 함수형 프로그래밍에서 가장 중요한 개념이자 본질적인 부분입니다. 이 글에서 살펴본 some은 함수만으로 만들어진 함수이며, orand 예제에도 함수만 등장합니다. 함수형 프로그래밍은 이와 같이 함수를 값으로 사용하며, 함수를 언어 그 자체로 바라봅니다. 추상화의 단위로 함수를 사용하며, 작은 문제를 해결한 함수들을 조합하여 복잡한 문제를 해결해갑니다. 이때 함수 평가의 시점을 최적화하여 우아함과 성능이라는 두마리 토끼를 노립니다.

다음 글에서는 '함수를 값으로 다루면서 원하는 시점에 평가하기'를 가지고 실무에서 굉장히 중요한 문제인 비동기/동시성/병렬성 문제를 해결해볼 것입니다. 이것은 함수형 프로그래밍의 특기이기도 합니다.