[JavaScript] 함수형 프로그래밍 (go, pipe, curry)

 

 

 

- 데이터

const products = [
  { name: '반팔티', price: 15000 },
  { name: '긴팔티', price: 20000 },
  { name: '핸드폰케이스', price: 15000 },
  { name: '후드티', price: 30000 },
  { name: '바지', price: 25000 }
]

 

 

 

# go

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

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

go() 함수는 함수들과 인자를 전달해서 즉시 어떤 값을 평가한다. go() 함수는 5가지의 인자를 함수를 받는다. go의 인자는 rest 파라미터를 이용하여 여러가지 인수를 배열로 받는다. rest 파라미터는 배열이고 이터러블 프로토콜을 만족한다. 차례대로 함수를 실행하여 값을 도출하기 위해 reduce() 함수를 사용하여 축약된 값을 반환하고 마지막은 console.log 함수를 통해 결과를 로그로 출력한다.

 

- go 함수 이용하기

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

기존의 map, filter, reduce 함수를 사용했을 때는 아래에서 위, 오른쪽에서 왼쪽으로 읽어야하는 느낌이 강했지만 go 함수를 사용하게되면 차례대로 위에서 아래, 왼쪽에서 오른쪽으로, 연속적으로 평가할 수 있다.

 

 

# pipe

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

const f = pipe(
               a => a + 1, 
               a => a + 10,
               a => a + 100
              );
  
console.log(f(0));

pipe 함수는 함수들이 나열된 합성된 함수를 리턴하는 함수이다. pipe 함수는 여러개의 함수들을 인자로 받아 연속적으로 실행하여 축약하는 하나의 함수를 만들어 리턴한다. pipe 함수의 인자로 여러가지 함수(fs)를 받고 함수를 시작하는데 쓰는 인자(a)를 나중에 받고, 받은 인자를 가지고 go()함수를 실행하면서 인자(a)와 함수(fs)를 전달한다.

 

- go, pipe 변형

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

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

const f = pipe(
               (a, b) => a + b, 
               a => a + 10,
               a => a + 100
              );
  
console.log(f(0, 1)); // 111
// console.log(f(add(0, 1))); // pipe() 함수의 이점이 사라짐

만약 인자를 2가지 이상을 받아서 평가된 값으로 함수를 진행시킨다면 go() 함수의 경우는 2개 이상의 인자를 전달 받고 평가하는 함수를 지정하면 된다. 하지만 pipe() 함수는 f(add(0, 1))의 형식으로 인자를 전달해야 하는데 이 방식은 pipe() 함수로 사용하기에 다소 아쉽다. 그렇기 때문에 pipe() 함수에서 인자를 전달 받는 방식을 변경한다. pipe() 함수의 첫번째 인자로 들어갈 함수(f)는 따로 지정하고 리턴할 함수의 인자(a)를 여러개를 받아야하기 때문에 (...as)로 받는다. go() 함수의 인자로 전달될 부분도 마찬가지로 첫번째 인자와 나머지 전달될 인자를 구분한다.

 

 

# curry

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

const mult = curry((a, b) => a * b);
console.log(mult(3)); // (..._) => f(a, ..._);
console.log(mult(3)(2)); // 6

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

