Search
Duplicate

코드를 값으로 다루어 표현력 높이기

go

개요

인자로 함수리스트를 받아 첫 번째 인자값을 초기값으로 하여, 두 번째 인자부터 함수들의 리스트를 받아 반환 값을 다음 함수로 넘겨 함수를 리턴합니다.
예를들어 go(0, f1,f2,f3,f4..,fn)으로 go라는 함수를 호출하게 될 경우 0을 초기값으로 다음 인자값으로 넘어온 함수들에게 차례차례 각각의 반환값을 전달하면서 결과들이 축약되어 최종 fn(...f4:return)이 수행되는 것입니다.

예제코드

이전에 작성했던 reduce를 응용해서 구현해 봅니다.
const go = (...args) => reduce((a,f)=>f(a), args);
Java
복사
테스트 코드
go( 0, a => a+1, a => a+10, a => a+100, log ) // 111
Java
복사

pipe

개요

go 함수와의 가장 큰 차이점은 go는 인자값으로 들어온 함수에서 즉시실행하여 결과값을 반환해 다음 함수로 전달한다면 pipe 함수는 함수자체를 반환하여 최종적으로 인자값으로 받은 함수리스트를 합성해서 합성된 합수를 가지고 로직을 수행합니다.
pipe 함수 flow

예제 코드

go와 유사한 로직이지만 즉시실행되는 부분만 변환하여 지연실행하도록 합니다.
const pipe = (...fs) => (a) => go(a, ...fs); const f = pipe( a=> a+1, a => a+10, a => a+100 ); f(0)// 111
Java
복사
강화된 pipe함수
⇒ 현재 구현한 pipe함수는 최초 인자값으로 인자값을 하나만 받기 때문에 처음 인자값을 (0,1,2,...)으로 줄 수는 없습니다. 굳이 사용하려면 f(add(0,1)) 이런식으로 사용을 해야합니다. 그래서 해당 부분을 좀 더 유연하게 보강해봅니다.
const pipe = (f,...fs) => (...as) => go(f(...as), ...fs); const f = pipe( (a,b)=> a+b, a => a+10, a => a+100 ); f(0,1) // 111
Java
복사
인자값을 2개 보내도 처리가 가능합니다.
인자값을 하나만 보내고 싶으면 pipe함수의 첫 번째 인자값을 a⇒a로 해주면 문제없이 잘 동작합니다.

go 를 사용하여 읽기 좋은 코드로 만들기

개요

map+filter+reduce 중첩 사용과 함수형 사고에 대해 공부할 때, 사용했던 코드는 아래와 같습니다. 현재는 3가지의 함수만 중첩하고 로직도 간단하지만, 로직이 복잡해지고, 인자값인 함수들이 복잡해질수록 가독성은 급격히 떨어집니다. 이를 go를 사용해서 리팩토링을 해봅시다.
reduce(add, 0, map(p => p.price, filter(p => p.price < 30000, books)))
JavaScript
복사

go를 사용한 리팩토링

