ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Functional] 코드를 값으로 다루어 표현력 높이기
    programing/Language 2019. 7. 18. 20:10

    안녕하세요, Einere입니다.

    (ADblock을 꺼주시면 감사하겠습니다.)

     

    2019/06/25 - [programing/JavaScript] - [Functional] 평가와 일급, 고차함수

    2019/07/15 - [programing/JavaScript] - [Functional] 순회와 이터러블

    2019/07/15 - [programing/JavaScript] - [Functional] 제네레이터와 이터러블

    2019/07/15 - [programing/JavaScript] - [Functional] map, filter, reduce


    오늘은 코드를 값으로 다루어 좀 더 깔쌈한 프로그래밍을 해보는 방법에 대해 알아보도록 하겠습니다.

     

     

     

    go

    // 첫번째 인자에 이후 로직을 적용해나가는 함수를 구현하고자 한다.
    // go(0, a => a+1, a => a+10, a => a+100, console.log);
    // 0이 a에 들어가 0+1을 계산하여 다음 인자로 전달
    // 1이 a에 들어가 1+10을 계산하여 다음 인자로 전달
    // 11이 a에 들어가 11+100을 계산하여 다음 인자로 전달
    // 111이 인자로 들어가 111을 출력
    
    // reduce를 이용해 다음과 같이 구현이 가능하다
    const go = (...args) => reduce((a, f) => f(a), args);
    go(0, a => a+1, a => a+10, a => a+100, console.log);
    // 111 출력

    go함수는 첫번째 인자로 초기값, 두번째 이후 인자부터 함수들을 받습니다.

    첫번째 인자로 받은 초기값을 두번째 인자인 함수에 파라미터로 넘긴 결과를 두번째 함수로 넘겨줍니다.

    두번째 함수에 파라미터로 넘긴 결과를 세번째 함수로 넘겨줍니다.

    이런식으로 마지막 함수까지 실행하는 함수입니다.

     

     

     

    pipe

    // 여러 함수를 인자로 받아, 하나의 함수로 반환해주는 함수를 구현하고자 한다
    // 즉, 다음과 같은 코드를 구현하고자 한다.
    // const f = pipe(a => a+1, a => a+10, a => a+100);
    // console.log(f(0));
    // 111 출력
    
    const pipe = (...funcs) => (parameter) => go(parameter, ...funcs);
    const f = pipe(a => a+1, a => a+10, a => a+100, console.log);
    f(0);
    // 111 출력

    pipe함수는 함수들을 인자로 받은 후, "초기값을 입력받아서 go함수를 실행시키는 함수"를 반환하는 함수입니다.

    go함수는 위에서 말했듯이, 초기값과 함수들에 대해 reduce를 적용하는 함수입니다.

    따라서, pipe(a => a+1, ...)의 결과인 f함수는 go(parameter, a=>a+1, ...);와 동일합니다. 그렇기 때문에 f(0)의 결과는 go(0, a=>a+1, ...)의 결과인 111과 동일합니다.

     

    그러나 위 pipe함수는 반드시 초기값이 하나인 경우에만 사용이 가능합니다.

    초기값으로 여러 값을 주고 싶다면 다음과 같이 수정을 하면 됩니다.

    const pipe = (f, ...funcs) => (...parameters) => go(f(...parameters), ...funcs);
    const f = pipe((a, b)=> a+b, a => a+10, a => a+100);
    console.log(f(0, 1));
    // 111 출력

    사실 위 함수는 딱히 필요성을 못느끼긴 합니다..ㅎㅎ

     

     

    refactoring

    const products = [
        { name: "반팔티", price: 12000 },
        { name: "긴팔티", price: 15000 },
        { name: "청바지", price: 30000 },
        { name: "야구모자", price: 8000 },
        { name: "캔버스화", price: 25000 }
    ];
    
    // 상품 가격이 20000미만인 상품들의 가격의 총합을 출력한다
    const add = (a, b) => a + b;
    console.log(
        reduce(
            add,
            map(
                p => p.pirce,
                filter(p => p.price < 20000, products)
            )
        )
    );
    
    // go함수를 이용해 위 코드를 리팩토링 한 코드
    go(
        products,
        products => filter(p => p.price < 20000, products),
        products => map(p => p.price, products),
        prices => reduce(add, prices),
        console.log
    );
    // 35000 출력

    go함수를 이용해 상품 가격이 20000미만인 상품들의 가격의 총합을 출력하는 코드를 리팩토링한 코드입니다.

     

     

    template method pattern

    // pipe함수를 이용해 코드의 중복 제거하기
    const totalPrice = pipe(
        products => map(p => p.price, products),
        prices => reduce(add, prices)
    );
    
    
    go(
        products,
        products => filter(p => p.price < 20000, products),
        totalPrice,
        console.log
    );
    // 35000 출력
    
    go(
        products,
        products => filter(p => p.price >= 20000, products),
        totalPrice,
        console.log
    );
    // 55000 출력

    pipe함수를 이용하여 template method pattern을 이용하여 코드의 중복을 최소화할 수 있습니다.

     

    위 코드를 좀 더 세분화 하면 다음과 같이 할 수 있습니다.

    // pipe함수를 이용해 코드의 중복 제거하기
    const totalPrice = pipe(
        products => map(p => p.price, products),
        prices => reduce(add, prices)
    );
    
    // 필터링 함수를 받아, 해당 기준에 부합하는 상품들의 가격의 합을 반환하는 함수
    const filteredTotalPrice = filterFunc => pipe(
        filter(filterFunc),
        totalPrice
    );
    
    go(
        products,
        filteredTotalPrice(p => p.price < 20000, products),
        console.log
    );
    // 35000 출력
    
    go(
        products,
        filteredTotalPrice(p => p.price >= 20000, products),
        console.log
    );
    // 55000 출력

     

     

     

    curry

    // curry는 함수인 f를 받아 p와 params를 받는 함수를 반환합니다.
    // 반환하는 함수는 첫번째 파라미터인 p와 그 이후의 파라미터인 params를 받아, params의 유무에 따라 다른 동작을 합니다.
    // 만약 두개 이상의 파라미터가 전달된 경우 즉시 f함수에 인자들을 넣어 실행한 결과를 반환합니다.
    // 만약 한개의 파라미터만 전달된 경우, 나머지 인자인 params를 전달받아, 미리 받아두었던 p와 params를 실행한 결과를 반환하는 함수를 반환합니다.
    const curry = (f) => 
        (p, ...params) => 
            params.length ? f(p, ...params) : (...params) => f(p, ...params);
    
    
    const multi = curry((a, b) => a * b);
    console.log(multi);
    // (p, ...params) => params.length ? f(p, ...params) : (...params) => f(p, ...params) 출력
    
    console.log(multi(10));
    // (...params) => f(p, ...params) 출력
    
    console.log(multi(10)(20));
    // 200 출력
    
    
    const multi3 = multi(3);
    console.log(multi3(10)); // 30 출력
    console.log(multi3(20)); // 60 출력
    console.log(multi3(30)); // 90 출력
    

    그럼 curry를 다른 함수에 적용시켜 보겠습니다.

    const curry = (f) =>
        (p, ...params) =>
            params.length ? f(p, ...params) : (...params) => f(p, ...params);
    
    // curry함수를 적용
    const map = curry((func, iter) => {
        let ret = [];
    
        for (const e of iter) {
            ret.push(func(e));
        }
    
        return ret;
    });
    
    // curry함수를 적용
    const filter = curry((f, iter) => {
        let arr = [];
        for (const e of iter) {
            if (f(e)) arr.push(e);
        }
        return arr;
    });
    
    // curry함수를 적용
    const reduce = curry((f, acc, iter) => {
        if (!iter) {
            iter = acc[Symbol.iterator]();
            acc = iter.next().value;
        }
        for (const e of iter) {
            acc = f(acc, e);
        }
        return acc;
    });
    
    
    const go = (...args) => reduce((a, f) => f(a), args);
    const pipe = (...funcs) => (parameter) => go(parameter, ...funcs);
    
    const products = [
        { name: "반팔티", price: 12000 },
        { name: "긴팔티", price: 15000 },
        { name: "청바지", price: 30000 },
        { name: "야구모자", price: 8000 },
        { name: "캔버스화", price: 25000 }
    ];
    
    const add = (a, b) => a + b;
    
    // filter, map, reduce는 콜백함수를 하나 받아, 나머지 인자를 받기를 기다리는 함수를 반환합니다.
    // 그리고 나머지 함수로서 각각 products, products, prices를 전달하면 미리 받아놧던 콜백함수를 실행합니다.
    go(
        products,
        products => filter(p => p.price < 20000)(products),
        products => map(p => p.price)(products),
        prices => reduce(add)(prices),
        console.log
    );
    // 위 함수는 다음과 같이 리팩토링 할 수 있습니다.
    // 왜냐하면, go함수의 특성상 각 인자로 주어지는 함수들에 자동으로 인자를 넣어주기 때문이며,
    // curry를 적용한 함수이므로 첫번째 인자만 받은 함수의 결과는 나머지 인자를 기다리는 함수이기 때문입니다.
    // 따라서 filter(...)의 반환값은 products를 기다리는 함수가 되며, go함수가 자동으로 products를 넣어줍니다.
    // map(...)의 반환값은 products를 기다리는 함수가 되며, go함수가 자동으로 products를 넣어줍니다.
    // reduce(...)의 반환값은 prices를 기다리는 함수가 되며, go함수가 자동으로 prices를 넣어줍니다.
    go(
        products,
        filter(p => p.price < 20000),
        map(p => p.price),
        reduce(add),
        console.log
    );
    

    curry를 이용해 장바구니에 담은 상품들의 합을 계산하는 함수를 만들어보도록 하겠습니다.

    const products = [
        { name: "반팔티", price: 12000, quantity: 1 },
        { name: "긴팔티", price: 15000, quantity: 2 },
        { name: "청바지", price: 30000, quantity: 3 },
        { name: "야구모자", price: 8000, quantity: 4 },
        { name: "캔버스화", price: 25000, quantity: 5 }
    ];
    
    // sum은 curry의 반환값이기 때문에, p와 params를 받는 함수가 됩니다.
    // sum함수는 우선 익명의 콜백함수를 받아두고, 나중에 products가 전달되면 해당 콜백함수를 실행합니다.
    // sum함수는 범용성을 가지기 위해, map의 인자로 줄 필터함수 f와 대상 iter를 하드코딩하지 않고, 따로 받습니다.
    const sum = curry((f, iter) => go(
            iter,
            map(f),
            reduce(add)
        )
    );
    
    // sum은 p와 params를 받는 함수이며, p로서 콜백함수를 먼저 받아놓습니다.
    // params는 나중에 주어집니다.
    const totalQuantity = sum(p => p.quantity);
    
    // 마찬가지로, p로서 콜백함수를 먼저 받아놓고, params는 나중에 받습니다.
    const totalPrice = sum(p => p.quantity * p.price);
    
    console.log(totalQuantity(products)); // 15 출력
    console.log(totalPrice(products)); // 289000 출력

     

     

    참고

    마이클 포거스, 『함수형 자바스크립트 : 새롭고 올바른 자바스크립트 프로그래밍 기법』, 한빛미디어(2014).

    유인동, 인프런 - 함수형 프로그래밍과 JavaScript ES6+

    댓글

Designed by black7375.