함수형 자바스크립트 01

나는 map, reduce, filter 와 같은 함수를 통해 부분적으로 FP를 작년부터 시작하고 있다. 그러나 여전히 자바스크립트에서 함수를 작성할 때, 중복된 표현식을 담은 익명 함수를 수고스럽게 작성하고 있었다는 사실을 뒤늦게 깨달았다.

하드 코딩된 표현식을 넘기고 받고 하는 흐름은 순수한 함수를 그대로 넘기고 받는 함수형 패러다임의 의도에 분명히 맞지 않는것 같았다.

익명 함수와 중복된 표현식

내장 함수와 화살표 함수만을 사용해서는 함수형 프로그래밍을 잘 사용할 수 없다고 깨달은 뒤, 그 대안으로 적절히 커스텀한 함수를 같이 사용해봤다.

예제로 L의 각 원소를 N만큼 더하기 위해 Array.prototype.map() 함수를 활용해보자

const L = [1, 2, 3]

L.map(x => x+1) //=> [2, 3, 4]
L.map(x => x+2) //=> [3, 4, 5]
...

함수의 인자에 들어갈 함수를 화살표 함수를 통해 간편히 구현했다. 그러나 각 원소를 N만큼 더하는 x+? 표현식이 뭔가 중복되어 보인다.

함수가 함수를 반환하는 패턴의 유용성

FP의 핵심 개념 중 고차 함수에서 함수가 함수를 반환하는 패턴이 있다. 이를 통해 코드의 중복을 줄여보자.

const add = a => x => x+a

/* 위의 화살표 함수 add와 같다
function add(a) {
    return function (x) {
        return x + a
    }
}*/

L.map(add(1))
L.map(add(2))

add() 함수가 호출되면 함수를 반환 대상으로 주어 add(1) 과 같이 인자를 미리 bind 가 가능하게 할 수 있다.

이 과정을 풀어서 쓰자면 아래와 같은 흐름으로 전개되는 셈이다.

const one = add(1)
one(1) // 2

다른 언어도 함수 반환이 된다면 만들 수 있다. 아래 예제는 파이썬이다.

add = lambda x: lambda y: x+y

서드파티 함수형 라이브러리

하지만 L.map() 같은 개체의 프로토타입 메서드는 근본적으로 개체가 다르면 동일한 결과를 나오지 않는 문제가 있다. 가독성과 부수적인 측면에서 프로토타입 기반 함수들은 쓰면 쓸수록 난해함을 느끼기 쉽다.

이러한 갈증을 서드파티 함수형 라이브러리들은 함수 개체를 단일적인 값으로서 넘겨주어 상당히 해소해준다.

그중 ramda 라이브러리를 둘러보겠다.

const R = require('ramda');
const {map} = R

map(add(1), L)) //=> [2, 3, 4]
map(add(2), L)) //=> [3, 4, 5]

함수의 인자로서 리스트와 함수를 받아 비로소 순수한(?) 코드가 짜졌다.

직접 map 함수를 만들어보자

const map = (F, L) => Array.prototype.map.call(L, F)

map(add(1), L)) //=> [2, 3, 4]

간단히 map 함수를 만들고 싶다면 function.prototype.call() 메서드를 통해 배열을 this로 연결하여 내부적으로는 L.map() 으로 작동하게 끔 할 수 있다.

ES6 명세가 구현되지 않은 오래된 브라우저에서는 아래의 함수로 대신 만들 수 있다.

function map(F, L) {
    var result = []
    for (var i = 0, len = L.lenght; i < len; i++) {
        result.push(F(L[i]))
    }
    return result
}

참조에 의한 값 비교하기

참조에 의한 값(객체 리터럴, 배열 등)은 값으로서 비교가 불가능하지만, 이를 가능하게 해주는 equals 함수를 서드파티 라이브러리들은 제공해준다.

const {equals} = R

equals(L.map(x=> x+1), map(add(1), L)) // true
equals(L.map(x=> x+2), map(add(2), L)) // true

함수를 더 조합해보자

const identity = x => x
identity(10) //=> 10

함수형 프로그래밍에서는 받은 인자를 그대로 넣는 identity라는 함수가 종종 사용한다.

받은 인자를 그대로 뱉어 계산할 필요가 없는, 이 쓸모 없어 보이는 함수가 대체 언제 어디서 활용되는지 다음을 보자.

const {filter} = R

filter([true, 10, 0, null, 'a', false], identity) //=> [true, 10, 'a']

identityfilter의 보조 함수1 2로 사용했더니 참에 가까운 값들(Boolean으로 true로 평가되는 값들)만 남았다. 이런 쓰임새가 생각보다 실용적으로 보인다. identity를 다른 고차 함수와 조합하는 식으로 유용한 함수를 만들 수 있다.

const {find} = R
const some = L => !!find(L, identity)
const every = L => filter(L, identity).length === L.lenght

some은 배열에 들어 있는 값 중 하나라도 긍정적인 값이 있으면 true를, 하나도 없다면 false를 반환한다. every는 모두 참인 값이어야 true를 반환한다.

그런데 everyfilter를 사용했기 때문에 배열의 마지막까지 순회하게 된다. 대신에 다른 함수를 조합하여 로직을 개선이 가능하니 다음을 보자.

const not = v => !v
const beq = a => b => a === b

not(1) //=> false
not(null) //=> true

beq(1)(1) //=> true

add에서 쓰였던 함수가 함수를 반환하는 패턴이 beq에서도 쓰였다. !, === 연산자 대신 왜 함수로 감싸는가에 대한 의문이 들겠지만, 일단은 위 두개의 함수를 통해 every를 다음과 같이 개선할 수 있다.

const {findIndex} = R
const every = L => beq(-1)(findIndex(L, not))

findIndexArray.prototype.findIndex와 마찬가지로 배열의 특정 원소가 색인된 위치를 알려주는 함수이다. 위 코드에선 찾는 원소가 없으면 -1을 반환하는 점을 응용했는데, 만약 not으로 거짓에 가까운 값(Falsy)이 검출된다면 beq(-1)-1 보다 큰 값이 들어온다. filter와 달리 Falsy한 값 하나만 맞닥드리면 로직이 끝나 더 효율적으로 개선되었다.

다음과 같이 함수를 더 쪼갤 수 있다. 조금 극단적이라고 생각할 수도 있겠다.

const positive = L => find(L, identity)
const some = L => not(not(positive(L)))
const negativeIndex = L => findIndex(L, not)
const every = L => beq(-1)(negativeIndex(L))

마무리

add 와 같은 고차 함수의 사용 예와 서드파티 함수형 라이브러리를 소개해보았다. 그 덕분에 몇개의 함수들을 더 설명하고 서로 조합하여 새로운 일을 해낼 수 있었다. 두번째 글에서는 조금 어려운 개념인 커링과 모니드에 대해 정리하겠다.


  1. “보통 콜벡 함수라고 이해하지만 이것은 비동기가 일어나는 상황에서 컨텍스트를 다시 돌려주는 역할에서 한정된다.” - “콜벡 함수라 잘못 불리는 보조함수, 1.4.6절, 함수형 자바스크립트 프로그래밍 - 유인동” [return]
  2. 개인적인 견해로서 콜벡함수와 보조함수, 이벤트 리스너 등은 고차 함수의 하위 집합이라고 생각하고, 그 하위 집합들은 고차 함수라는 개념 외에는 서로 공유되는 개념이 없는, 독립적인 개념이라고 봅니다. [return]