손으로 연습하는 함수형 프로그래밍 1

With·2022년 12월 11일
0

요즘 함수형 프로그래밍에 빠져있는 듯 하다. 선언적인 코드와 유연하게 함수를 자르고 붙임으로써 여러 결과를 얻을 수 있는 장점이 있는 듯하다. 아직 실제 구현 코드에서 이것을 자연스럽게 사용하는 것이 익숙하지 않아서 정확히 함수형 프로그래밍이란 '이것'이다 라고 할 순 없는 수준이다. 그래도 계속 반복적인 연습과 익숙해지기 위해 연습을 많이 해보고 있다.

함수형 프로그래밍에 대한 여러 이론적 개념도 중요하겠지만 함수를 붙이고 자르고 하는 것이 함수형 프로그래밍에 있어서 가장 기본이 되는 테크닉이고 이것을 내가 자유자재로 무의식적인 수준에서 가지고 놀 수 있어야 겠구나... 하는 생각이 들었다. 여러가지가 있지만 이 글에서는 curry, go, pipe 같은 기능을 하는 함수가 있는데 이것을 간단하게 설명해보고자 한다.

아래 내용을 이해하기 위해서는 iterable, iterator에 대해서 어느정도 이해를 하고 있어야 한다.

go

const go = (...args) => {
  return reduce((acc, el)=> el(acc), args);
};

go함수는 인자로 받은 값과 함수를 연속적으로 실행시켜서 결과로 값을 반환하는 함수이다. 여기서 값이란, 함수가 아니라는 뜻이다.

go 함수의 특징은 인자로 입력되는 함수들이 연쇄적으로 실행되는데 이전 함수의 결과가 다음 실행될 함수의 인자로 다시 입력이 되는 형태를 가진다.

이 말이 무슨 말인지 아래 코드를 보면서 설명한다. go 함수에 첫번째 인자로 0, 그리고 3개의 함수를 인자로 넣었다. 이 경우 0이 다음 함수의 인자로 들어가서 두번째 인자이자, 첫번째 함수는 1을 반환한다. 그리고 다시 그 1일이 다음 함수에 인자로 들어가서 1 + 10 = 11 이라는 값을 반환하고 마찬가지로 11 이라는 값이 세번째 함수의 인자로 들어가서 11 + 100 = 111 이라는 값을 반환하고 마지막으로 111이 console.log의 인자로 들어가서 화면에 값이 출력되고 go 함수가 종료된다.

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

이것이 가능한 이유는 go 내부에 있는 reduce 함수로 인해 가능하다. js array proto가 제공하는 기본 메서드와는 아주 조금 다르다.

const reduce = (f, acc, iter) => {
  	// iter가 없다면, acc의 iteator를 iter로 대입하고,
  	// 그 첫번째 값을 acc로 대입한다.
	if(!iter){
      iter = acc[Symbol.iterator]();
      acc = iter.next().value;
    }
  
  	for(const el of iter){
    	acc = f(acc, el);
    }
  
  return acc;
}

Array뒤에 붙어서 (e.g. Array.reduce()) 사용되는 것과 달리 직접 대상 배열, 더 정확히 말하면 iterable을 인자로 받는다. 그리고 Array 메서드가 아니기때문에 iterable은 모두 이 커스텀 reduce를 사용할 수 있다.

함수 구현 중간에 있는 로직으로 iterable를 인자로 받지 않았을때는 해당 값의 첫번째 값은 acc로 사용되도록 기능이 갖춰져 있다.

아무튼 이 reduce를 통해 go에서는 인자로 받은 함수를 연속적으로 실행하고 그것의 값을 acc로서 반환할 수 있는 것이다. 참고로 go에서 인자를 ... 를 통해 받고 있는데 이것도 역시 iterable 이기때문에 reduce를 사용하여 go를 만들 수 있게되는 것이다.

pipe

pipe도 go와 비슷한 기능을 하는데 go와 다른 점은 값을 반환하는 것이 아니라 함수를 반환한다는 점이다. 즉 go는 인자를 입력했을 때 즉각적으로 연산이 되어 값이 반환된다면 pipe는 함수를 반환함으로써 지연 실행을 할 수 있다.

