[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 함수가 중첩되어있는 함수이다. 이렇게 작성된 함수들은 뒤부터 읽으면 쉽게 읽을 수 있다. 함수를 작성할 때 내부에서 사용하려는 함수와 외부에서 평가될 이터러블의 값을 유추하면서 작성한다.

 

 

[JavaScript] 함수형 프로그래밍 (제너레이터, 이터레이터)

 

 

 

# 제너레이터 / 이터레이터

 - 제너레이터: 이터레이터이자 이터러블을 생성하는 함수

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

let iter = gen();
// console.log(iter[Symbol.iterator]() == iter); // true
console.log(iter.next()); // { value: 1, done: false } 
console.log(iter.next()); // { value: 3, done: false } 
console.log(iter.next()); // { value: undefined, done: true } 
console.log(iter.next()); // { value: undefined, done: true } 

for(const a of iter) console.log(a); // 1 3

제너레이터는 이터러블과 이터레이터의 조건을 만족한다. 따라서 제너레이터 함수의 객체를 이터러블을 만족하는 작업 없이도 for...of 문으로 순회 할 수 있다. 또한 제너레이터는 순회할 값을 문장(yield)으로 표현할 수 있다. 어떠한 값이든 이터러블이면 순회할 수 있고, 제너레이터는 문장을 값으로 만들수 있고 문장을 통해 순회할 수 있는 값을 만들 수 있기때문에 어떠한 상태나 어떠한 값이든 순회할 수 있도록 만들 수 있다.

 

이 점은 함수형 프로그래밍의 관점에서 중요하다. 자바스크립트는 다형성이 높다고 할 수있다. 제너레이터라는 문장을 통해 순회할 수 있는 값을 만들 수 있다는 것은 어떠한 값도 순회할 수 있는 형태로 동작할 수 있고 다양한 값을 순회하는 이터러블을 만들 수 있다.

 

 

# odds

function* infinity(i = 0){
  while(true) yield i++;
}

function* limit(l, iter){
  for (const a of iter){
    yield a;
    if(l == a) return;
  }
}

function* odds(l) {
  for (const a of limit(l, infinity(1))){
    if (a % 2) yield a;
  }
}

let iter = odds(10);
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: 5, done: false }
iter.next(); // { value: 7, done: false }
iter.next(); // { value: 9, done: false }
iter.next(); // { value: undefined, done: true }
iter.next(); // { value: undefined, done: true }

for(const a of odds(40)) console.log(a); // 1, 3, 5 ... 39

제너레이터를 이용해서 홀수만을 출력하는 함수를 만들었다. infinity() 제너레이터함수의 경우는 무한하는 while 문에서 i의 값을 1씩 증가시킨다. 겉으로 보기엔 함수를 실행하면 브라우저가 다운되겠지만 제너레이터 함수의 yield를 통해서 하나씩 값을 실행하고 실행을 중지 시키기때문에 그렇게 되지는 않는다.

 

limit() 제너레이터함수는 파라미터로 숫자와 이터러블을 만족하는 값을 받아온다. 받아온 이터러블을 순회하면서 인자로 받은 숫자와 이터러블의 값이 같아질 때 리턴하여 done: true를 반환한다.

 

odds() 제너레이터 함수는 limit() 제너레이터함수와 infinity() 제너레이터함수를 사용하여 홀수 값을 가져온다. limit과 infinity는 이터러블 프로토콜을 만족하므로 for...of를 순회하는 이터러블 값으로 사용될 수 있다.

 

 

# for of, 전개 연산자, 구조 분해 할당, 나머지 연산자

console.log(...odds(10)); // 1 3 5 7 9
console.log([...odds(10), ...odds(20)]); // [1, 3, 5, 7, 9, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

const [head, ...tail] = odds(5);
console.log(head); // 1
console.log(tail); // [3, 5]

const [a, b, ...rest] = odds(10);
console.log(a); // 1
console.log(b); // 3
console.log(rest); // [5, 7, 9]

자바스크립트에서는 이터러블 / 이터레이터 프로토콜을 가지고 활용할 수 있는 문법들과 기능들이 많다. 또한 많은 라이브러리나 함수들도 이터러블 프로토콜을 따르도록 구성이 되어있다면 제너레이터와 이터레이터를 활용하여 좀 더 조합성이 높은 프로그래밍을 할 수 있다.

[JavaScript] 함수형 프로그래밍 (이터러블, 이터레이터)

 

 

 

