[functional-js] 지연평가2

younoah·2021년 2월 6일
1

[functional-js]

목록 보기
8/16

이 글은 유인동님의 함수형 프로그래밍 강의내용을 정리한 글입니다.

결과를 만드는 함수 reduce, take

reduce → join

객체로부터 url의 queryString을 만들어내는 함수를 만들어 보자.

const queryStr = obj =>
  go(
    obj,
    Object.entries, // [[key, value], [key, value],...]를 변환
    map(([k, v]) => `${k}=${v}`), // 구조분해를 통해 key와 value를 받음
    reduce((a, b) => `${a}&${b}`) // '&'로 연결
  );

console.log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

queryStr은 obj를 받아서 그대로 obj로 전달하기 때문에 아래처럼 pipe로 대체가 가능하다.

const queryStr = pipe(
  Object.entries,
  map(([k, v]) => `${k}=${v}`),
  reduce((a, b) => `${a}&${b}`)
);

console.log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

이어서 reduce 를 통해 join 함수를 만들어 보자. 이 join 함수는 이터러블 값을 순회하면서 축약하기 때문에 Array에서만 사용할 수 있는 join 메서드 보다 훨씬 다형성이 높다.

const join = curry((sep = ',', iter) => 
	reduce((a, b) => `${a}${sep}${b}`, iter)
);

function* a() {
  yield 10;
  yield 11;
  yield 12;
  yield 13;
}

console.log(a().join(' - ')); // 에러!, join메서드는 Array에만 사용가능
console.log(join(' - ', a())); // 10 - 11 - 12 - 13

위와 같이 제너레이터함수가 정의되어 있을 때, 일반적인 join 메서드로는 결과를 만들 수 없지만, 위에서 선언한 join 함수는 결과를 만들 수 있다.

join 함수는 reduce 를 통해 축약을 해서 이터러블 프로토콜을 따르고 있다. 따라서 join 함수에게 가기전에 만들어지는 값들을 지연시킬 수 있다. 그래서 mapL.map 이여도 동일한 결과를 나타낸다. 그리고 entries 역시도 다음과 같이 제너레이터 함수로 지연평가 방식으로 구현할 수 있다.

L.entries = function* (obj) {
   for (const k in obj) yield [k, obj[k]];
};

const join = curry((sep = ',', iter) => 
	reduce((a, b) => `${a}${sep}${b}`, iter)
);

const queryStr = pipe(
   L.entries,
   L.map(([k, v]) => `${k}=${v}`),
   join('&')
);

console.log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

take → find

take 함수를 통해 find 함수를 만들어 보자.

  • 비효율적
const users = [
  { age: 32 },
  { age: 31 },
  { age: 37 },
  { age: 28 },
  { age: 25 },
  { age: 32 },
  { age: 31 },
  { age: 37 },
];

const find = curry((f, iter) =>
  go(
    iter,
    filter(f), // take 하기 전에 모든 값들을 다 조회한다.
    take(1), // 하나의 값만 리턴한다.
    ([a]) => a // 구조분해, 배열에서 값을 꺼내 준다. take가 배열로 감싸서 리턴하기 때문
  )
);

console.log(find(u => u.age < 30, users));
// {age: 28}

filter 함수에 콘솔을 찍어보면, take 함수 이전에 모든 값들을 다 조회하므로 비효율적이다.

  • 효율적 : L.filter 지연 평가
const find = (f, iter) => go(
   iter,
   L.filter(f),
   take(1),
   ([a]) => a
);

console.log(find(u => u.age < 30, users));
// {age: 28}

지연평가 함수인 L.filter 를 사용하면 필요할 때만 값을 조회하므로 효율적이다.

  • curry 추가
const find = curry((f, iter) => go(
   iter,
   filter(a=> (console.log(a), f(a))),
   take(1),
   ([a]) => a));

console.log(find(u => u.age < 30)(users));
// {age: 28}

지연성 / 이터러블 중심의 코드 다루기

L.map, L.filter로 map과 filter 만들기

앞에서 배웠던 map , filter 함수를 L.map, L.filtertake 조합으로 만들어보자.

L.map + takemap 만들기

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

/* L.map + take 로 map 만들기 */
const map = curry((f, iter) => go(
   iter,
   L.map(f),
   take(Infinity)
));

//↓↓↓

const map = curry((f, iter) => go(
   L.map(f, iter),
   take(Infinity)
));

//↓↓↓

const map = curry(pipe(L.map, take(Infinity)));

L.filter + takefilter 만들기

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

/* L.filter + take 로 filter 만들기 */
const filter = curry(pipe(L.filter, take(Infinity)));

console.log(filter(a => a % 2, L.range(4)));
// [1, 3]

L.flatten, flatten

flatten 함수는 하나의 배열 안에 있는 묶인 배열을 펼쳐서 하나의 배열로 만드는 역할을 해준다.

[[1, 2], 3, 4, [5, 6], [7, 8, 9]][1, 2, 3, 4, 5, 6, 7, 8, 9]

  • L.flatten
