들어가며
함수형 프로그래밍은 어렵다. 내용 자체가 어렵다기보단 함수형으로 생각하는 방법이 어려운 것 같다. 함수들 이름도 뭔가 와닿지 않는 느낌이다. map, filter, reduce 함수는 자바스크립트의 배열의 메서드로 자주 사용해서 익숙한데 go, pipe, curry 등의 함수는 들어본 적도 없고 이런 목적으로 사용하는 함수를 본 적도 없다. 구글링 해본 결과 예전부터 다른 사람들이 go, pipe, curry 등의 함수를 만들어 함수형 프로그래밍을 한 것 같은데 아마 함수형 프로그래밍을 할 때 자주 사용하게 되는 함수들인 것 같다.
map, filter, reduce
배열의 메서드로 자주 사용했던 함수들이다. 이 함수들은 배열 객체에만 사용할 수 있기 때문에 이를 이터러블/이터레이터 프로토콜을 따르는 함수로 바꾸면 함수형 프로그래밍에서 다형성을 만족하는 매우 유용한 함수가 된다.
- map: 이터러블을 순회하며 보조함수를 사용해 요소의 값을 변경하고 변경된 값을 반환하는 함수이다. 코드를 통해 쉽게 알 수 있다.
arr 배열을 순회하며 각 요소의 값에 2를 곱한 값이 담긴 배열을 반환한다. 이를 모든 이터러블에 사용가능한 함수로 만들면 아래와 같다.1 const arr = [1, 2, 3, 4, 5];2 console.log(arr.map((item) => item * 2)); // [2, 4, 6, 8, 10]
배열 메서드를 사용했을때와 같은 값을 반환한다. 그러나 아래에서 정의한 map 함수의 장점은 다른 이터러블 객체를 입력 받았을 때에도 같은 목적으로 사용할 수 있다는 것이다.1 const arr = [1, 2, 3, 4, 5];2 const map = (func, iter) => {3 let res = [];4 for (const a of iter) res.push(func(a));5 return res;6 };7 console.log(map((a) => a * 2, arr)); // [2, 4, 6, 8, 10] - filter: 이터러블을 순회하며 보조함수를 만족하는 요소를 모아 반환하는 함수이다. 코드를 보자
3보다 큰 값을 반환하기 위한 함수를 인자로 주었고 4와 5를 반환했다. 이 코드를 정의해보자.1 const arr = [1, 2, 3, 4, 5];2 console.log(arr.filter((item) => item > 3)); // [4, 5]
같은 결과를 반환하지만 모든 이터러블 객체에 사용할 수 있는 filter 함수를 정의했다.1 const arr = [1, 2, 3, 4, 5];2 const filter = (func, iter) => {3 let res = [];4 for (const a of iter) if (func(a)) res.push(a);5 return res;6 };7 console.log(filter((a) => a > 3, arr)); // [4, 5] - reduce: 이터러블을 입력 받아 축약된 값으로 반환해주는 함수이다. 배열에 있는 모든 값을 더할때 자주 사용할 수 있다.
리듀서 함수와 초기값을 입력 받아 결과값을 출력한다. 1과 2를 더한 3이라는 값을 3과 더해 6을 만들고 6과 4를 더해 10을 만들고 10과 5를 더해 15를 만들어 반환하는 방식이다.1 const arr = [1, 2, 3, 4, 5];2 console.log(arr.reduce((acc, cur) => acc + cur, 0)); // 15
결과는 같지만 모든 이터러블에 대해 사용가능한 reduce 함수를 정의했다. 자바스크립트 배열의 reduce 메서드는 acc 값이 주어지지 않았을 때 배열의 맨 첫번째 값을 꺼내 그 값을 acc로 사용한다. 코드를 추가해보자1 const arr = [1, 2, 3, 4, 5];2 const reduce = (func, acc, iter) => {3 for (const a of iter) {4 acc = func(acc, a);5 }6 return acc;7 };8 console.log(reduce((a, b) => a + b, 0, arr)); // 15
만약 함수의 인자로 보조함수와 이터러블만 넘겨줬다면 reduce 함수에서 iter 값은 undefined 일 것이다. 그렇기 때문에 iter 값을 비교해서 undefined라면 iter에 이터레이터를 할당하고 acc에는 iterator의 첫번째 값을 꺼내 할당해준다.1 const arr = [1, 2, 3, 4, 5];2 const reduce = (func, acc, iter) => {3 if (!iter) {4 iter = acc[Symbol.iterator]();5 acc = iter.next().value;6 }7 for (const a of iter) {8 acc = func(acc, a);9 }10 return acc;11 };12 console.log(reduce((a, b) => a + b, arr)); // 15
go, pipe, curry
함수형 프로그래밍에서는 코드를 값으로 다루는 아이디어를 많이 사용한다. 만약 어떤 함수가 함수를 받아서 평가하는 시점을 내가 원하는 시점으로 다룰 수 있다면 코드의 표현력이 더 좋아질 것이다.
go: 초기값과 여러 개의 함수를 입력 받아 순차적으로 실행한 함수의 결과값을 다음 함수로 넘겨 평가하고 최종적으로 값을 반환하는 함수이다. 만약 입력값에 1을 더하는 함수, 10을 더하는 함수, 100을 더하는 함수를 인자로 넘겨 줬을 때 모든 함수를 실행한 결과값을 반환 받고자 한다.
1 const go = (...args) => reduce((a, f) => f(a), args);2 console.log(3 go(4 0,5 (a) => a + 1,6 (a) => a + 10,7 (a) => a + 1008 )9 ); // 111go 함수는 인자들을 받아 하나씩 값을 계산하고 마지막 함수의 반환값을 반환해준다.
pipe: go 함수와 비슷하지만 값을 반환하는 것이 아닌 입력 받은 함수를 순서대로 실행하는 새로운 함수를 반환한다.
1 const pipe =2 (...fs) =>3 (input) =>4 go(input, ...fs);5 const newFunc = pipe(6 (a) => a + 1,7 (a) => a + 10,8 (a) => a + 1009 );10 console.log(newFunc(0)); // 111pipe 함수를 실행하면 a=>a+1, a=>a+10, a=>a+100 함수를 순서대로 실행하는 함수를 반환한다. 새로운 변수에 해당 함수를 할당하고 newFunc 함수를 실행하면 go를 사용했을 때와 같은 값을 반환하는 것을 확인할 수 있다.
curry: 받아 둔 함수를 내가 원하는 시기에 평가할 수 있게 하는 함수이다. 원하는 개수 만큼의 인자가 들어오면 실행된다.
1 const curry =2 (f) =>3 (a, ..._) =>4 _.length ? f(a, ..._) : (..._) => f(a, ..._);5 const add = (a, b) => a + b;6 const curriedAdd = curry(add);7 console.log(curriedAdd(1)); // 함수8 console.log(curriedAdd(1)(2)); // 3add 함수는 더해질 두개의 인자가 필요하다. curriedAdd(1) 함수는 하나의 인자만 받았기 때문에 함수를 반환하고 다음 입력이 들어오면 값을 출력한다. curry 함수를 적용하는 것을 currying 이라고 하는데 커링과 관련해서 더 살펴보면 좋을 것 같다. 코어 자바스크립트 커링
range, take, L.range
range: 입력 받은 인자만큼의 길이를 가지는 배열을 생성하는 함수이다.
1 const range = (length) => {2 let i = -1;3 let res = [];4 while (++i < length) {5 res.push(i);6 }7 return res;8 };9 console.log(range(5)); // [0, 1, 2, 3, 4]이 함수를 위에서 정의한 map 함수와 같이 사용하는 경우를 생각해보자
1 go(range(5), (iter) => map((a) => a + 2, iter), console.log); // [2, 3, 4, 5, 6]range 함수를 사용해 [0, 1, 2, 3, 4] 배열을 생성하고 map 함수를 통해 각 요소에 2를 더한 값을 반환한다.
take: 입력 받은 개수만큼 이터러블을 잘라서 출력한다.
1 const take = (limit, iter) => {2 let i = 0;3 let res = [];4 for (const a of iter) {5 res.push(a);6 if (res.length === limit) return res;7 }8 };9 go(range(5), (iter) => take(2, iter), console.log); // [0, 1]range 함수를 사용해 길이가 5인 배열을 만들고 그 중 두 개의 요소를 뽑아서 출력했다. map 함수를 사용했을 때는 모든 요소를 순회해야하기 때문에 문제가 되지 않지만 take 함수를 사용할 때는 문제가 될 수 있다. range 함수는 당장 평가되지 않는 요소라도 미리 만들어 놓는다. 만약 range의 인자로 1000000이 들어가고 take로 2개의 요소만 뽑아낸다고 생각해보자. range는 길이가 백만인 배열을 먼저 만들어 놓고 그 중에서 두 개의 요소만 실제로 사용된다. 이는 효율성을 떨어뜨릴 수 있다. 이를 해결하기 위해 지연된 range (L.range) 함수를 정의해서 사용할 수 있다.
L.range: 위에서 언급한 문제를 해결하기 위해 사용하는 함수이다. 지연된 range라는 말이 무슨 말인지 아래의 코드를 통해 확인해보자.
1 const L = {};2 L.range = function* (length) {3 let i = -1;4 while (++i < length) {5 yield i;6 }7 };8 go(L.range(5), (iter) => take(2, iter), console.log); // [0, 1]L.range 함수를 사용해도 range 함수를 사용했을 때와 결과가 다르지 않다. 그러나 L.range의 인자로 매우 큰 값이 들어가고 take 함수의 인자 값이 작아서 range로 생성한 이터러블에 비해 적은 값만 뽑아내야할 때 range와 L.range의 차이가 발생한다. L.range는 제너레이터를 사용해 이터레이터를 생성한다. 그렇기 때문에 값이 실제로 필요한 상황에 L.range에서 값을 가져온다. 필요하지 않은 값은 아직 평가되지 않는다. 그렇기 때문에 앞에서 설명했던 1000000의 경우에도 L.range를 사용하면 take 함수에서 필요한 두 개의 값만 가져오고 실행이 종료된다. 이와 같은 방법을 map, filter 등의 함수에도 적용할 수 있다.
정리
함수형에서 중요한 점은 위에서 설명한 함수들을 구현하는 것도 중요하지만 필요한 상황에 맞게 함수를 정의하고 정의한 함수들을 조합해 사용하는 것이 중요한 것 같다. 아직 어려운 부분이 있지만 함수형을 사용했을 때 편리한 점이 눈에 조금씩 보이고, 나도 함수형을 적용해 프로그래밍 해보고 싶다는 생각을 하게 되었다.
그런데 나는 함수형 인간일까? 무조건 반사를 하는 함수가 몸에 내장돼있다. 무릎을 치거나 공이 눈앞으로 날아오거나 뜨거운 걸 만지거나 하면 우리 몸에서는 무조건 반사 함수가 실행된다. 다양한 입력에 대해 함수를 실행하기 때문에 다형성도 만족하는 것 같다. 토하는 함수가 있다고 해보자. 음식을 잘못먹으면 토하는 함수가 실행되기도 하고, 뇌를 다치면 그 함수가 실행되기도 한다. 이런게 함수형 프로그래밍일까? 만약 빠른 속도로 축구공이 눈앞으로 날아온다고 생각해보자. 갑자기 눈앞에 물체가 날아오기 때문에 무조건 반사 함수가 실행 될 것이다. 그 후 머리를 세게 맞아 뇌가 손상 됐고 이로 인해 토하는 함수가 실행됐다고 하자. 그럼 이는 함수의 합성일까? 앞에서 정리했던 go나 pipe 함수와 비슷한 것 같다.
생각보다 함수형 프로그래밍은 우리 주변에 함께 있는 것 같다...라는 생각을 하면서 빨리 자러가야겠다.
참고자료
함수형 프로그래밍 - go, pipe, curry https://velog.io/@codenmh0822/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-go-pipe-curry
커링이란 https://ko.javascript.info/currying-partials