Skip to content
Underbleu
GithubLinkedin

3-2. 코드를 컬렉션으로 다루기 - go, pipe, curry, 실무적 사례

Functional programming3 min read

프로그래머스에서 진행한 유인동님의 ES6로 알아보는 동시성 & 함수형 프로그래밍 강의를 들으며 정리한 내용입니다.

# Javascript Basic

Rest vs Spread

Rest operator와 Spread opertor는 생김새는 같지만 명확히 반대의 기능이다.

  • Rest operator - spread the values
    ... 1 2 3 -> [1, 2, 3]
    collects the remaining items of an iterable into an Array
  • Spread operator - gather the values
    ...[1, 2, 3] -> 1 2 3
    before an array (or an iterable) it spreads the element of the array in individual variable

Converting Iterable or Array-like objects to Arrays

"Array-like or Object (has property length)", you can use Array.from() to convert it to an Array

1// Example
2const arrayLike = {
3 '0': 'a',
4 '1': 'b',
5 '2': 'c',
6 length: 3
7};
8
9const arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

전개연산자와 Maximum call-stack Error

참고 MDN Spread with many values

spread operator는 내부적으로 iterator를 만들고, 순회를해서, 꺼낸값을 함수에 apply하여 집어넣는 일을 한다

1function foo(a, b, c){
2 console.log("a", a);
3 console.log("b", b);
4 console.log("c", c);
5}
6
7var arr = [1, 2, 3];
8
9foo.apply(undefined, arr);
10// a 1
11// b 2
12// c 3
13
14foo(...arr);
15// a 1
16// b 2
17// c 3

함수 호출시 spread operator를 사용하게 된다면, JavaScript engine's argument length limit을 넘지 않도록 유의 하여야 한다

콜스택에러의 발생 기준은 브라우저마다, 테스트 시점마다 다르지만 크롬의 V8엔진에서는, 배열을 전개연산자로 전달시 대략 arguments 길이 30000000일때 부터 콜스택에러가 생긴다

1function makeArr(len) {
2 const arr = []
3 for(let i = 0; i < len; i++) arr.push(i)
4 return arr
5}
6
7const readArr = (...arr) => console.log(arguments)
8
9const arr= makeArr(30000000)
10readArr(arr)
11// Paused before potential out-of-memory crash

함수에 전달해야할 인자가 수 만개라면, 함수 호출시 인자를 펼치지 않고 통채로 전달할 수 있도록 설계하자

1// pipe함수 설계. go대신 reduce를 사용
2
3const reject = (...fs) =>
4 arg => go(arg, ...fs) // fs펼쳐서 전달
5
6const reject = (...fs) =>
7 arg => reduce((arg, f) => f(arg), fs, arg) // fs통채로 전달

# go(): 함수세트를 중첩적으로 실행시켜 을 리턴

reduce를 활용하여 명령형으로 작성해뒀던 go를 리팩토링 해보자

  • 부모함수 reduce가 비동기를 지원하기 때문에, 비동기문제를 신경쓰지 않아도 된다
  • then은 함수를 하나만 받을 수 있지만, go는 여러 함수를 중첩적으로 받을 수 있기 때문에 확장성이 더 크다 (비동기까지 되닌깐 then은 빠이~~)
  • reduce의 acc가 undefined일 경우 coll의 첫번째 인자를 acc로 쓰기 때문에, 인자 받는곳의 네이밍을 더 간단하게 할 수 있다
    (arg, ...fs) -> (...coll)
1/*
2const go = (arg, ...fs) => reduce((arg, f) => f(arg), fs, arg); */
3const go = (...coll) => reduce((arg, f) => f(arg), coll);
4
5go(Promise.resolve(10),
6 a => a + 10,
7 a => Promise.resolve(a + 10),
8 console.log); // 30

# 다른 함수형 라이브러리들과의 비교

개발자로서 사용자의 입장에 있으면 underscore, lodash의 체이닝이 편하다. 하지만 라이브러리에서 지원해주지 않는 어떤 함수를 메서드에 추가해주려면, 그 라이브러리의 인터페이스를 공부하고 거기에 덧 데어야한다 (mixin)

