지연성

jodmsoluth·2020년 3월 13일
0

함수형프로그래밍

목록 보기
5/17

range

숫자를 하나 받고 그 숫자의 크기만큼 배열을 리턴하는 함수

const range = l => {
  let i = -1;
  let res = [];
  while(++i < l) {
    console.log(i); // 출력 됨
    res.push(i);
  }
  return res;
};

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

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

//--------
// 모든 배열 요소를 더하는 함수
const add = (a, b) => a + b;

var list = range(4);
console.log(list) // 배열 출력
console.log(reduce(add, list));

느긋한 L.range

const L = {};

L.range = function *(l) {
  let i = -1;
  console.log('hi'); // next() 하지 않으면 출력 안됨(평가 안됨)
  while(++i < l) {
    console.log(i); // next() 하지 않으면 출력 안됨(평가 안됨)
    yield i;
  }
};

// 모든 배열 요소를 더하는 함수
var list = L.range(4);
console.log(list); // 알기 힘든 값(이터러블) 출력
console.log(reduce(add, list));

느긋한 L.range 와 range의 차이

range는 range가 실행 되었을 때 이미 모든 부분의 평가가 되면서 배열로 값이 만들어 지지만(나중에 reduce, map 안에서 값으로 만들어진 배열이 Symbol.iterator로 이터레이터를 만들어서 사용된다.) - 효율면에서 떨어짐
반면에 느긋한 L.range는 이터러블로 평가되므로 next()를 하기 전 까지 평가가 되지 않는다.(나중에 reduce, map 등에 들어가면 평가됨)

느긋한 L.range 와 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))); // 더 빠르다.

take

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

console.time('');
console.log(take(5, range(100000)));
console.timeEnd('');
// output [0, 1, 2, 3, 4] 9.2719...ms
go(
  range(10000),
  take(5),
  reduce(add),
  log);

console.time('');
console.log(take(5, L.range(100000)));
console.timeEnd('');
// output [0, 1, 2, 3, 4] 0.438964...ms
go(
  L.range(10000),
  take(5),
  reduce(add),
  log);

// 위 두개의 차이
// range는 100000개의 배열 값을 만들지만
// L.range는 평가되기 전이기 때문에 100000개의 배열 값을 만들지 않고 5번의 next()함수가 실행되어 5개의 배열값만 만든다.

이터러블 중심 프로그래밍에서의 지연 평가

  • 제때 계산법 : 필요하기 전까지는 평가되지 않음
  • 느긋한 계산법
  • 제너레이터/이터레이터 프로토콜을 기반으로 구현

L.map

const L = {};
L.map = function *(f, iter) {
  for(const a of iter) yield f(a);
};
var it = L.map(a => a + 10, [1, 2, 3]); // 여기서는 평가 안됨
log(it.next()); // 평가됨

L.filter

const L = {};
L.filter = function *(f, iter) {
  for(const a of iter) {
    if(f(a)) yield a;
  }
};
var it = L.filter(a => a % 2, [1, 2, 3, 4]) // 여기서는 평가 안됨
log(it.next()); // 평가됨

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

// map, filter, reduce는 curry 적용됨
go(
  range(10), // 10번 반복
  map(n => n + 10), // range로 만들어진 배열이 [Symbol.iterator]()을 통해 이터레이터로 변환한다.
  filter(n => n % 2), // map으로 만들어진 배열이 [Symbol.iterator]()을 통해 이터레이터로 변환한다.
  take(2), // filter로 만들어진 배열(5개 요소 가짐)이 이터레이터로 변환하여 take 실행됨
  console.log);

// 동작방식
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
// [11, 13, 15, 17, 19]
// [11, 13]

L.range, L.map, L.filter, take의 평가 순서

// L.map, L.filter, L.reduce는 curry 적용됨
go(
  L.range(10),
  // 4번 while문 안쪽에 들어가면서 1번째 배열 값yield 됨, 그 배열 값을 map -> filter -> take 순으로 작동
  L.map(n => n + 10),
  // 3번 : 이터레이터가 들어감 iter.next()가 아무것도 없어서 range로 들어감
  L.filter(n => n % 2),
  // 2번 : 이터레이터 들어감 iter.next()에 아무것도 없어서 map으로 들어감
  take(2),
  // 1번 : 이터레이터가 들어감 // iter.next()를 실행하니 아무것도 없어서 filter로 들어감
  console.log);

// 동작방식
// 0(range) -> 10(map)-> false(filter)
// 1(range) -> 11(map) -> true(filter) -> push(take)
// 2(range) ...

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

  • 사용하는 데이터가 무엇이든지
  • 사용하는 보조 함수가 순수 함수라면 무엇이든지
  • 이래와 같이 결합한다면 둘 다 결과가 같다.

    [[mapping, mapping], [filtering, filtering], [mapping, mapping]]

    [[mapping, filtering, mapping], [mapping, filtering, mapping]]
    (가로로 결합하던걸(위의 식), 세로로 결합(아래 식)해도 값이 같다)
profile
풀스택이 되고 싶은 주니어 웹 개발자입니다.

0개의 댓글