Node.js/JS

[JavaScript] go, curry 함수를 만들어 읽기 좋은 코드 만들기

턴태 2023. 4. 29. 02:35

https://dev-scratch.tistory.com/151

 

[JavaScript] go, pipe로 읽기 좋은 코드 만들기

기존 함수의 문제 https://dev-scratch.tistory.com/150 [JavaScript] Map + Filter + Reduce 중첩 사용과 함수형 사고 Map, Filter, Reduce 중첩하여 사용 map, filter, reduce 함수를 커스텀으로 생성하고, 이를 중첩하여 사용

dev-scratch.tistory.com

이제 위 게시물의 가장 상단 복잡했던 코드들을 아래와 같이 작성할 수 있다.

 

먼저 원본 코드는 아래와 같다.

const filter = (f, iter) => {
	const res = [];
    for (const a of iter) {
	    if (f(a)) res.push(a);
    }
    return res;
};

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 products = [
	{ name: '반팔티', price: 15000 },
    { name: '긴팔티', price: 20000 },
    { name: '핸드폰케이스', price: 15000 },
    { name: '후드티', price: 30000 },
    { name: '바지', price: 25000 },
];
const add = (a, b) => a + b;

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

 

이 함수를 go 함수를 사용해 아래와 같이 가독성 좋게 표현할 수 있다.

 

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

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

위 함수는 위에부터 아래로 읽으면서 전체 흐름을 한 눈에 파악하기 좋다.

 

Curry

Curry 함수는 함수를 값으로 다루면서, 받아둔 함수를 내가 원하는 시점에서 평가하는 함수이다.

함수를 받아서 함수를 리턴하고, 인자를 받아서 원하는 인자 개수만큼 들어왔을 때 받아두었던 함수를 나중에 평가시킨다.

 

const curry = f => 
	(a, ..._) => _.length
    ? f(a, ..._)
    : (..._) => f(a, ..._);

위 함수는 첫 번째 매개변수와 나머지 함수 인자를 받아들인다. 이때, 총 인자가 2개 이상인 경우에는 인자와 함께 그 함수를 실행하고, 아니면 그 이후에 받은 인자들을 합쳐서 실행하는 함수이다.

 

const multiple = curry((a, b) => a * b);

multiple(1);

만약 이렇게 값을 받아서 사용하게 된다면, multiple 의 반환값은 아래와 같다. 위에서 거짓인 경우의 상황인 것이다.

(..._) => f(a, ..._)

이때, 한 번 더 값을 불러오면 원하는 값을 얻어낼 수 있다.

multiple(1)(2);
// 2

multiple(2)(3);
// 6

혹은 미리 변수에 첫 번째 함수를 반환하는 함수를 평가해 변수에 함수의 포인터를 전달하여 사용할 수도 있다.

const multiple3 = multiple(3);

multiple3(10);
// 30

multiple3(5);
// 15

multiple3(3);
// 9

 

그러면 이제 go와 curry를 혼합해 더 가독성이 좋은 코드를 만들어 볼 수 있다.

 

const map = curry((f, iter) => {
	const res = [];
	for (const a of iter) {
	    res.push(f(a))
    }
    return res;
});

const filter = curry((f, iter) => {
	const res = [];
    for (const a of iter) {
	    if (f(a)) res.push(a);
    }
    return res;
});

const reduce = curry((f, acc, iter) => {
	if (!iter) {
	    iter = acc[Symbol.iterator]();
        acc = iter.next().value;
    }
    for (const a of iter) {
	    acc = f(acc, a);
    }
    return acc;
});

이때, go 함수를 사용할 때는 아래와 같이 사용할 수 있다.

const add = (a, b) => a + b;

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

여기서 이 과정을 조금 더 축약할 수 있는데, 다음과 같다.

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),
    console.log
);

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

여기서는 map과 reduce가 겹치기에 추가적으로 조합을 사용해 간단히 나타낼 수 있다.

const total_price = pipe(
	map(p => p.price),
    reduce(add)
);

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

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

pipe를 사용해 함수들을 등록한 후, 향후에 트리거할 수 있도록 함수 포인터만 받아서 다시 go 함수에 전달한다.

 

그런데 여기서 filter와 total_price를 사용하는 것이 겹치므로 한 번 더 가공해 표현할 수 있다.

const total_price = pipe(
	map(p => p.price),
    reduce(add)
);

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

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

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

 


출처: 인프런 함수형 프로그래밍과 JavaScript ES6+(https://www.inflearn.com/course/functional-es6/dashboard)

 

사실 curry에 대해서 완벽히 이해되지 않아서 조금 아쉽다. 그런데 매력이 느껴졌다. 복잡한 절차형 프로그래밍을 단순하게 함수형 프로그래밍으로 변경시킴으로써 함수를 값으로도 사용할 수 있기에 더욱 가독성 있는 코드, 유지보수하기 원활한 코드가 될 수 있다고 생각한다. curry의 의미를 조금 더 열심히 파악해야겠다.