히바리 쿄야 와 함께 하는 Developer Cafe
[2일차] DO IT 타입스크립트 프로그래밍/p178 ~ p 258/ 함수형 프로그래밍, 람다 본문
함수형 프로그램은 함수와 선언형 프로그래밍의 함수 조합과 모나드 조합 으로 코드를 설계 하고 구현하는 기법
타입스크립트 제네릭 구문
function g1<T>(a: T): void {}
function g2<T, Q>(a: T, b: Q): void {}
# Compose
compose는 컨베이어 벨트라고 생각하면 된다. 일련의 함수들(=컨베이어 벨트의 기계들)이 나열되어 있고, 데이터(=조립할 대상)이 컨베이어 벨트에 input되는 것이다. 그러면 1번 함수가 input 받아 데이터에 변형을 가한 뒤, 2번 함수에게 전달하고, 2번 함수는 1번 함수의 결과물 데이터를 input 받아 함수 실행 후 output을 3번 함수에게 전달한다. 이렇게 일련의 과정을 거친 후에 최종적으로 원하는 결과물을 리턴시키는 것을 compose라고 한다.
정리하자면, Composition은 원하는 결과를 얻기 위해 데이터와 일련의 함수들을 배치시키고 구성하는 시스템 디자인 원칙이다.
참고로 compose는 자바스크립트에 내재된 개념은 아니며, 보통 Ramda 같은 라이브러리를 이용해서 사용한다고 한다.
const compose = (f, g) => (data) => f(g(data)); const multiplyBy3 = (num) => num*3; const makePositive = (num) => Math.abs(num); const multiplyBy3AndAbsolute = compose(multiplyBy3, makePositive); multiplyBy3AndAbsolute(-50); // 150 |
여기서는 compose를 라이브러리를 이용하지 않고 자체적으로 구현했다. 여기서 최초 input 데이터는 -50이고, 이를 조작할 함수(=컨베이어 벨트의 기계)는 multiplyBy3, makePositive 함수이다. compose 안에선 이들 함수를 컴포넌트라고 부른다. 이 컴포넌트들은 최대한 pure해야 한다.
# Pipe
pipe는 본질적으로는 compose와 동일한 개념인데, 단지 방향이 반대일 뿐이다.
const compose = (f, g) => (data) => f(g(data)); const pipe = (f, g) => (data) => g(f(data)); // compose와 순서가 다르다. 왼쪽부터 오른족으로. const multiplyBy3 = (num) => num*3; const makePositive = (num) => Math.abs(num); const multiplyBy3AndAbsolute = compose(multiplyBy3, makePositive); multiplyBy3AndAbsolute(-50); // 150 |
compose는 컴포넌트 실행의 순서가 오른쪽 -> 왼쪽이라면, pipe는 왼쪽 -> 오른쪽이다. 본인 개인의 선호(?)에 따라 달리 사용하면 된다고 한다.
fn1(fn2(fn3(50))); compose(fn1, fn2, fn3)(50); // fn3 실행 후, fn2 실행 후, fn1 실행(오른쪽 -> 왼쪽) pipe(fn3, fn2, fn1)(50); // fn3 실행 후, fn2 실행 후, fn1 실행(왼쪽 -> 오른쪽) |
현재 ramda 패키지에서 제공하는 함수 목록은 다음과 같습니다. 대강 살펴만 보아도 ramda가 제공하는 함수는 200개 남짓입니다. 다 살펴보기보다 자주 사용하는 것부터 살펴봅시다. 앞으로 사용하면서 주기적으로 여기에 정리해보겠습니다.
ramda가 제공하는 대부분의 함수는 2차 고차 함수 형태로 이뤄져 있습니다.
때문에 R.func()() 아니면 const a = R.func(); a(); 형태로 주로 사용하니 익숙해집시다.
const numbers = R.range(1, 10); // 커링 const incNumbers = R.pipe( R.map(R.inc), R.tap((n) => console.log("after", n)) )(numbers); // 커링 안하고 partial func를 만들어 사용하기 const incNumbers = R.pipe( R.map(R.inc), R.tap((n) => console.log("after", n)) ); incNumbers(numbers);
또한, 2차 고차 함수는 포인트 없는 함수 형태로도, 있는 함수로도 나타낼 수 있습니다. 포인트가 있다는 것은 부분 함수의 형태를 쓰지 않고 한 번에 모두 호출하는 것이고, 포인트가 없다는 것인 부분 함수로 한 번 나눠서 사용하는 것을 의미합니다.
양 쪽 모두에 익숙해져야 합니다.
import * as R from "ramda"; // 포인트가 있는 함수 const pointFunc = (val: number) => R.add(1)(val); console.log(pointFunc(3)); // 포인트가 없는 함수 const pointLessFunc = R.add(1); console.log(pointLessFunc(3));
함수
R.range(n, n+a)
import * as R from "ramda"; console.log(R.range(0, 5)); // [ 0, 1, 2, 3, 4 ]
R.add(a, b), R.subtract(a, b), R.multiply(a, b), R.divide(a, b)
단순한 사칙 연산입니다. ramda 라이브러리 함수는 자동 커리가 되기 때문에 분할해서 이중 호출을 할 수도 있습니다.
console.log(R.add(2, 3)); // 자동 커리됨 console.log(R.add(2)(3));
R.adjust( index, function, list)
list의 index에 해당하는 값에 function을 적용합니다.
// ["a", "b", "c"] 배열의 0번 째 인덱스에 R.toUpper를 적용합니다. R.adjust(0, R.toUpper, ["a", "b", "c"]); // [ 'A', 'b', 'c' ]
R.tap( callback func )
단순한 출력입니다. 콘솔로 찍으면 되는데 왜 tap을 사용해야 하느냐고 궁금증이 생길 수 있는데 R.pipe등 연쇄적인 작업을 할 때 도중에 결과를 확인하기 위함입니다. R.pipe 내부에서는 console.log 문을 직접 사용할 수 없기 때문에 R.tap이 필요한 것이라고 생각하시면 됩니다.
// 간단한 사용 const arr = R.range(0, 10); R.tap((n) => console.log(n))(arr); // 실사용 const numbers = R.range(1, 10); const incNumbers = R.pipe( R.map(R.inc), R.tap((n) => console.log("after", n)) )(numbers);
R.curryN(N, 함수)
고차함수를 만드는데 유용하게 사용됩니다.
const sum = (...numbers: number[]): number => numbers.reduce((sum, number) => sum + number); // 3차 고차 함수 console.log(sum(sum(sum(1), 2), 3)); // curryN을 통해 3차 고차 함수로 변형 const curriedThree = R.curryN(3, sum); console.log(curriedThree(1, 2, 3));
'TypeScript' 카테고리의 다른 글
[4일차] DO IT 타입 스크립트 프로그래밍/p294~p338/ 모나드 (0) | 2021.04.12 |
---|---|
[3일차] DO IT 타입 스크립트 프로그래밍/ p260 ~ p292/ 제네릭 (0) | 2021.04.12 |
[1일차] DO IT 타입스크립트 프로그래밍/p14~p176/타입스크립트 생성, 관리, 객체와 클래스,함수와 메서드, promise와 async,await (0) | 2021.03.26 |