함수형 프로그래밍 (2)

Dan·2023년 5월 11일
1

실무공부

목록 보기
8/12
post-thumbnail

코드를 값으로 다루어 표현력 높이기

앞서 만들어 뒀던 map, fiter, reduce를 모듈화 해놓고 사용해보자. 아래 코드는 정상적으로 잘 작동하지만 중첩되어 있어서 이해하기 어렵다.

  const products = [
    {name: '반팔티', price: 15000},
    {name: '긴팔티', price: 20000},
    {name: '핸드폰케이스', price: 15000},
    {name: '후드티', price: 30000},
    {name: '바지', price: 25000}
  ];

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

  log(
    reduce(
      add,
      map(p => p.price,
        filter(p => p.price < 20000, products))));

go, pipe

해당 코드와 똑같은 기능을 하는 코드를 좀 더 사용 친화적인 코드로 변환해보자

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

 go(
    add(0, 1),
    a => a + 10,
    a => a + 100,
    log);
 // 111

// 함수를 리턴하는 함수
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);

const f = pipe(
    (a, b) => a + b,
    a => a + 10,
    a => a + 100);

console.log(f(0, 1));

log(
    reduce(
      add,
      map(p => p.price,
        filter(p => p.price < 20000, products))));

currying

커리는 함수의 분해기법, 다수의 인자를 가지는 함수 대신 , 하나의 인자를 가지는 연속된 함수둘의 중첩이다. 클로저를 활용하여 외부환경의 값들을 기억했다가 사용한다.

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

const mult = curry((a,b) => a*b)
console.log(mult(3)(2));

const mult3 = mult(3);
console.log(mult3(10));
console.log(mult3(5));
console.log(mult3(3));

위의 커리 함수를 이용해서 기존에 작성해뒀던 코드를 좀 더 깔끔하게 변경해보자

// 커리 사용 전
go(
    products,
    products => filter(p => p.price < 20000, products),
    products => map(p => p.price, products),
    prices => reduce(add, prices),
    log);


// 커리로 기존 map, filter, reduce를 감싸준다
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;
});

// 커리 사용 후
go(
    products,
    filter(p => p.price < 20000),
    map(p => p.price),
    reduce(add),
    log);

함수 조합으로 함수 만들기

  const total_price = pipe(
    map(p => p.price),
    reduce(add));

  const base_total_price = predi => pipe(
    filter(predi),
    total_price);

  go(
    products,
    base_total_price(p => p.price < 20000),
    log);

  go(
    products,
    base_total_price(p => p.price >= 20000),
    log);

지연 평가 (Lazy Evaluation)

제너레이터/이터레이터 프로토콜 기반으로 구현하여 값의 평가가 꼭 필요로할 떄만 하도록 하는 계산법

L.map (Lazy Evaluation map)

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

var it = L.map(a => a+10, [1,2,3]);
console.log(it.next()) // {value: 11, done: false}
console.log(it.next()) // {value: 12, done: false}
console.log(it.next()) // {value: 13, done: true}
console.log([...it]) // [11,12,13]

L.filter (Lazy Evaluation filter)

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

var it = L.filter(a => a % 10, [1,2,3,4]);
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 3, done: false}
console.log(it.next()) // {value: undefined, done: true}
console.log([...it]) // [11,12,13]

엄격한 계산과 느긋한 계산 비교해보기

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

  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;
    } else {
      iter = iter[Symbol.iterator]();
    }
    let cur;
    while (!(cur = iter.next()).done) {
      const a = cur.value;
      acc = f(acc, a);
    }
    return acc;
  });

  // console.time('');
  // go(range(100000),
  //   map(n => n + 10),
  //   filter(n => n % 2),
  //   take(10),
  //   log);
  // console.timeEnd('');

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

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

  L.map = curry(function* (f, iter) {
    iter = iter[Symbol.iterator]();
    let cur;
    while (!(cur = iter.next()).done) {
      const a = cur.value;
      yield f(a);
    }
  });

  L.filter = curry(function* (f, iter) {
    iter = iter[Symbol.iterator]();
    let cur;
    while (!(cur = iter.next()).done) {
      const a = cur.value;
      if (f(a)) {
        yield a;
      }
    }
  });

  // [0, 1, 2, 3, 4, 5, 6, 7, 8...]
  // [10, 11, 12, ...]
  // [11, 13, 15 ..]
  // [11, 13]
  //
  // [0    [1
  // 10     11
  // false]  true]
  //

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

결과로 보는 즉시 평가와 지연 평가

위에서 go 함수를 실행했을 때 결과의 실행순서를 보도록 하자

// 즉시 평가
// 1. range(10) 의 결과 값을 먼저 계산해서 map 함수로 넘겨준다
[0,1,2,3,4,5,6,7,8,9]
// 2. 해당 값을 가지고 map으로 계산되어 결과 값을 filter로 넘겨준다
[10,11,12,13,14,15,16,17,18,19]
// 3. filter 함수의 결과 값을 다 계산하여 take로 넘겨준다
[11,13,15,17,19]
// 4. take의 결과를 반환하여 평가한다.
[11,13]


// 지연 평가
// 1. 함수가 실행되면 L.range, L.map, L.filter의 평가를 미뤄두고 take함수부터 실행하게 된다. 
// 2. take 함수의 !(cur = iter.next()).done 조건을 만나는 순간 평가를 미뤄뒀던 filter로 넘어 간다. 
// 3. range까지 iter.next() 를 만나면 미뤄뒀던 평가를 하게 된다.
// 4. range 함수에서 yield를 만나면 평가된 값을 map으로 반환한다.
0
// 5. map에서 yield를 만나면 filter에게 평가를 맡긴다
10
// 6. filter의 결과를 take로 넘긴다.
false
// 7. 앞 값이 true일 시 take에서 값을 담는다. length가 2가 될 때까지 해당 작업들은 반복한다.

1
11
true
[11]
2
12
false
[11]
3
13
true
[11,13]

// 위와같이 실행된 다음 얻은 결과 값을 평가하게 된다.

즉시 평가와 지연 평가의 동작원리를 보면 계산의 효율성에서 큰 차이를 보인다는 것을 알 수 있다. 데이터의 크기가 커지면 커질수록 즉시평가보다 지연평가가 성능적으로 더 유리하다는 것을 알 수 있다.

정리를 하자면,

  1. 즉시 평가
  • 결과에 필요 없는 모든 계산을 다 진행한다.
  1. 지연 평가
  • 필요한 계산만 진행하기에 배열의 크기가 무한대이더라도 성능상 문제가 안생긴다.
profile
만들고 싶은게 많은 개발자

0개의 댓글