함수형 프로그래밍 with JS ③

:D ·2023년 5월 5일
0

해당 강의를 듣고 정리한 글입니다.... 🐿️
https://www.inflearn.com/course/functional-es6

섹션 6. 지연성 1

range와 느긋한 L.range

range 함수를 만들어보자. 파이썬의 range 함수처럼 연속적인 숫자 객체를 만들어서 반환해주는 함수를 만들려는 것 같다.

console.log(range(5)); // [0, 1, 2, 3, 4]

아래와 같이 range 함수를 만들 수 있다.

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

저번 시간에 구현해봤던 reduce 함수를 사용하여 range로 만든 배열의 총 합도 구할 수 있다.

const add = (a, b) => a + b;
const list = range(5);

console.log(reduce(add, list)); // 10

강의에서 하라는 대로 느긋한 L.range도 만들어 보자. => 최대한 연산을 미루기 때문에 느긋한 L.range라고 명명하신것 같다-!

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

const add = (a, b) => a + b;
const list = L.range(5);

console.log(reduce(add, list)); // 10

두 개의 range 함수에서는 차이가 있다. 첫 번째 range 함수에서는 range() 함수를 실행하면 모든 값들이 평가가되어 list 배열이 만들어진다. 하지만 두 번째 range 함수에서는 reduce함수에서 list.next().value가 실행되는 순간 값이 평가되어진다.

두 개의 range 함수의 성능을 비교해보자.

function test(name, time, f) {
  console.time(name);
  while (time--) f();
  console.timeEnd(name);
}

test("range", 10, () => reduce(add, range(1000000)));
test("L.range", 10, () => reduce(add, L.range(1000000)));

결과는 range: 559.301ms, L.range: 389.364ms로 나왔다.

take

원하는 숫자만큼 배열을 자르는 take 함수를 작성해보자.

const take = (l, iter) => {
  let res = [];
  for (const a of iter) {
    res.push(a);
    if (res.length == l) return res;
  }
  return res;
};

console.log(take(5, range(100))); // [0, 1, 2, 3, 4]

L.range 함수도 take 함수 사용이 가능하다.
L.range 함수의 장점은 range 함수에서는 1000000 만큼의 배열을 만드는데 L.range 함수는 딱 필요한 5개의 숫자만 가지는 배열을 만들기 때문에 효율적이다.

console.log(take(5, range(100000))); // 2.308ms
console.log(take(5, L.range(100000))); // 0.009ms

여기서 의문점, take 에서 원하는 배열의 크기만 가지도록 하기 때문에 효율적인 것은 알았다.

test("range", 10, () => reduce(add, range(1000000)));
test("L.range", 10, () => reduce(add, L.range(1000000)));

그러나, 이 테스트 에서는 똑같이 100000개의 숫자로 이루어진 배열을 reduce 하는데 왜 효율적인 것 일까? 이 부분에 대해서는 좀 더 알아봐야겠다..!

L.map

기존에 작성했던 map과 차이점은 map을 했다고 해서 배열이 만들어지는게 아니라 next()를 해줄 때 값이 평가된다.

L.map = function* (f, iter) {
  for (const a of iter) yield f(a);
};
const it = L.map((a) => a + 10, [1, 2, 3]);

console.log(it.next()); // 11
console.log(it.next()); // 12
console.log(it.next()); // 13

L.filter

filter도 마찬가지로 next()를 해줄 때 값이 평가된다.

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

const it = L.filter((a) => a % 2, [1, 2, 3, 4]);

console.log(it.next()); // 1
console.log(it.next()); // 3

range, map, filter, take를 사용했을 때와 L.range, L.map, L.filter, take를 사용했을 때의 평가 순서 비교

지난 시간과 이번에 공부했던 코드들이다.

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

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

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

const reduce = curry((func, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  for (const a of iter) {
    acc = func(acc, a);
  }
  return acc;
});

const add = (a, b) => a + b;

const go = (...args) => {
  reduce((a, f) => f(a), args);
};

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

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

const take = curry((l, iter) => {
  let res = [];

  for (const a of iter) {
    res.push(a);
    if (res.length == l) return res;
  }

  return res;
});

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

range, map, filter, take를 사용했을 때와 L.range, L.map, L.filter, take를 사용했을 때의 평가 순서 비교를 해보려고 한다.

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

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

첫 번째 경우에는 모든 배열의 요소가 range[0,1,2,3,4,5,6,7,8,9] > map[10,11,12,13,14,15,16,17,18,19] > filter[11,13,15,17,19] > take[11,13] 이런 가로 방식으로 전개가 되고,
두 번째 경우에는 range[0] > map[10] > filter[], range[1] > map[11]> filter[1] > take[11] 하나씩 세로 방식으로 전개가 된다.

map, filter 계열 함수들이 가지는 결합 법칙

사용하는 데이터가 무엇이든지, 사용하는 보조 함수가 순수함수라면
위의 코드 예시처럼 모든 배열의 요소가 mapping -> filtering 하던지, 배열 한개씩 mapping -> filtering 하던지 결과가 같다.

ES6의 기본 규악을 통해 구현하는 지연 평가의 장점

서로 다른 라이브러리라 던지 서로 다른 함수들이던지 약속된 자바스크립트의 기본 값을 통해 소통하기 때문에 조합성이 높고, 합성을 안전하게 할 수 있다.

profile
강지영입니...🐿️

0개의 댓글