함수형 프로그래밍과 javascript es6 구현하기 - iterator, iterable, generator, spread operator

함수형 프로그래밍과 javascript es6

  • 앞서 블로그는 es5를 기반으로 작성하였다.
  • es6 이후부터는 iterator, iterable, generator, spread operator 등 함수형 프로그래밍을 더 강력하게 만들어줄 기능이 추가되었다.
  • es5에서 구현한 함수를 es6에 맞춰 재작성 하였다.

전개 연산자 Spread Operator

  • es6부터 전개 연산자가 생겼다.
  • 컬렉션 map과 set이 생겼다.
  • 전개연산자와 사용하면 set, map을 손쉽게 array로 변환할 수 있다.
  • https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax
const log = console.log;

let arr = [1,2,3,4];
let set = new Set([5,6,7,8]);
let map = new Map([['name', 'kim'], ['age', 10]]);
const combined = ['a', 'b', ...arr, ...set, ...map.keys(), ...map.values()];
log(combined);

구조 분해

  • 전개 연산자를 역전시켜서, 배열로부터 변수에 값을 부여할 수 있다. 이를 구조 분해라 한다.
  • a, b는 첫 번째 인자와 두 번째 인자를 의미한다. ...args는 3번째부터 마지막까지를 의미한다.
const [a, b, ...args] = [1,2,3,4,5,6,7];
log(a);
log(b);
log(args);

iterable 과 iterator

  • 함수형 프로그래밍으로의 기능을 극대화하기 위하여 for...i 구문이 아닌 for...of 구문을 신설하였다.
  • for...i 구문은 기본적으로 평가를 즉각 수행한다. iterator는 요청할 때마다 값을 하나씩 꺼내는 형태로 지연 평가를 구현하는 것에 유리하다.

  • iterable한 객체는 [Symbol.iterator] 를 구현한다.
  • array, map, set 은 iterable 하다. document.querySelector의 출력값 역시 iterable하다.
  • map은 keys, values, entries로 iterator를 출력할 수 있다.
const log = console.log;

const arr = [1,2,3,4];
const set = new Set([5,6,7,8]);
const map = new Map([['name', 'kim'], ['age', 10]]);

for (const i of arr[Symbol.iterator]()) log(i);
for (const i of set[Symbol.iterator]()) log(i);
for (const i of map.keys()[Symbol.iterator]()) log(i);
for (const i of map.values()[Symbol.iterator]()) log(i);
for (const i of map.entries()[Symbol.iterator]()) log(i);

const all = document.querySelectorAll('*');
// for...of와 for...i 의 결과는 동일하다.
for (const e of all) log(e);
for (let i = 0; i < all.length; i++) log(all[i]);

사용자 정의 iterable

  • Symbol.iterator를 통해 iterator를 구현할 수 있다.
  • next()[Symbol.iterator]() 함수를 구현한다.
const count3 = {
    [Symbol.iterator]() {
        let i = 3; // 클로져로서 값을 가진다.
        return {
            next(){ // 
                return i == 0? {done : true} : {value : i--, done :false}
            },
            [Symbol.iterator]() { // 잘 만들어진 iterator는, iterator를 리턴할 때 자기 자신을 리턴할 수 있다.
                return this;
            }
        }
    }
}

for (const c of count3) log(c); // iterator 동작
for (let i = 0; i < count3.length; i++) log(count3[i]); // for i 는 동작하지 않네.

let iter1 = count3[Symbol.iterator]();
log(iter1.next());
log(iter1.next());
log(iter1.next());
log(iter1.next());

let iter2 = count3[Symbol.iterator]();
log (iter2.next()); // 하나를 이미 사용했다.
for (const i of iter2) log(i); // 나머지 두 개만 동작한다.

let iter3 = count3[Symbol.iterator]();
let iterOfIter3 = iter3[Symbol.iterator](); // 자기 자신을 리턴한다.
log(iter3===iterOfIter3); // true

log(iter3.next());
log(iterOfIter3.next());
log(iter3.next());
log(iterOfIter3.next()); // done : true