go( books,//(1) books => filter(p => p.price < 30000, books),//(2) books => map(p => p.price, books),//(3) prices => reduce(add, prices),//(4) log//(5) );
JavaScript
복사
⇒ 이 코드와 위 기존 코드는 동일한 로직과 결과를 가져옵니다. 코드의 라인수는 조금 더 길어졌지만, 가독성은 많이 높아졌습니다. books를 첫 번째 인자로 시작해서 (2) 함수인 filter에 넘겨주고 해당 로직 수행 뒤 반환값을 (3),(4)로 넘겨주며 (5)에서 출력을 하게됩니다.
위에서부터 라인별로 로직을 작성하여 알아보기도 편하고 반환값이 무엇인지 확인하기도 편합니다.

go + curry를 사용하여 더 읽기 좋은 코드로 만들기

개요

go를 이용해 함수중첩코드를 리팩토링했지만 여기서 currying을 통해 한 번 더 리팩토링을 할 수 있습니다.

Currying이란?

⇒ 여러 개의 인자를 가진 함수를 호출 하는 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면 누락된 파라미터를 인자로 받는 기법.
즉, 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것입니다. 부분적으로 적용된 함수를 체인으로 계속 생성해서 결과적으로 값을 처리하도록 하는것이 본질입니다.

Currying 예제

const basicCurried = (first) => { return (second) =>{ console.log(first + second); } } basicCurried("Hello, ")("World"); // Hello, World basicCurried("Hello, "); // function(second){ console.log("Hello, "+ second);} const processCurried = basicCurried("Hello, "); processCurried("Universe.") // Hello, Universe
JavaScript
복사
⇒ 두 개의 문자(first, second)를 받아 결합해 출력하는 함수를 만들고자 하는데, 접두사, 혹은 접미사중 하나는 정해졌지만 다른 하나는 나중에 인자로 받고싶은 경우가 있습니다.
그럴경우 커링을 사용하면 위 코드처럼 하나의 함수만 사용한 함수가 반환되고 이것은 나중에 나머지 인자값을 전달할 때 사용할 수 있습니다.

Currying 함수 만들기

위의 기본적인 Currying함수예제처럼 특정 매개변수가 항상 비슷한 경우 prcessCurried 처럼 일부 인자값만 적용하여 새로운 함수를 생성해서 사용할 수 있습니다. 하지만, 로직이 복잡한 경우 커링 함수를 만들 때 함수를 중첩하고, 인자를 ()로 분리하고, 새 함수를 호출할 수 있게 만들어야 합니다.
이럴 경우 함수의 이름과 인자만 가지고 사용할 수 있는 currying 함수를 만들어 사용하면 됩니다.
function curryIt
const curryIt = (uncurried) =>{ let parameters = Array.prototype.slice.call(arguments, 1);//(0) return () => { return uncurried.apply(this, parameters.concat( //(1) Array.prototype.slice.call(arguments, 0) )); }; };
JavaScript
복사
→ 첫 번째인자(uncurried)로 함수를 받습니다.
→ (0) : 첫 번째 인자를 제외한 나머지 인자들을 parameters에 넣어둡니다.
→ (1) : 추후 curryIt을 통해 만든 Currying함수에 인자를 전달해 함수를 호출할 경우 기존 Currying함수를 선언할 때 미리 전달한 parameters들과 concat으로 합쳐서 완성된 인자값을 전달해서 최종 함수를 수행하도록 합니다.
curryIt 사용 예제
function curryIt(uncurried){ let parameters = Array.prototype.slice.call(arguments, 1); return function(){ return uncurried.apply(this, parameters.concat( Array.prototype.slice.call(arguments, 0) )); }; }; const greeter = (greeting, seperator, emphasis, name) =>{ console.log(greeting + seperator + name + emphasis); } const greetHello = curryIt(greeter, "Hello", ", ", "."); //Hello, hansol.
JavaScript
복사
주의점 - 인자 순서
⇒ 커링 함수에서 인자의 순서는 매우 중요합니다. 앞에 있는 인자일수록 변동가능성이 낮아야하고 뒤로갈수록 변동가능성이 높습니다. 그렇기 때문에 커링 함수를 만들 때 인자 순서를 잘 고려해서 구현해야 합니다.

Curry 함수 만들어보기

const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
JavaScript
복사
⇒curry함수는 인자값으로 함수(f)를 받아서 함수를 리턴합니다. ((a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);) 그리고 반환된 함수가 실행되었을 때 인자값이 2개 이상일 경우(.length?) 즉시 함수를 실행합니다.(f(a,..._))) 인자값이 2개 미만일 경우 함수를 다시 반환 후 ( (..._) => f(a, ..._)) 가지고 있다가 나중에 인자값을 받으면 (..._) 받아놨던 인자와 합쳐 실행합니다.( f(a, ..._) )
사용해보기
const mult = curry((a, b) => a * b); log(mult);//(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._) log(mult(1)); //(..._) => f(a, ..._) log(mult(2,3)); // 6 log(mult(2)(3)); //6
JavaScript
복사
인자값 없이 mult 호출 시 currry함수만 호출되는걸 볼 수 있습니다. 출력된 함수내용 중 f부분에 mult에서 인자값으로 넘겨 준 함수가 들어가는 것입니다.
인자값을 하나만 줬을 경우에는 _.length가 false가 되어 (..._) ⇒ f(a,..._)가 반환됩니다.
인자값을 2개(2,3)를 넘겨주면 _.length가 true로 f(a, ..._)함수가 즉시 실행되어 6을 반환합니다.
인자값을 순차적으로 2개(mult(2)(3))를 넘겨주면 첫 인자값을 넘겨주며 함수가 반환되고((..._) ⇒ f(2,..._)) 그 다음 인자값인 (3)을 넘겨주며 함수가 실행되어 6이 반환됩니다.

map+reduce+filter 중첩함수를 Currying으로 리팩토링

⇒ 기존에 낮은 가독성의 함수형 중첩함수를 go를 통해 리팩토링했습니다. 이번에는 Currying을 통해 한 번 더 리팩토링 하겠습니다.
1.
map, reduce, filter함수에 curry함수로 wrapping해주기
const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._); const map = curry((f, iter) => { let res = []; for (const i of iter) { res.push(f(i)); } return res; }); const filter = curry((f, iter) => { let 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 코드에 적용 */ go( books, books => filter(p => p.price < 30000)(books), books => map(p => p.price)(books), prices => reduce(add)(prices), log )
JavaScript
복사
괄호만 나눠진것이지 무엇이 장점일까요? 각 함수에 books, prices등을 인자로 받아 함수에서 2차 함수로 해당 파라미터를 넣어주고있습니다. 이것은 하나로 합칠 수 있습니다.
go( books, filter(p => p.price < 30000), map(p => p.price), reduce(add), log )
JavaScript
복사

함수 조합으로 함수 만들기

개요

같은 함수를 가지고 다른 로직을 수행하는 함수들이 있습니다
//30,000 원이상의 제품의 총 가격을 구하는 함수 go( books, filter(p => p.price >= 30000), map(p => p.price), reduce(add), log ) //30,000 원미만의 제품의 총 가격을 구하는 함수 go( books, filter(p => p.price < 30000), map(p => p.price), reduce(add), log )
JavaScript
복사
위 두 개의 함수를 보시면 목적은 다르지만 중복되는 코드들이 몇개 보입니다. (map, reduce, log ...) 이런 중복 코드를 조합해서 중복을 제거할 수 있습니다.(모듈화)
모듈화 - 1차
const total_price = pipe( map(p => p.price), reduce(add) ); go( books, filter(p => p.price < 30000), total_price, log )
JavaScript
복사
기존 로직에서 map(p => p.price)과 reduce(add) 함수를 따로 빼서 total_price라는 파이파함수로 모듈화했습니다.
모듈화 - 2차
const total_price = pipe( map(p => p.price), reduce(add) ); const base_total_price = predi => pipe( filter(predi), total_price ); go( books, base_total_price(b=>b.price >=30000), log ) go( books, base_total_price(b=>b.price <30000), log )
JavaScript
복사
1차 모듈화후에도 filter와 total_price라는 두개의 함수가 인자값을제외하면 중복되기에 모듈화가 가능합니다. (base_total_price)