배열과 객체 다루기

2017년 03월 17일


배열 다루기 2

지금까지 소개한 함수들을 살펴보면 Array의 메서드와 비슷한 함수들이 많습니다.

굳이 Array의 메서드를 두고 Ramda를 사용할 필요가 있을까 싶지만, Ramda를 이용하는 함수형 스타일이 대부분 경우에 낫습니다.

map

Array.map에 해당하는 함수입니다. Array.map과는 다르게 콜백 함수에 index와 array가 전달되지 않습니다.

import * as R from 'ramda';

const xs = R.range(0, 3);
R.map((x) => x * 10, xs); // [0, 10, 20];

기존의 Array.map은 체이닝을 할 수 있었습니다. 여러개로 나눠진 동작을 체이닝을 통해 조합할 수 있었습니다.

하지만 Ramda의 map을 사용하면 함수 호출로 형태가 바뀌었기 때문에 더이상 체이닝을 할 수 없습니다.

import * as R from 'ramda';

const xs = R.range(0, 3);
const ys = R.range(3, 6);
// R.inc = (x) => x + 1;

xs.map(R.inc).map(R.inc); // [2, 3, 4]
ys.map(R.inc).map(R.inc); // [5, 6, 7]

R.map(R.inc, R.map(R.inc, xs)); // [2, 3, 4]
R.map(R.inc, R.map(R.inc, ys)); // [5, 6, 7]

이러한 문제는 커링과 함수합성을 이용해서 해결할 수 있습니다.

체이닝을 통해 함수를 조합하는 방식은 재사용하기 어려웠지만, 커링과 합성을 사용하는 방식은 재사용이 간단합니다.

import * as R from 'ramda';

const xs = R.range(0, 3);

const f = R.pipe(R.inc, R.inc);
xs.map(f); // [2, 3, 4]

const g = R.map(f);
g(xs); // [2, 3, 4]

const h = R.pipe(R.map(R.inc), R.map(R.inc))
h(xs); // [2, 3, 4]

forEach

Array.forEach에 해당하는 함수입니다. Ramda에서 제공하는 몇 안되는 비함수형 기능 중 한 가지입니다.

map과 마찬가지로 콜백함수에 index, array가 전달되지 않습니다.

이 함수는 배열을 순회하면서 사이드 이펙트를 발생시키는게 목적입니다.

import * as R from 'ramda';

const xs = R.range(0, 3);

for (const x of xs)
  console.log(x);

// 이 경우 console.log가 value, index, array 3개의 인자를 받습니다
xs.forEach(console.log);    

R.forEach(console.log, xs); // 원소를 모두 출력한 뒤 xs 반환

R.pipe(
  R.forEach(console.log),
  R.forEach(console.log)
)(xs);

간혹 map을 forEach의 용도로 사용하는 경우가 있는데, 이 둘의 목적은 전혀 다르다는 것을 알아야합니다.

사이드 이펙트가 필요할때는 map이 아닌 forEach를 사용합니다.

import * as R from 'ramda';

const xs = R.range(0, 3);

function foo(x) {
  console.log(x);
  return R.inc(x);
}

const ys = R.map(foo, xs);
const zs = R.map(foo, ys);

위 코드는 출력과 계산이 하나의 함수에서 일어나고 있습니다. 이 중 출력은 사이드 이펙트에 해당하는 동작입니다.

이런 경우에는 사이드 이펙트와 계산을 분리해서 작성하는 편이 낫습니다.

import * as R from 'ramda';

const xs = R.range(0, 3);
const ys = R.map(R.inc, xs);
const zs = R.map(R.inc, ys);

R.forEach(console.log, xs);
R.forEach(console.log, ys);

가능한 사이드 이펙트는 피하고, 꼭 필요하다면 최대한 분리합니다. 사이드 이펙트를 분리할수록 함수형 프로그래밍을 적용할 기회가 많아집니다.

filter

Array.filter메서드에 해당하는 함수입니다. 마찬가지로 index와 array에 접근할 수 없습니다.

import * as R from 'ramda';

const xs = R.range(0, 4);
const isOdd = (x) => x % 2 === 1;

xs.filter(isOdd);    // [1, 3]
R.filter(isOdd, xs); // [1, 3]

const oddFilter = R.filter(isOdd);
oddFilter(xs); // [1, 3]
인자를 받아서 불리언을 반환하는 함수를 Predicate라 합니다. filter는 Predicate를 인자로 받는 고차함수입니다.

filter는 map과 함께 사용되는 경우가 많습니다. 마찬가지로 체이닝을 커링과 합성으로 표현합니다.

import * as R from 'ramda';

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

const isEven = (x) => x % 2 === 0;
const mult10 = (x) => x * 10;

xs.filter(isEven).map(mult10).slice(0, 3); // [100, 120, 140];
ys.filter(isEven).map(mult10).slice(0, 3); // [200, 220, 240];

const f = R.pipe(
  R.filter(isEven),
  R.map(mult10),
  R.take(3)
);

f(xs); // [100, 120, 140];
f(ys); // [200, 220, 240];

reduce

Array.reduce 메서드에 해당하는 함수입니다. Array.reduce와 다르게 초기값 인자를 생략할 수 없습니다.

import * as R from 'ramda';

const xs = R.range(0, 100);
const add = (z, x) => z + x;

xs.reduce(add);       // 4950
xs.reduce(add, 0);    // 4950
R.reduce(add, 0, xs); // 4950

인자로 받는 함수는 두 개의 인자를 받습니다. 첫 번째 인자는 누적되고 있는 값, 두 번째 인자는 현재 순회중인 배열의 값입니다.

이 함수는 약간 복잡하지만 자주이용하기 때문에 동작을 확실하게 이해할 필요가 있습니다.

import * as R from 'ramda';

const xs = R.range(0, 5);
const accumulate = (z, x) => R.prepend(x, z);

const reverse = R.reduce(accumulate, []);
reverse(xs);
// [] @ [0, 1, 2, 3, 4]
// [0] @ [1, 2, 3, 4]
// [1, 0] @ [2, 3, 4]
// [2, 1, 0] @ [3, 4]
// [3, 2, 1, 0] @ [4]
// [4, 2, 1, 0] @ [] >>> [4, 3, 2, 1, 0]

지금까지 살펴본 함수를 조합하면 복잡한 함수도 간단한 함수를 엮어 만들 수 있습니다.

인자로 받은 숫자 미만의 모든 소수의 제곱의 합을 구하는 함수를 만들어보겠습니다.

import * as R from 'ramda';

function isPrime(n) {
  const upper = Math.sqrt(n + 1);
  const ns = R.range(2, upper);
  const isDivisor = (m) => n % m === 0;
  return !R.any(isDivisor, ns);
}

const square = (x) => x * x;

const f = R.pipe(
  R.range(2),
  R.filter(isPrime),
  R.map(square),
  R.reduce(R.add, 0)
);

f(10); // 87
f(99); // 65796