ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Functional] range, take, 느긋한 L.range, L.take, L.reduce
    programing/Language 2019. 7. 22. 16:40

    안녕하세요, 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

    2019/07/18 - [programing/JavaScript] - [Functional] 코드를 값으로 다루어 표현력 높이기


    range

    const range = length => {
        let i = -1;
        let result = [];
        
        while(++i < length) {
            res.push(i);
        }
        
        return res;
    };
    
    console.log(range(3));
    // [0, 1, 2] 출력

    지정한 범위만큼 배열을 생성하는 함수입니다.

     

     

    느긋한 L.range

    const L = {};
    L.range = function *(length) {
        let i = -1;
        let result = [];
        
        while(++i < length) {
            yield i;
        }
    };
    
    const list = L.range(3);
    for(const e of list) console.log(e);

    지정한 범위만큼 배열을 미리 생성하지 않고, 평가할 때 동적으로 생성하는 함수입니다.

     

     

     

     

    take

    const take = (limit, iterable) => {
        let result = [];
        
        for(const e of iterable) {
            result.push(e);
            if(result.length === limit) return result;
        }
        
        return result;
    };

    iterable에서 limit개수만큼 뽑아내어 반환하는 함수입니다.

     

    const find1 = (f, iter) => L.go(
        iter,
        L.filter(val => f(val)), // 인자로 받은 f를 모든 요소에 적용시킨다
        L.take(1), // 그 중 첫번째, 한 요소만 추출한다
        ([user]) => user // 반환 요소가 배열이기 때문에, 비구조화 할당을 통해 요소를 추출한다
    );
    
    const find2 = (f, iter) => L.go(
        iter,
        L.filter(val => f(val)), // 인자로 받은 f를 모든 요소에 적용시킨다
        L.take(Infinity), // 모든 요소를 추출한다
        ([user]) => user // 반환 요소가 배열이기 때문에, 비구조화 할당을 통해 요소를 추출한다
    );
    
    console.log(find1(user => user.age > 30, users)); // 유저들 중, 나이가 30세 초과인 첫번째 유저를 출력한다
    console.log(find2(user => user.age > 30, users)); // 유저들 중, 나이가 30세 초과인 모든 유저를 출력한다

    위 코드와 같이 take함수를 적용할 수 있습니다.

    만약, 몇개를 추출해야 될 지 모르는 상황이라면, 인자로 Infinity를 주면 됩니다.

     

     

     

    L.take

    비동기 상황(인자가 promise인 경우)를 대비해, 다음과 같이 수정할 수 있습니다.

     

    L.nop = Symbol("nop");
    
    L.take = L.curry(async (limit, iterable) => {
        let result = [];
    
        for (const e of iterable) {
            // result.push(await e);
            try {
                const res = await e;
                result.push(res);
            } catch (err) {
                if (err === L.nop) continue;
                else return Promise.reject(err);
            }
            if (result.length === limit) return result;
        }
    
        return result;
    });

    여기서 L.nopL.filter에서 사용됩니다.

     

     

     

    L.reduce

    L.reduce = L.curry(async (f, acc, iterator) => {
        if (!iterator) {
            iterator = acc[Symbol.iterator]();
            acc = await iterator.next().value;
        } else {
            iterator = iterator[Symbol.iterator]();
        }
    
        for (const e of iterator) {
            try {
                acc = await f(acc, await e);
            } catch (err) {
                if (err !== L.nop) return Promise.reject(err);
            }
        }
        return acc;
    });

    iterator가 인자로 입력되지 않은경우 acc에서 뽑아내고, 입력되었다면 iterator에서 뽑아냅니다. (well formed iterable이기 때문에 가능합니다.)

    그리고 iterator의 각 요소를 await하면서 순회합니다. (요소가 promise의 instance일 수 있기 때문입니다.) await한 값을 인자로 받은 f에 넣어줍니다. f에서 promise를 리턴할 수 있기 때문에, 또 await로 결과값을 추출합니다.

    만약 이 과정에서 reject된 값이 있다면 catch문에 걸리게 되며, err를 비교하여 nop인 경우 아무짓도 하지 않으며(continue와 동일), nop을 제외한 오류라면 reject promise를 throw하듯이 return합니다.

     

    그런데 위 코드는 순회할 iterator의 첫번째 요소를 포함하여, nop이 여러개인 경우에는 정상적으로 작동하지 않습니다.

    const {L, C} = require("./myUtils.js");
    
    L.go(
        [1, 2, 3, 4, 5, 6, 7, 8, 9],
        L.map(val => new Promise(res => setTimeout(()=> res(val), 1000))),
        L.filter(val => Promise.resolve(val > 6)),
        C.reduce((acc, val) => {
            console.log('reduce...', acc, val);
            return acc + val
        }),
        console.log,
    ).catch(console.log);
    
    /* 출력
    Object [Generator] {}
    */

    위 코드에서 제네레이터가 출력되었다는 것은, L.reduce가 아무런 동작을 하지 않았기 때문입니다. 이 모든 원인은 순회할 첫번째 요소가 nop으로 인해 만들어진 promise이기 때문입니다.

    이러한 현상을 해결하기 위해, 순회할 iterator의 첫번째 요소부터 연속적으로 nop을 최대한 제거하는 코드는 다음과 같습니다.

     

    L.reduce = L.curry(async (f, acc, iterator) => {
        if (!iterator) {
            iterator = acc[Symbol.iterator]();
    
            // 순회할 iterator의 앞쪽부터 nop을 최대한 제거한다
            acc = await async function dequeueNop(acc, iterator){
                try{
                    acc = await iterator.next().value;
                }
                catch(err) {
                    if(err !== L.nop) return Promise.reject(err);
                    if(err === L.nop) return dequeueNop(acc, iterator);
                }
                return acc;
            }(acc, iterator);
        } else {
            iterator = iterator[Symbol.iterator]();
        }
    
        for (const e of iterator) {
            try {
                acc = await f(acc, await e);
            } catch (err) {
                if (err !== L.nop) return Promise.reject(err);
            }
        }
        return acc;
    
        /*return L.branch(acc, function recurReduce(acc) {
            let e;
            while (!(e = iterator.next()).done) {
                acc = L.branch2(acc, e.value, f);
                if (acc instanceof Promise) return acc.then(recurReduce);
            }
            return acc;
        });*/
    });

    재귀함수인 dequeueNop을 이용하여, 정상적인 값이라면 바로 acc를 반환하며, nop이라면 재귀호출을, 그 외의 에러라면 Promise.reject(err)를 반환합니다.

     

    다음은 개선된 L.reduce를 활용한 예제입니다.

    const {L, C} = require("./myUtils.js");
    
    L.go(
        [1, 2, 3, 4, 5, 6, 7, 8, 9],
        L.map(val => new Promise(res => setTimeout(()=> res(val), 1000))),
        L.filter(val => Promise.resolve(val > 6)),
        L.reduce((acc, val) => {
            console.log('reduce...', acc, val);
            return acc + val
        }),
        console.log,
    ).catch(console.log);
    
    /* 출력
    reduce... 7 8
    reduce... 15 9
    24
    */
    
    L.go(
        [1, 2, 3, 4, 5, 6, 7, 8, 9],
        L.map(val => new Promise(res => setTimeout(()=> res(val), 1000))),
        L.filter(val => Promise.resolve(!(val % 2))),
        L.reduce((acc, val) => {
            console.log('reduce...', acc, val);
            return acc + val
        }),
        console.log,
    ).catch(console.log);
    
    /* 출력
    reduce... 2 4
    reduce... 6 6
    reduce... 12 8
    20
    */

    참고

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

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

    댓글

Designed by black7375.