generator

  • 평선 예약어 뒤에 *에 넣어(function* get(){}) iterator 함수를 구현할 수 있다. 이를 제너레이커 함수라 한다.
  • yield에 정의한 값을 리턴한다.
  • 더 이상 값을 찾을 수 없거나 return을 반환하면 종료한다.
let genIter = gen();
for (const i of genIter) log(i);

function* odd(l){
    for (let i = 0; i < l; i++) {
        if(i%2==1) yield i;
        if(i==l) return;
    }
}

let odd10 = odd(10);
// odd 함수를 보면 for...i에서 한 번씩 누락한다. 그러므로 나는 exist - null - exist - null의 형태로 출력할 줄 알았다.
// 실제로는 다음과 같이 출력한다 : 
// Object {value: 1, done: false}
// Object {value: 3, done: false}
// Object {value: 5, done: false}
// Object {value: 7, done: false}
// Object {value: 9, done: false}
// Object {value: undefined, done: true}
// Object {value: undefined, done: true}
for (let i = 0; i < 7; i++) log(odd10.next());

generator의 조합으로 함수형 프로그래밍 하기

  • generator을 조합하여 사용할 경우 좀 더 간결하고 좋은 코드를 작성할 수 있다.
function* infinity(start = 0){
    while(true) yield start++;
}

let two2Infinity = infinity(2);
for (let i = 0; i < 3; i++) log(two2Infinity.next()); // 2부터 1씩 가산하여 5까지

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

let iter5to10 = limit(10, infinity(5));
for (const i of iter5to10) log(i); // 5, 6, 7, 8, 9, 10

function* odd(start, end){
    for (const i of limit(end, infinity(start))) {
        if(i%2==1) yield i;
    }
}

let odd2To8 = odd(2, 8);
for (const o of odd2To8) log(o); // 3, 5, 7

iterator를 활용한 함수형 프로그래밍의 기본적인 함수 구현 : map, filter, reduce

  • iterator를 활용하면 이전보다 발전된 형태의 함수를 구현할 수 있다.
  • map, filter, reduce를 구현하였다.
// 공통으로 사용하는 값
const log = console.log;

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

curry, map

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

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

log(map(p => p.name, products));
log(map(p => p.price, products));

// querySelectorAll은 iterator가 구현되어 있다. map을 사용할 수 있다.
log(map(el => el.nodeName, document.querySelectorAll('*')));

// 구조 분해를 아래와 같이 적극적으로 활용할 수 있다. 
let m2 = new Map(
    map(
        ([k, a]) => [k, a * a],
        new Map([['a', 10], ['b', 20]])
    )
);
  • 참고로 es6에서는 map 메서드가 존재하나 현 상황에서는 사용하지 않는다. array-like에서 동작하지 않기 때문이다.
log([1,2,3].map(a => a * a));
log(document.querySelectorAll('*').map(a => a * a)); // 동작하지 아니함.

filter

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


log(map(p=>p.name, filter(p=>p.price > 15000, products)));
log(map(p=>p.name, filter(p=>p.price <= 15000, products)));
log(filter(
    a => a % 2 == 0,
    function * (){
        yield 1 ;
        yield 3 ;
        yield 10 ;
    }())
)

reduce

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

const go = (...args) => reduce((a, f)=>f(a), args);
go(
    0,
    a => a + 1,
    a => a + 10,
    a => a + 100,
    log
);

pipe

  • 함수 리스트를 클로져로 만들고, 해당 클로저에 인자를 삽입하여 평가한다.
  • 아래는 다소 복잡한데
    • 함수를 두 개로 분리한다.
    • 첫 번째 함수(f)는 인자를 여러 개(…as) 받을 수 있다.
    • f(…as)의 결과값은 하나이며 이를 go 함수를 통해 처리한다.
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);

const f_pipe2 = pipe(
    (a, b) => a + b,
    a => a + 10,
    a => a + 100
);
log(
    f_pipe2(3, 7)
); // 120

go를 사용하기

const total_price = pipe (
    map(p=>p.price),
    reduce((a, b) => a + b)
);

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

go(
    products,
    base_total_price(p=>p.price<20000),
    console.log
);

다음의 수업을 학습하고 블로그를 정리하였습니다. 매우 강추 합니다! : https://www.inflearn.com/course/functional-es6