const isIterable = a => a && a[Symbol.iterator];
//이터러블인지 판별

L.flatten = function* (iter) {
  for (const a of iter) {
    // if (isIterable(a)) for (const b of a) yield b;
    if (isIterable(a)) yield* a;
    else yield a;
  }
};

var it = L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]);
console.log([...it]);
//[1, 2, 3, 4, 5, 6, 7, 8, 9]

console.log(take(3, L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]])));
// [1, 2, 3]
  • flatten

    take의 조합으로 즉시평가할수 있는 flatten도 만들 수 있다.

const flatten = pipe(L.flatten, take(Infinity));

console.log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]));
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

yield*

yield* 를 활용하면 다음과 같이 코드를 변경 할 수 있습니다.

yield *iterablefor (const val of iterable) yield val; 과 같다.

/* yield 사용 X */
L.flatten = function* (iter) {
   for(const a of iter) {
      if(isIterable(a)) for(const b of a) yield b
      else yield a;
   }
};

/* yield* 사용 O */
L.flatten = function* (iter) {
   for(const a of iter) {
      if(isIterable(a)) yield* a;
      else yield a;
   }
};

L.deepFlat

만약 깊은 이터러블을 모두 평치고 싶다면 아래와 같이 L.deepFlat 함수를 구형하여 사용할 수 있다.

L.deepFlat = function* f(iter) {
   for (const a of iter) {
      if (isIterable(a)) yield* f(a);
      else yield a;
   }
};

console.log([...L.deepFlat([1, [2, [3, 4], [[5]]]])]);
// [1, 2, 3, 4, 5];

L.flatMap, flatMap

flatMap은 map한 값에 flatten한 것과 동일한 값을 가진다. 하지만 map과 flatten을 하는것은 모든 값을 참조하기 때문에 비효율적이다.

// flatMap
console.log([[1, 2], [3, 4], [5, 6, 7]].flatMap(a => a.map(a => a * a)));
// [1, 4, 9, 16, 25, 36, 49]

// map + flatten
console.log(flatten([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a))));
// [1, 4, 9, 16, 25, 36, 49]

먼저 map을 하고 그 결과를 flatten하면 flatMap과 정확히 일치한다. 하지만 두 코드는 배열의 처음부터 끝까지 순회하기 때문에 시간복잡도의 차이는 없다.

하지만 flatMap된 결과에서 take함수를 사용하여 일부분만 뽑아 올때는 지연성으로 작성되지 않았기 때문에 비효율적으로 동작하게된다.

따라서 지연성이 있는 L.flatmap을 구현해보는것이 이번 강의 목표다.

L.flatMap

다형성이 높은 L.flatMap을 만들어 보자.

L.flatMap = curry(pipe(L.map, L.flatten));

var it1 = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it1]); // [1, 4, 9, 16, 25, 36, 49]

var it2 = L.flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it2]); // [1, 2, 3, 4, 5, 6, 7]

flatMap

L.flatten이 아닌 flatten을 사용하면 즉시 평가하는 flatMap을 만들수 있다.

const flatMap = curry(pipe(L.map, flatten));

console.log(flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]));
// [1, 2, 3, 4, 5, 6, 7]

2차원 배열 다루기

L.flatMap을 활용하면 2차원 배열을 편하게 다룰 수 있다.

const arr = [
   [1, 2],
   [3, 4, 5],
   [6, 7, 8],
   [9, 10]
];

go(arr,
   L.flatten,
   L.filter(a => a % 2),
   L.map(a => a * a),
   take(4),
   reduce(add),
   console.log);
//84

지연성/ 이터러블 프로그래밍, 실무적용

지연성을 활용해서 실무에 어떻게 적용하는지 예시를 보자.

  • 유저 정보
var users = [
   {
      name: 'a', age: 21, family: [
         {name: 'a1', age: 53}, {name: 'a2', age: 47},
         {name: 'a3', age: 16}, {name: 'a4', age: 15}
      ]
   },
   {
      name: 'b', age: 24, family: [
         {name: 'b1', age: 58}, {name: 'b2', age: 51},
         {name: 'b3', age: 19}, {name: 'b4', age: 22}
      ]
   },
   {
      name: 'c', age: 31, family: [
         {name: 'c1', age: 64}, {name: 'c2', age: 62}
      ]
   },
   {
      name: 'd', age: 20, family: [
         {name: 'd1', age: 42}, {name: 'd2', age: 42},
         {name: 'd3', age: 11}, {name: 'd4', age: 7}
      ]
   }
];
  • 유저정보 활용
go(users,
   L.map(u => u.family),
   L.flatten, // 2차원 배열 펼치기
   L.filter(u => u.age < 20), // 20세 이하 필터
   L.map(u => u.age), // 나이만
   take(3), // 3개만
   console.log); // 16, 15, 19
profile
console.log(noah(🍕 , 🍺)); // true

0개의 댓글