함수형 프로그래밍 5 코드를 값으로 다루기

go

첫 번째 parameter를 두 번째 parameter로 전달한다.
이를 마지막 parameter까지 반복하는 함수이다.
즉 이전 함수의 실행결과를 다음 함수의 인자로 전달하는 함수이다.
값을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);
// 111

고차함수가 한 번에 이해되지 않아서 콘솔로 실행 순서를 추적해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
console.log(f, acc, a);
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);

// (a, f) => f(a) 0 (a) => a + 1
// (a, f) => f(a) 1 (a) => a + 10
// (a, f) => f(a) 11 (a) => a + 100
// (a, f) => f(a) 111 ƒ log() { [native code] }
// 111

실행 순서는 다음과 같다.

  1. reduce에 콜백함수 c (a,f)=>f(a)와 인자배열을 전달하는 함수인 go를 만든다.
  2. reduce로 전달된 인자가 2 개밖에 없으므로 args[0]이 acc가 된다. 이 때 args는 콜백함수만 존재하는 이터러블이 된다.
  3. args[0]은 이미 평가되었으므로 args[1]부터 args를 순회하며 acc에 c에 args 이러터블의 value를 전달한 값을 저장한다. 즉 acc에는 (acc,args[Symbol.iterator]().next().value)=>args[Symbol.iterator]().next().value(a)가 실행되어 저장된다.
  4. 모든 콜백함수의 실행을 마친 후 acc를 반환한다.

pipe

go와 같은 역할을 하지만 값이 아닌 함수를 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);

const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);

console.log(f(0, 1));
///111

마찬가지로 콘솔로 실행순서를 추적해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);

const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);

console.log(f);

// (...as) =>
// go(f(...as), ...fs)

console.log(f(0, 1));
///111

실행순서는 다음과 같다.

  1. pipe에 함수를 평가한 값을 parameter로 받도록 하기위해 args의 첫 번째 함수를 따로 분리한다.
1
2
3
4
const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);
  1. f에 pipe와 콜백을 전달한다. f엔 다음과 같은 함수객체가 저장된다.
    f의 실행컨텍스트엔 pipe에 저장된 첫 번째 함수가 있다.
1
f = (...as) => go(f(...as), ...fs);
  1. f에 0,1을 전달한다. f(0,1)은 add(0,1)이므로 1로 평가되어 전달된다.
    즉 acc의 초기값은 1이 된다.
1
f = (0,1) => go(f(0,1), ...fs);
  1. go 내부에서 acc를 계산한후 반환한다.
1
2
console.log(f(0, 1));
// 111

curry

함수를 parameter로 받아서 원하는 개수만큼의 parameter가 들어왔을 때 평가시켜 반환하는 함수이다.
코드의 재사용성을 높여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
//parameter가 2개 이상이라면 바로 실행하고 두 개보다 작다면 이후 parameter를 받아서 실행하는 함수이다.

const mult = curry((a, b) => a * b);
console.log(mult);
//(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._)
console.log(mult(1));
// (..._) => f(a, ..._)
console.log(mult(1)(2));
// 2

const mult3 = mult(3);
console.log(mult3(10));
// 30
console.log(mult3(5));
// 15
console.log(mult3(3));
// 9

go 응용하기

다음의 코드는 아래와 같이 go를 이용해서 다시 작성할 수 있다.

1
2
3
4
5
6
7
8
9
console.log(
reduce(
add,
map(
(p) => p.price,
filter((p) => p.price < 20000, products)
)
)
);

다음 파라미터의 인자로 이전 파라미터의 실행값이 전달되는 go를 이용해서 다시 작성하였다.

1
2
3
4
5
6
7
go(
products,
(products) => filter((p) => p.price < 20000, products),
(products) => map((p) => p.price, products),
(prices) => reduce(add, prices),
console.log
);

filter,reduce,map에 모두 curry를 적용했을 경우엔 다음과 같이 수정할 수 있다.

1
2
3
4
5
6
7
8
9
10
go(
products,
filter((p) => p.price < 20000),
// filter((p) => p.price < 20000)(products) 가 되고 filter에 2개의 parameter가 전해져서 filter가 실행된다.
map((p) => p.price),
reduce(add),
console.log
);

// 파라미터가 전달되기 때문에 생략하여도 가능하다.

함수 조합으로 함수 만들기

pipe를 이용하여 중복을 제거할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
log
);

go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
log
);

같은부분을 함수로 작성하여 중복을 제거하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const total_price = pipe(
map((p) => p.price),
reduce(add)
);

const base_total_price = (predi) => pipe(filter(predi), total_price);

go(
products,
base_total_price((p) => p.price < 20000),
console.log
);

go(
products,
base_total_price((p) => p.price >= 20000),
console.log
);

Ref

함수형 프로그래밍과 JavaScript ES6+

댓글