ES6부터 리스트를 순회하는 방법의 차이가 생겼다.

console.log('Array');
const arr = [1, 2, 3];
for(const a of arr) console.log(a); // 1 2 3

console.log('Set');
const set = new Set([1, 2, 3]);
for(const a of set) console.log(a); // 1 2 3

console.log('Map');
const map = ([['a', 1], ['b', 2], ['c', 3]]);
for(const a of map) console.log(a); // ["a", 1] ["b", 2] ["c", 3]
for(const a of map.keys()) console.log(a); // a b c
for(const a of map.values()) console.log(a); // 1 2 3
for(const a of map.entries()) console.log(a); // ["a", 1] ["b", 2] ["c", 3]

 

 

# 이터러블/이터레이터 프로토콜

 - 이터러블: 이터레이터를 리턴하는 [Symbol.iterator]() 를 가진 값

 - 이터레이터: { value, done } 객체를 리턴하는 next() 를 가진 값 (리절트 객체)

 - 이터러블 / 이터레이터 프로토콜: 이터러블을 for...of, 전개 연산자, 구조 분해 할당 등과 함께 동작하도록 만든 규약

arr[Symbol.iterator] // f values() { [native Code] }

let iterator = arr[Symbol.iterator]();
iterator.next() // { value: 1, done: false} 
iterator.next() // { value: 2, done: false} 
iterator.next() // { value: 3, done: false} 
iterator.next() // { value: undefined, done: true}

 

 

# 사용자 정의 이터러블을 통해 알아보기

const iterable = {
  [Symbol.iterator](){
    let i = 3;
    return {
      next(){
        return (i === 0) ? {done: true} : { value: i--, done: false };
      }
    }
  }
}

let iterator = iterable[Symbol.iterator]();
iterator.next();
for(const a of iterable) console.log(a); // 2 1
for(const a of iterator) console.log(a); // TypeError: iterator is not iterable

이터러블 / 이터레이터 프로토콜을 사용하기위해 사용자가 정의할때는 위의 [Symbol.iterator]() 메서드를 정의하고 [Symbol.iterator]() 메서드는 next() 메서드를 반환해야하며 next() 메서드의 내용으로는 { value, done } 을 가지는 객체를 반환해야 한다.

 

하지만 위의 이터러블 / 이터레이터는 완벽하지 않다. Array나 Set, Map 등 다른 이터러블 프로토콜을 만족하는 것들의 내부를 보면 이터러블 프로토콜을 만족하면서 이터레이터 프로토콜을 만족한다. 그러한 이터러블 프로토콜을 well-formed 이터러블 프로토콜 이라고 한다.

 

const iterable = {
  [Symbol.iterator](){
    let i = 3;
    return {
      next(){
        return (i === 0) ? {done: true} : { value: i--, done: false };
      },
      [Symbol.iterator]() { return this; }
    }
  }
}

let iterator = iterable[Symbol.iterator]();
iterator.next();
for(const a of iterable) console.log(a); // 2 1
for(const a of iterator) console.log(a); // 2 1

well-formed 이터러블 프로토콜은 이터러블을 for...of 문으로 순회를 해도 순회가되고 이터레이터로 만들어서 for...of 문으로 순회해도 순회가 된다. 또한 일정부분 next() 메서드를 통해 진행을 하고 순회를 해도 마찬가지로 순회가 된다.

 

console.log(document.querySelectorAll('*')) // NodeList(...) [...]
for(const a of document.querySelectorAll('*')) console.log(a); // <html>...</html> <head>...</head> ...
const all = document.querySelectorAll('*');
let iter = all[Symbol.iterator]();
iter.next(); // { value: html, done: false }
iter.next(); // { value: head, done: false }
iter.next(); // { value: script, done: false }

해당하는 이터러블 프로토콜은 오픈소스 라이브러리 뿐만 아니라 브라우저의 DOM 또한 이터러블 프로토콜을 만족한다.

 

 

# 전개연산자

const a = [1, 2];
// a[Symbol.iterator] = null; // null이라면 아래 코드 TypeError
console.log([...a, ...arr, ...set, ...map.keys()]); // [1, 2, 1, 2, 3, 1, 2, 3, "a", "b", "c"]

자바스크립트에서 ES6의 이터러블 / 이터레이터 프로토콜은 중요하다. 해당하는 프로토콜을 만족하면 기존에 ES5에서 사용하던 자료구조보다 더 다양하고 쉬운 방식으로 데이터들을 핸들링 할 수 있다.

+ Recent posts