JavaScript/함수형 프로그래밍과 JavaScript ES6+

range, map, filter, take, reduce 중첩 사용(개발자도구로 순서확인)

느리지만 꾸준하게 2021. 8. 20. 16:57

지금까지 range map filter take reduce함수등을 만들고 사용해 보았고

지연적으로 동작하는 L.range L.map L.filter를 만들어 보았는데 같은문제를 즉시 평가되는 range map filter를 사용해서 해결 해보고

 

다른 방법으로는 L.range L.map L.filter를 사용해서 해결해보고 각각의 코드가 어떤 순서대로 평가되는지 확인해보면서 명확한 차이와 구체적인 차이가 어떻고 평가순서가 상관없는 함수형 프로그래밍에 이점등을 살펴보도록 하자.

 

여태까지 만들었던 함수들을 아래와 같이 나타낸다. 

const log = console.log;

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

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

L.range, L.map L.filter도 아래와 같이 나타내준다.

### L.range, L.map, L.filter, take, reduce 중첩 사용

<script>
    L.range = function* (l) {
        let i = -1;
        while (++i < l) {
            yield i;
        }
    };

    L.map = curry(function* (f, iter) {
        for (const a of iter) {
            yield f(a);
        }
    });

    L.filter = curry(function* (f, iter) {
        for (const a of iter) {
            if (f(a)) {
                yield a;
            }
        }
    });

    
</script>

즉시평가되는 함수들로 간단한 문제를 해결해보자. 0부터 9까지의 값을 담은 배열이 만들어졌고 log를 찍어본다.

### range, map, filter, take, reduce 중첩 사용

<script>
// ~~
    go(range(10),
        log);

</script>

// (10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

그리고 map함수를 통해서 함수합성을 하는데 각 값마다 10을 더한 값으로 해준다.

    go(range(10),
        map(n => n + 10),
        log);
        
        // [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

filter를 해서 홀수만 남기도록 해준다.

    go(range(10),
        map(n => n + 10),
        filter(n => n % 2),
        log);


// (5) [11, 13, 15, 17, 19]

이 상태에서 take(2)를 해주면 앞에서 두개의 값만 남겨서 결과를 만들게 된다.

    go(range(10),
        map(n => n + 10),
        filter(n => n % 2),
        take(2),
        log);
        
        // (2) [11, 13]

 

똑같은 코드를 L.range L.map L.filter를 사용해서 만들어본다.

    go(L.range(10),
        L.map(n => n + 10),
        L.filter(n => n % 2),
        take(2),
        log);


// (2) [11, 13]

결과는 같은데 두 코드가 동작하는 방식 or 순서가 다르고 리턴되는 값들과 함수들이 어떤 순서대로 평가되는지 차이가 있는데 하나씩 살펴보도록 하자.

 

즉시 평가되는 코드부터 어떻게 동작하는지 살펴본다. breakpoint를 찍으면서 살펴볼것인데, 처음에 들어왔을 때 그리고 push하는 과정, 어떻게 리턴이 되는지 살펴볼 것이다. range만 놓고 먼저 살펴보자. 새로고침을 해서 멈춰있게 놔두고

range(10)을 했을 때 함수가 실행되고 10이라는 값을 인자로 받았다. 인자로 받은 10이 지역변수(scope)로 표시가 되어져 있고 다음으로 넘어가보면 while문 안에 들어와서 현재 i값이 0이고 그 0은 push가 되게 된다.

 

 

 

 

반복을 하게되면 i가 바뀌고 리턴할 값인 res에도 담기기 시작한다. 계속 실행할 때마다 담기게 된다.

여기서 ++i한 값이 l(10)보다 작을 때까지만 동작을 하니까 같아지면 나가서 해당하는 값까지만 담고 종료하게 된다.

 

 

 

나머지 함수들도 breakpoint를 찍어보면서 확인을 해보자.

확인을 하기전에 map filter take reduce의 for 부분의 코드는 숨겨진 부분이 많다.

여기 코드를 상세하게 보고

for 안에서 어떤 일이 일어나는지를 중간점 breakpoint를 통해 확인함으로써 좀 더 세세하게 for부분이 하는 명령형으로 작성해서 대체해보자.

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

 

 

for부분 코드를 작성해본다. 먼저 iter의 값에 Symbol.iterator를 실행해주고 현재 루프를 돌면서 저장할 임시 변수를 선언해주고 while문을 사용해준다. cur가 iter.next()결과랑 같을 때 값이 done이 아닐 때까지 계속해서 루프를 돌고 a에 cur.value를 담는다. let res부터 while까지가 for문이 하는 역할이다. 결과를 보면 정상동작하게 된다.

    const map = curry((f, iter) => {
        let res = [];
        iter = iter[Symbol.iterator]();
        let cur;
        while (!(cur = iter.next()).done) {
            const a = cur.value;
        // }
        // for (const a of iter) {
            res.push(f(a));
        }
        return res;
    });
    go(range(10),
        map(n => n + 10),
        filter(n => n % 2),
        take(2),
        log);
        
        // (2) [11, 13]

 

 

