배열과 함수의 응용

2017년 03월 22일


map

자바스크립트에서 기본 메서드 Array.map를 제공할 정도로 map은 유용한 함수입니다.

Array.map 메서드와 비교하면 R.map 함수는 체이닝을 할 수 없는 대신 커링과 합성을 할 수 있었고, index에 접근할 수 없지만 객체에도 적용할 수 있는 다형성을 갖고 있었습니다.

배열에 대한 map의 성질

map의 인자로 사용하는 함수에 사이드 이펙트가 없고, 종료된다는 것이 보장되면 map에 대한 몇 가지 성질이 드러납니다.

이렇게 함수에는 사이드 이펙트가 없을때만 사용할 수 있는 성질이 있고, 이 성질을 이용하는 것이 함수형 프로그래밍의 특징 중 한 가지입니다.

배열의 길이를 보존한다

배열에 아무리 map을 적용해도 배열의 길이는 항상 보존됩니다. 다음 코드는 ???에 어떤 함수(사이드 이펙트가 없는 함수)가 들어오던지 상관없이 결과값이 항상 10입니다.

import * as R from 'ramda';

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

const f = R.pipe(
  R.map(???),
  R.map(???),
  R.map(???),
  R.map(???),
  R.map(???),
  R.map(???),
  R.prop('length')
);

f(xs); // 10

이어서 호출하는 경우 계산 순서를 바꿀 수 있다

배열에 여러번 map을 적용하는 것과, 적용될 함수를 합성한 뒤 한 번만 map한 것의 결과가 같습니다.

import * as R from 'ramda';

const f = R.pipe(R.map(R.multiply(10)), R.map(R.add(5)));
const g = R.map(R.pipe(R.multiply(10), R.add(5)));

const xs = R.range(1, 6);
R.equals(f(xs), g(xs)); // true

이 성질을 이용하면 계산을 재배치할 수 있습니다.

우선 map을 두 번 적용하는 f를 살펴봅니다. 우선 R.map(R.multiply(10))의 호출로 임시배열이 만들어지고, 이 배열을 R.map(R.add(5))의 인자로 사용합니다.

multiply(10)
1 -> 10
2 -> 20
3 -> 30
4 -> 40
5 -> 50

add(5)
10 -> 15
20 -> 25
30 -> 35
40 -> 45
50 -> 55

반면에 g는 map을 적용할 함수들을 합성한 뒤, 한 번만 map을 적용합니다.

덕분에 중간에 발생하는 임시배열이 없고 공간복잡도가 낮습니다.

pipe(multiply(10), add(5))
1 -> 15
2 -> 25
3 -> 35
4 -> 45
5 -> 55

이러한 성질은 최적화에 사용할 수 있습니다.

실제로 파이썬의 map 함수는 바로 계산하지 않고 함수를 누적했다가 값이 필요할 때 한 번에 계산하고, 하스켈은 더 나아가 컴파일하는 시점에 계산을 재배치해버립니다. 언어에서 사이드 이펙트를 철저하게 금지하기 때문에 가능한 최적화입니다.

아쉽게도 자바스크립트에서는 어떠한 최적화도 일어나지 않습니다. 프로그래머가 신경써서 작성하거나, 지연 자료구조를 제공하는 라이브러리를 사용해야합니다.