1. go 함수체이닝

함수가 특정 프로토타입에 갇히지 않아 어디에나 조합가능 (확장성 good)

2. undescore.js 함수체이닝

  • 함수를 사용할 데이터타입의 prototype에 메서드로 구현해줘야함 (번거로움)
  • 컬렉션 타입에 대한 대응도 까다로워짐
    (객체는 Object.prototype에, 배열은 Array.prototype에 해당 함수를 메서드로 추가해줘야한다)
1// 1. go
2const addAll = nums => reduce((a, b) => a + b, nums)
3
4go([1,2,3],
5 filter(a => a % 2),
6 map(a => a * 2),
7 addAll)
8
9// 2. underscore.js
10Array.prototype.addAll = function() {
11 this.reduce(...)
12}
13
14nums
15 .filter(a => a % 2)
16 .map(a => a * 2)
17 .addAll(...)

# pipe(): 함수세트를 중첩적으로 실행시킨 함수를 리턴

go와 다르게 인자는 받지않고, 함수만 받는다 -> 중첩 실행된 함수 리턴

1. go()를 활용 -> 콜스택에러 위험

go(arg, ...fs)

1const pipe = (...fs) => arg => go(arg, ...fs)
  • 함수를 리턴해야 하면, 화살표를 한 번 더 쓰면된다 (심플하게 생각하자)
  • go를 활용하게되면, rest 파라미터로 받아놓은 함수세트 [f, f, f...]를 spread operator로 펼쳐서 go함수에 전달해야 한다
  • 이 때 pipe에 전달될 argument length(fs)가 만오천 이상되면 maximum callstack에러가 날 수도 있다
    • spread operator는 내부적으로 iterator를 만들고, 순회를해서, 꺼낸값을 함수에 apply하여 인자로 집어넣는 일을 한다

2. reduce()를 활용하여, spread연산자를 쓰지 않도록

Rest 파라미터로 받은 함수세트(fs)를 배열형태 그대로 reduce에 전달. 이렇게 하면 함수에 전달되는 인자는, 단 3개 (f, fs, acc)이기 때문에 절대 콜스택 에러 안난다 ^^

1const pipe = (...fs) =>
2 arg => reduce((arg, f) => f(arg), fs, arg);

근데 만약 아래처럼 인자를 전달하면, 죽을 수 있다

1const go = (arg, ...fs) => reduce(call2, [arg, coll])
2const go = (arg, ...fs) => reduce(call2, [arg, ...fs])

3. call(), call2() - 함수를 호출하는 함수

함수콜을 작은 함수로 독립시켜, go() pipe()의 코드를 간결하게 만들어보자

1const call = (f, a) => f(a);
2const call2 = (a, f) => f(a);
3
4const go = (...coll) => reduce(call2, coll);
5const pipe = (...fs) => arg => reduce(call2, fs, arg);

pipe는 순수함수를 리턴하는 부수효과 없는 함수, go는 부수효과를 내는 함수로 구분지어 생각해보자


인자가 없는 함수?

  • OOP에선 인자가 적을수록 좋다고도 얘기하지만
  • 함수형프밍에선 인자가 없으면, 부수효과가 있을 수 있다는 의심을 하고봐야 한다.
    -> 인자가 없다?
    -> 자기 혼자 리턴값을 만든다?
    -> 외부의 값을 가져다가 쓰면서, 부수효과를 만들 수 있겠구나 !
1// 함수형에서 유일하게 인정해주는 인자가 없는 함수
2const always10 = _ => 10;
3const noop = _ => undefined;

# curry()

함수의 인자 받는 방식을 다양하게 만들어 주는 함수

1. curry 만들기

curry가 있다면 함수들을 인자를 받는 방식을 다양하게 만들어 줄 수 있다
일반적으로 보기에 f( , ) 처럼 인자를 받는게 f( )( ) 보다 피곤하다고 생각한다