나머지부분들도 for of 문을 대신하는 코드를 filter과 take reduce에 넣어준다.

<script>
    const range = l => {
        let i = -1;
        let res = [];
        while (++i < l) {
            res.push(i);
        }
        return res;
    };

    const map = curry((f, iter) => {
        let res = [];
        iter = iter[Symbol.iterator]();
        let cur;
        while (!(cur = iter.next()).done) {
            const a = cur.value;
            res.push(f(a));
        }
        return res;
    });

    const filter = curry((f, iter) => {
        let res = [];
        iter = iter[Symbol.iterator]();
        let cur;
        while (!(cur = iter.next()).done) {
            const a = cur.value;
            if (f(a)) res.push(a);
        }
        return res;
    });

    const take = curry((l, iter) => {
        let res = [];
        iter = iter[Symbol.iterator]();
        let cur;
        while (!(cur = iter.next()).done) {
            const a = cur.value;
            res.push(a);
            if (res.length == l) return res;
        }
        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;
    });

</script>

 

 

 

reduce같은 경우에는 else를 해서 아래와 같이 해준다.

const reduce = curry((f, acc, iter) => {
        if (!iter) {
            iter = acc[Symbol.iterator]();
            acc = iter.next().value;
        } else {
            iter = acc[Symbol.iterator]();
        }
        let cur;
        while (!(cur = iter.next()).done) {
            const a = cur.value;
            acc = f(acc, a);
        }
        return acc;
    });

 

 

range map filter take가 각각 어떻게 동작하는지 확인해보자. 각각의 값을 찍어서 확인을 해주자.

우선 range가 실행됐을 때 바로 인자로 10이 들어오고 l에 10이라고 되어있는 것을 확인할 수 있다.

 

 

한번 더 실행을 하게되면 while문 안쪽으로 들어가게 되고 또 클릭하면 현재 i값이 0이고 0이 push가 될 것이다.

 

 

 

계속해서 클릭을 하게되면 while문 조건을 충족할 때까지 계속해서 도는데 i와 l값이 같아져서 더 이상 i가 l보다 작지않을 때 return으로 넘어오게 된다.

 

 

 

이 값이 그대로 리턴이 될 것이고 range(10)자리에 평가가 될 것이다. 평가후에 go를 통해서 전달된다. 즉 map으로 들어오게 된다. map을 보면 iter에 range결과가 들어온다. 결과를 보면 전달받은 함수도 있고 iter라는 값도 있다. iter는 iterable한 값 배열이다. 이 상태에서 통과하면 iter이 바뀌게 된다.

 

 

 

array라는 값으로 바뀌게 되어서 next를 가지고 있는 이터러블에서 이터레이터로 바뀌게 되었다.(Symbol.iterator을 실행시켜서 이터레이터가 되었다. 

 

 

 

 

이제 while문에서 next를 실행하면서 cur에 담고 끝나야 되는지를 확인하면서 while문을 돌게된다. while문 안쪽으로 들어가보면 next를 통해 꺼낸값을 cur에 value로 참조를 해서 꺼내서 a에 담았고 그 값은 0이 들어오게 된다. 앞에서 range에서 만들었었던 배열의 첫번째 값이 들어오게 되고 실행버튼을 클릭하면 해당하는 리턴값을 계속 push를 하게 될 것이다.

 

 

 

 

n => n +10함수가 적용된 값이 배열의 크기였던 10개만큼 다 돌고 완성을 해서 return을 하면 해당결과를 받아서 go를 통해 다음 함수에게 넘겨주게 된다.

 

 

 

 

filter로 넘어와서 iter라는 값의 배열로 10개의 length를 가진 10개의 값 즉 map통해서 10이 더해진 값이 들어온다.

실행을 하면 10개의 값을 iter로 만들게 된다.

 

 

 

iter를 가지고 반복을 하면서 a의 있는 값을 n => n % 2를 적용해보고 true라면 if문을 실행하게 되고 아니면 넘긴다.(홀수만 넘어가게 된다.)

if (f(a)) res.push(a);

 

 

 

앞서 받았던 10개의 array를 모두 순회하면서 원하는 조건에 값만 filter함수가 담게 된다. 담은 후에 take함수에게 전달이 되고 take함수에게 전달된 결과는 5개의 length를 가진 배열을 받는다.

 

 

그 상태에서 이터레이터를 만들고

 

 

이터레이터를 반복을 하면서 res의 length가 2개가 될때까지 담는다. length값이 l과 같은지를 비교하고 같을 때까지 담게 된다. 같아지게 되면 true로 평가되어 return을 하고 log가 찍히게 된다.

 

 

 

<출처 : 유인동 함수형 프로그래밍과 JavaScript ES6+>

https://www.inflearn.com/course/functional-es6/dashboard

 

함수형 프로그래밍과 JavaScript ES6+ - 인프런 | 강의

ES6+와 함수형 프로그래밍을 배울 수 있는 강의입니다. 이 강좌에서는 ES6+의 이터러블/이터레이터/제너레이터 프로토콜을 상세히 다루고 응용합니다. 이터러블을 기반으로한 함수형 프로그래밍,

www.inflearn.com