배열과 함수의 응용

2017년 03월 22일


lift

lift는 함수를 이용해 새로운 함수를 파생시키는 고차함수입니다. 값을 받아 값을 반환하는 함수를 배열을 받아 배열을 반환하도록 바꿔줍니다.

예시

두 개의 수를 받아 문자열을 반환하는 함수와 lift를 이용해 두 개의 수의 배열을 받아 문자열의 배열을 반환하는 함수를 파생시킵니다.

import * as R from 'ramda';

const f = (x, y) => `${x} * ${y} = ${x * y}`;
const g = R.lift(f);

const xs = R.range(1, 10);
const ys = R.range(1, 10);
const result = g(xs, ys); // g(xs, xs)도 괜찮습니다.
// [
//   '1 * 1 = 1',
//   '1 * 2 = 2',
//   '1 * 3 = 3',
//   ...
//   '9 * 7 = 63',
//   '9 * 8 = 72',
//   '9 * 9 = 81'
// ]

파생된 함수는 인자로 받은 배열에서 원소를 뽑아 기존의 함수에 전달합니다. 이 때 원소를 뽑는 방법은 배열의 원소로 만들 수 있는 모든 경우의 수를 고려합니다.

명령형과 비교

위의 동작을 명령형으로 흉내내는 방법으로 루프의 중첩을 사용하는 방법이 있습니다.

import * as R from 'ramda';

const f  = (x, y) => `${x} * ${y} = ${x * y}`;

const xs = R.range(1, 10);
const ys = R.range(1, 10);

let result = [];

for (const x of xs)
  for (const y of ys)
    result.push(f(x, y));

기능이 확장되서 3개의 곱을 표현하는 구구단이 필요한 상황을 가정해봅시다.

명령형 방식에선 함수를 손보고 루프를 하나 더 중첩해서 문제를 해결할 수 있습니다.

import * as R from 'ramda';

const f  = (x, y, z) => `${x} * ${y} * ${z} = ${x * y * z}`;

const xs = R.range(1, 10);
const ys = R.range(1, 10);
const zs = R.range(1, 10);

let result = [];

for (const x of xs)
  for (const y of ys)
    for (const z of zs)
      result.push(f(x, y, z));

반면에 lift는 함수만 수정하면 됩니다.

import * as R from 'ramda';

const xs = R.range(1, 10);

const f = (x, y) => `${x} * ${y} = ${x * y}`;
const F = R.lift(f);
F(xs, xs).length;
// 81 (9 * 2)

const g = (x, y, z) => `${x} * ${y} * ${z} = ${x * y * z}`;
const G = R.lift(g);
G(xs, xs, xs).length; 
// 729 (9 * 9 * 9)

lift는 확장성 측면에서 유리합니다. 동작을 함수로 잘 엮어놓으면 기능을 변경하거나 확장할 때 손봐야 하는 부분이 적습니다.

import * as R from 'ramda';

const xs = R.range(1, 10);

const h = (w, x, y, z) => `${w} * ${x} * ${y} * ${z} = ${w * x * y * z}`;
const H = R.lift(h);
H(xs, xs, xs, xs).length; 
// 6561 (9 * 9 * 9 * 9)

루프의 중첩이라는 구현에서 벗어나, 배열에 대해 lift된 함수는 경우의 수를 조합한다는 성질에 집중합니다.