1const curry = f => (a, ..._) =>
2 _.length == 0 ? (..._2) => f(a, ..._2) : f(a, ..._);
3
4var add = curry((a, b, c) => a + b + c);
5
6add(10, 10, 10) // 30
7add(10)(10, 10) // 30

2. curry를 활용한 cmap, cfilter

map, filter가 추후에 인자를 받아 실행될 수 있게 해준다. 코드가 훨씬 깔끔해짐

1const cfilter = curry(filter)
2const cmap = curry(map)
3
4var nums = [1, 2, 3, 4, 5, 6]
5
6// before
7go(
8 nums,
9 nums => filter(a => a % 2, nums), // [1, 3, 5]
10 nums => map(a => a * 2, nums), // [2, 6, 10]
11 log)
12
13// after
14go(
15 nums,
16 cfilter(a => a % 2),
17 cmap(a => a * 2),
18 log) // [2, 6, 10]

3. curry, go로 baseMF() 리팩토링

  • curry()로 리팩토링 하면 기존의 map, filter함수가
    1. 인자를 map(f, coll)로 한 번에 받던 방식뿐만아니라,
    2. 인자를 map(f)(coll)로 coll은 나중에 받는 방식도 가능해진다
  • go()then2()를 대체하면 코드가 간결해진다
    --> 코드 어디에도 비동기처리가 안나타나기 시작한다
1then2(f(a), b => f1(res, a, b))
2go(a, f, b => f1(res, a, b))
1const baseMF = (f1, f2) => curry((f, coll) =>
2 hasIter(coll) ?
3 reduce((res, a) => go(a, f, b => f1(res, a, b)), coll, []) :
4 reduce((res, [k, a]) => go(a, f, b => f2(res, k, a, b)), entriesIter(coll), {}));
5
6var nums = [1, 2, 3, 4, 5, 6];
7
8// 인자받는 방법 1.
9go(
10 nums,
11 nums => filter(a => a % 2, nums),
12 nums => map(a => a + 10, nums),
13 log)
14
15// 인자받는 방법 2.
16go(
17 nums,
18 filter(a => a % 2),
19 map(a => a + 10),
20 log) // [11, 13, 15]

4. filter와 map을 활용하는 함수들 리팩토링

curry()baseMF()를 리팩토링한 후엔, filter와 map 역시 인자받는 방식이 다양해진다. 이를 바탕으로 filter와 map을 활용한 함수도 리팩토링 해보자

1const compact = coll => filter(a => a, coll)
2const compact = filter(identity);

# reject()의 보조함수 리팩토링

reject의 보조함수는 go와 pipe로 리팩토링할 수 있다

  1. go() 활용

    1not(f(a))
    2go(a, f, not)
    • 코드해석이 쉬워질 뿐만아니라
    • go()가 reduce로 빌드되었기 때문에 비동기 처리를 할 수 있으므로
    • not()이 비동기 대응을 하지 않는, 단순히 값만 부정하는 함수가 될 수 있다
  2. pipe() 활용

  3. negate()
    함수를 반대로 동작하게 하는 pipe(f, not)을 추상화 시킨 작은함수 독립

1const not = a => !a
2const negate = f => pipe(f, not)
3
4const reject = (f, coll) => filter(a => go(a, f, not), coll)
5const reject = (f, coll) => filter(pipe(f, not), coll)
6const reject = (f, coll) => filter(negate(f), coll)

# 함수형프밍은 대입문이 없는 프로그래밍이다

  • 대입이 생기면 문장이 생기고,
  • 문장이 생기면 부수효과가 생겨 오류가 생길 여지가 생기는데,
  • 함수형은 부수효과를 일으키지 않는 컨셉에 중점을 둔다 -> 항상 동일하게 동작하는 프밍
  • 부수효과가 없는 함수들을 조합해서만, 대입도 문장도 만들지 않으며 코드를 짠다

go를 만들고 map, filter, reduce, find를 만들고 나면 , 함수를 조합해 코딩할 수 있고, 내 코드의 어디에도 비동기가 나타지 않기 때문에

비동기처리에 골머리 쓰지않고, 코드를 해결하는 것에만 사고를 하면된다