curry는 함수를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수이다. 먼저 함수를 받아서 함수를 리턴하고 인자를 받아서 인자가 원하는 갯수만큼 일때 받아두었던 함수를 평가시킨다. mult() 함수는 두개의 인자를 받고 곱하는 함수이다. 이 함수를 curry() 함수로 호출하면 두번째 인자까지 받았다는 조건(_.length ?)이 만족되면 실행(f(a, ..._)하고 첫번째 인자까지 받았다면 함수((..._) => f(a, ..._))를 반환한다.

 

- go와 curry의 조합

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

const map = curry((f, iter) => {
  let res = [];
  for (const a of iter) {
    names.push(f(a));
  }
  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(
//    products,
//   products => filter(p => p.price < 20000)(products),
//    products => map(p => p.price)(products),
//    prices => reduce(add)(prices),
//    console.log
//   ); // 30000  
  
go(
   products,
   filter(p => p.price < 20000),
   map(p => p.price),
   reduce(add),
   console.log
  ); // 30000

기존에 만들었던 map, filter, reduce에도 curry를 적용했다. 하여 이 세 함수가 인자를 하나만 받으면 이후 인자를 더 받기로 기다리는 함수를 리턴하도록 되어있다. go() 함수를 살펴보면 기존에 함수(filter, map, reduce)를 첫번째 인자로 받고 두번째 인자에 데이터(products)를 받았는데, curry() 함수를 통해서 (map or filter or reduce)(products or price) 형식으로 첫번째 인자를 먼저 실행하면서 두번째 인자를 이후에 실행할 수 있다. 

또한, products를 받아서 함수(map or filter or reduce)에 그대로 products를 전달한다는 것은 해당하는 함수가 products를 받는다는 이야기고 products => (map or filter or reduce())(product) 함수가 products를 받는다는 얘기는 products를 제외하고 map or filter or reduce() 만으로 사용할 수 있다.

 

 

# 함수 조합으로 함수 만들기

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

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

go(
   product,
   base_total_price(p => p.price < 20000),
   console.log
  ); // 30000
  
go(
   product,
   base_total_price(p => p.price >= 20000),
   console.log
  ); // 75000

 

total_price라는 pipe 함수를 만들어서 가격의 축약된 값을 도출하는 함수를 만들어 중복을 제거한다. 또, base_total_price라는 predi 함수를 전달 받는 pipe 함수를 만든다. 이 함수의 경우는 predi함수로 전달받은 함수를 filter 함수의 인자로 pipe를 리턴하는 함수이다. 이러한 방식들로 고차함수들을 통해 함수의 조합으로 만들어가면서 함수를 잘게 나누면서 중복을 제거하며 더 많은 곳에서 사용할 수 있다.

 

 

[JavaScript] 함수형 프로그래밍 (map, filter, reduce)

 

 

 

- 데이터

const products = [
  { name: '반팔티', price: 15000 },
  { name: '긴팔티', price: 20000 },
  { name: '핸드폰케이스', price: 15000 },
  { name: '후드티', price: 30000 },
  { name: '바지', price: 25000 }
];

 

 

# map

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

함수의 인자로 받은 iter를 통해 순회를 하고 새로운 배열의 형태를 만든다. 첫번째 인자 f는 이터러블의 어떤 값을 수집할 것인지 추상화한다. 그렇기때문에 받아온 함수(f)를 통해서 어떤 값을 수집할 것인지 f 함수에게 위임한다.

 

let names = [];
for (const a of products){
  names.push(p.name);
}

console.log(names); // ["반팔티", "긴팔티", "핸드폰케이스", "후드티", "바지"]
console.log(map(p => p.name, products)); // ["반팔티", "긴팔티", "핸드폰케이스", "후드티", "바지"]


let prices = [];
for (const a of products){
  prices.push(p.price);
}
console.log(prices); // [15000, 20000, 15000, 30000, 25000];
console.log(map(p => p.price, products)); // [15000, 20000, 15000, 30000, 25000];

함수형 프로그래밍에선 map이라는 함수의 보조함수를 통해서 이터러블 안에 있는 수집하고자 하는 어떠한 값을 수집할 수 있다. map 함수는 함수를 값으로 다루면서 원하는 시점에 인자를 적용하는 고차함수이다.

 

 

# 이터러블 프로토콜을 따른 map의 다형성

console.log([1, 2, 3].map(a => a + 1)); // [2, 3, 4]
console.log(document.querySelectorAll('*')); // undefined

[1, 2, 3]은 배열(Array)이고 이터러블이다. 하지만 querySelectorAll(NodeList)은 이터러블이긴 하지만 배열은 아니므로 Array.prototype을 상속하고 있지 않다. 따라서 map함수를 사용할 수 없다.

 

console.log(map(el => el.nodeName, document.querySelectorAll('*')));

const it = document.querySelectorAll('*')[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

querySelectorAll은 이터러블이므로 [Symbol.iterator]() 메서드를 가지며 next()메서드로 값을 순회할 수 있다.

 

function* gen(){
  yield 2;
  if(false) yield 3;
  yield 4;
}

console.log(map(a => a * a, gen())); // [4, 16]

querySelectorAll 말고도 이터러블을 만족하는 것이라면 위에서 만들어놓은 map함수를 사용할 수 있다. gen()이라는 제너레이터 함수 또한 map으로 순회 가능하다. 이터러블 프로토콜을 따르는 이미 만들어져있는 이터러블에 map을 사용할 수 있지만 gen() 제너레이터 함수의 결과(문장)에 대해서도 map을 사용하여 순회할 수 있다. 그렇다는 것은 사실상 모든것들에 대해 map을 할 수 있다.

 

앞으로 만들어지는 WEB API 자바스크립트를 포함한 브라우저에서 사용되는 값들(querySelectorAll) 또한 ECMAScript의 이터러블 프로토콜을 따르고 있기때문에 계속해서 이러한 형태로 만들어질 것이다. 이터러블 프로토콜을 따르는 함수를 사용하는 것은 앞으로의 많은 다른 함수와의 조합성이 좋아진다. 훨씬 더 유연하고 다형성이 있는 기법을 사용할 수 있을것이다.

 

- Map

let m = new Map();
m.set('a', 10);
m.set('b', 20);

console.log(map(([k, a]) => [k, a * 2], m)); // [ ["a", 20], ["b", 40] ]
console.log(new Map(map(([k, a]) => [k, a * 2], m))); // { "a" => 20, "b" => 40 }

처음에 만든 Map객체와 동일하게 내부적으로 로직처리를 하고 난 후(*2)의 새로운 Map 객체를 map함수를 통해서 만들 수 있다.

 

 

# filter

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

map과 형태는 동일하다. 하지만 첫번째 인자로 받는 함수(f)를 가지고 어떤 조건을 가지고 로직을 처리할 것인지에 대해 체크하는 부분을 받아온 함수에 위임한다.

 

let under20000 = [];
for (const a of products){
  if(p.price < 20000) under20000.push(p);
}
console.log(...under20000); // { name: "반팔티", price: 15000 } { name: "핸드폰케이스", price: 15000 }

console.log(...filter(p => p.price < 20000, products)); // { name: "반팔티", price: 15000 } { name: "핸드폰케이스", price: 15000 }

let over20000 = [];
for(const a of products){
 if(p.price >= 20000) over20000.push(p);
}
console.log(...over20000); // { name: "긴팔티", price: 20000 } { name: "후드티", price: 20000 } { name: "바지", price: 25000 }

console.log(...filter(p => p.price >= 20000, products); // { name: "긴팔티", price: 20000 } { name: "후드티", price: 20000 } { name: "바지", price: 25000 }

내부에 있는 값에대한 다형성은 첫번째 인자로 받은 보조함수를 통해서 지원을 해주고 외부의 경우는 두번 째 인자로 받은 이터러블 프로토콜을 따르는 인자를 통해서 다형성을 지원해줄 수 있다.

 

console.log(filter(n => n % 2, [1, 2, 3, 4])); // [1, 3]
console.log(fitler(n => n % 2, function* () {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}())); // [1, 3, 5]

filter함수 역시 이터러블 프로토콜을 따르고 함수형적인 코딩을 통해서 중복을 제거하고 구현할 수 있다.

 

 

# reduce

const reduce = (f, acc, iter) => {  
  for(const a of iter){
    acc = f(acc, a)
  }
  return acc;
}

reduce는 값을 축약하는 함수이다. 특정한 값들을 순회하면서 하나의 값으로 누적할 때 사용하는 함수이다. 

 

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

console.log(reduce(add, 0, [1, 2, 3, 4, 5]); // 15

// console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5)); 형태로 실행된다.

reduce 함수는 첫번째 인자로 로직을 처리할 함수, 두번째 인자로 초기값, 세번째 인자로 이터러블을 받는다.

 

console.log(reduce(acc, [1, 2, 3, 4, 5]); // 15

초기 값을 전달하지 않아도 reduce 함수는 똑같은 값을 도출하는데 그 이유는

 

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;
}

위의 if문의 로직과 같이 reduce 함수의 파라미터의 초기값을 누락하고 호출하게되면 reduce 내부의 로직에서 acc에 들어온 이터러블의 [Symbol.iterator] 메서드를 실행하고 초기 값에 iter 이터러블의 next()를 실행한 값을 넘겨주기때문에 초기값이 없더라도 같은 값으로 실행된다.

 

console.log(reduce(
  (total_price, product) => total_price + product.price, 
  0, 
  products)); // 105000

reduce는 보조함수를 통해서 축약하는 것을 완전히 위임하기 때문에 숫자나 숫자 배열 뿐만아니라 객체 등의 복잡한 값들을 특정한 값으로 축약할 수 있다. reduce도 역시 보조함수를 통해서 안쪽에 있는 값의 다형성을 지원해주며 이터러블을 통해서 외부값의 다형성도 지원한다.

 

 

# map, filter, reduce

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

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

map과 filter, reduce 함수가 중첩되어있는 함수이다. 이렇게 작성된 함수들은 뒤부터 읽으면 쉽게 읽을 수 있다. 함수를 작성할 때 내부에서 사용하려는 함수와 외부에서 평가될 이터러블의 값을 유추하면서 작성한다.

 

 

+ Recent posts