그래서 pipe는 go함수를 이용해서 만들 수 있는데, go를 지연 실행하는 식으로 만들면 결국 그게 pipe가 된다는 점을 알게 된다.

// go
const delayGo = (a) => go(a, ...args);

// pipe
const pipe = (...fs) => {
  return (a) => go(a, ...fs);
}

그래서 이렇게 작성하면 go와 동일하게 작동하는 것을 알 수 있다.

const readyPipe = pipe(
  a => a + 1,
  a => a + 10,
  a => a + 100,   
)

const r = readyPipe(0) // 111

정리

정리하자면 gopipe 모두 여러 함수의 동작을 합쳐주는 기능을 가진 함수다. 어떤 데이터를 넣고 그 데이터에 작동시킬 여러 함수, 가령 map, filter, reduce 와 같은 함수들을 인자로 넣어서 결과를 얻는 것이다.

go는 그 값을 즉각적으로 받을 수 있고, pipe는 일단 적용시킬 함수를 먼저 넣고 나중에 데이터를 넣어 값을 얻을 수 있다. 간단하지만 조금 더 실용적인 예시를 든다면 아래 코드와 같다.

// data
const products = [
  {
    name: "반팔티",
    price: 15000,
    q: 1
  },
  {
    name: "긴팔티",
    price: 20000,
    q: 2
  },
  {
    name: "핸드폰케이스",
    price: 15000,
    q: 3
  },
  {
    name: "후드티",
    price: 30000,
    q: 4
  },
  {
    name: "바지",
    price: 25000,
    q: 5
  }
];

const goResult = go(
  products, (products) => map(p => p.price, products), 
  products, (products) => reduce((acc, price) => acc + price, products), 
  console.log // product의 가격의 합이 반환
)


const redayPipe = pipe(
  (products) => map(p => p.price, products),
  (products) => reduce((acc, price) => acc + price, products)
)

const pipeResult = readyPipe(products) // // product의 가격의 합이 반환

curry

여기에서 끝나지 않고, 커링이라는 개념이 있다. 커링이란, 여러 인자를 받는 함수를 단일 인자로 받는 여러개의 함수로 구성한 것을 의미한다. 처음부터 함수를 그렇게 만들어도 되지만, 우리가 만든 함수가 모두 커링이 적용되어 있을리가 없다.

그래서 커링이 적용되어 있지 않은 함수가 커링이 된 것처럼 함수의 모양을 바꿀 수 있도록 기능을 제공하는 함수를 만들어서 사용을 한다.

무슨 말이나면, 아래 처럼 원래 있던 함수를 커링된 함수로 만들어주는 중간 유틸 함수를 만든다고 생각하면 된다.

// 2개의 인자를 받는 함수를
const showNumber = (a, b) => a + b

// 이렇게 커링을 해서 
const curriedShowNumber = curry(showNumber);

// 이렇게 사용할 수 있도록 만들어주는 것이다.
curriedShowNumber(1)(2) // 3

커리 함수의 구현은 아래와 같다. 일단 curry는 함수를 리턴하는 함수이며 가장 첫 인자로 함수를 받는다. 그리고 리턴된 함수에서 2가지로 분기가 되는데,

리턴된 함수의 인자의 갯수가 1개 초과인 경우라면 그냥 가장 처음에 받았던 함수를 실행시켜버린다. 근데 만약 인자의 갯수가 1개라면, 또 다시 함수를 리턴하고 그 함수에서 가장 처음에 받았던 함수를 실행한 값을 리턴하도록 만든다.

이 구현으로 인해 만약 curried 함수가 인자를 1개 이상을 받더라도 비록 커링이 실제로 적용되진 않았지만 그냥 함수 본연의 기능은 실행되게 된다.

const curry = (f) => (arg, ..._) => _.length ? f(arg, ..._) : (..._) => f(arg, ..._);
profile
주니어 프론트엔드 개발자 입니다.

0개의 댓글