[functional JS ES6+] 지연성 2 : L.flatten, L.flatMap, 2차원 배열

작성:    

업데이트:

카테고리:

태그: , ,

본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.


reduce와 map의 활용

queryString 만들기

객체의 정보를 이용해 queryString을 만드는 코드를 작성해보자

const queryStr = obj => go(
  obj, // { limit: 10, offset: 10, type: 'notice' }
  object.entries, // [["limit", 10], ["offset", 10], ["type", "notice"]]
  map(([k, v]) => `${k}=${v}`), // ["limit=10", "offset=10", "type=notice"]
  reduce((a, b) => `${a}&${b}`) // "limit=10&offset=10&type=notice"
);

log(queryStr({ limit: 10, offset: 10, type: 'notice' }));


  • 참고로 obj를 받아서 첫 값이 obj 그대로 사용되므로 pipe 함수를 사용해도 된다.
const queryStr = pipe(
  Object.entries, // [["limit", 10], ["offset", 10], ["type", "notice"]]
  map(([k, v]) => `${k}=${v}`), // ["limit=10", "offset=10", "type=notice"]
  reduce((a, b) => `${a}&${b}`) // "limit=10&offset=10&type=notice"
);

log(queryStr({ limit: 10, offset: 10, type: 'notice' }));


Array.prototype.join보다 다형성이 높은 join 함수

reduce 대신에 array에서 제공하는 join 내장함수 쓰면 안 되나요?

  • join 함수는 array 객체를 상속하는 객체에서만 사용 가능
  • 말그대로 array의 prototype에만 존재하는 join 메서드이므로

  • 반면 위의 reduce는 iterable이라면 모두 사용 가능
  • 더 큰 다형성을 확보 가능


그렇다면 join 함수를 더 다형성이 높게 변형해보자

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

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

log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
  • go나 pipe에서 인자 전달이 더 수월하게 하기 위해 curry 함수를 씌워서 작성
  • sep(결합문자)이 별도로 없는 경우 기본으로 ‘,’ 지정
  • array 뿐만이 아니라 iterable이라면 join 함수 사용 가능
  • generator로 만든 iterable도 join 함수 사용 가능

  • join 이전의 함수들은 iterable protocol을 따르고 있어서 지연 가능


take와 find

  • 앞서 join 함수를 reduce 함수를 통해서 제작
  • 함수형 프로그래밍은 계보를 따르도록 작성 가능
  • take 함수를 통해 find 함수를 제작해보자.

find 함수: iterable에서 조건을 만족하는 첫 번째 요소를 찾는 함수


즉시 평가 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), // [{age: 28}, {age: 25}]
  take(1), // [{age: 28}], 조건을 만족하는 요소 1개만 취함
  ([a]) => a)); // {age: 28} : 배열을 깨는 함수

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


즉시 평가 싫어요

  • 위의 함수는 하나의 결과값만 꺼내더라도 이전까지 모두 순회하며 배열을 준비하는 상황
  • 이를 지연 평가를 통해 불필요한 평가를 회피해보자.


지연 평가 find

const find = curry((f, iter) => go(
  iter,
  L.filter(f), 🔆
  take(1),
  ([a]) => a));

log(find(u => u.age < 30)(users));
  • L.filter 함수를 통해 take부터 평가를 시작
  • take 함수에게 결과 연산을 미뤄서 하나의 값이 꺼내지면 더이상의 연산을 하지 않도록 한다.


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

L.map과 map

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

log(map(a => a + 10, range(4)))

위의 map 함수를 L.map을 이용해 바꿔보자.


// go 함수 기본 구조
const map = curry((f, iter) => go(
  iter,
  L.map(f),
  take(Infinity)
));

// 초기값 iter를 첫 함수에 전달해 go 함수 축약
const map = curry((f, iter) => go(
  L.map(f, iter),
  take(Infinity)
));

// go 함수를 pipe 함수로 변경
const map = curry(pipe(L.map, take(Infinity)));


L.filter와 filter

// 기존 filter 함수
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;
})

위의 filter 함수를 L.filter 함수를 통해 지연평가 방식으로 변경해보자.


// go 함수를 이용해 L.filter 처리
const filter = curry((f, iter) => go(
  iter,
  L.filter(f),
  take(Infinity)
));

// 초기값 iter를 첫 함수에 전달해 go 함수 축약
const filter = curry((f, iter) => go(
  L.filter(f, iter),
  take(Infinity)
));

// go 함수를 pipe 함수로 변경
const filter = curry(pipe(L.filter, take(Infinity)));


L.filter 리팩토링

앞서 동작을 명확히 하기 위해 풀어쓴 L.filter 함수 코드를 리팩토링 해보자.

// 기존 L.filter
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;
    }
  }
});

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


L.flatten과 flatten

L.flatten

배열 내부에 또다른 배열들이 있는 경우 이들을 모두 구조분해해서 하나의 배열로 만드는 함수

const list = [[1, 2], 3, 4, [5, 6], [7, 8, 9]];
??? // [1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 위와 같이 동작하기 위해 만들어진 함수이다.
  • 이 함수를 만들어보자.


// a가 Symbol.iterator를 가지고 있다면 true
// a[Symbol.iterator]가 null일 수 있으므로 'a &&'로 안전하게 처리
const isIterable = a => a && a[Symbol.iterator];

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

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


yield *

위의 코드를 보다 더 간단하게 바꿔보자

L.flatten = function *(iter) {
  for (const a for iter) {
    if (isIterable(a)) yield *a;
    else yield a;
  }
}
  • yield *iterfor (const val of iter) yield val;과 같다.


즉시 평가 flatten 함수

L.flatten을 즉시평가하는 함수 flatten을 L.flatten 기반으로 만들어보자

const flatten = pipe(L.flatten, take(Infinity));
log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]])); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
  • flatten 함수에 배열을 인자로 전달하면 다음과 같은 순서로 진행된다.
  1. take 함수는 limit이 Infinity이므로 L.flatten의 yield마다 take에 추가한다.
  2. L.flatten은 매번 next를 진행하며 yield를 반환한다.
  3. 이를 반복한다.


L.deepFlat

만약 배열 내부에 또 내부 배열이 있다면?

깊은 iterable을 모두 펼치는 L.deepFlat 함수를 구현해보자.

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

재귀적으로 내부 배열을 다시 f 함수로 넣어 yield를 내보내는 것이다.


L.flatMap, flatMap

JS 내장 flatMap 함수

flatten과 map을 동시에 실행하는 함수

  • 최신 자바스크립트에 추가
  • array.prototype에만 존재하는, 범용성이 떨어지는 함수
log([[1, 2], [3, 4], [5, 6, 7], 8, 9].flatMap(a => a)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
log([[1, 2], [3, 4], [5, 6, 7], 8, 9].flatMap(a => a.map(a => a * a))); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
log(flatten([[1, 2], [3, 4], [5, 6, 7], 8, 9].map(a => a.map(a => a * a)))); // [1, 4, 9, 16, 25, 36, 49, 64, 81]


L.flatMap

  • 자바스크립트가 기본적으로 지연적으로 동작하지 않기 때문에 추가
  • array를 포함해 iterable이라면 모두 동작하도록 작성
L.flatMap = curry(pipe(L.map, L.flatten));
const flatMap = curry(pipe(L.map, flatten));

const it = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7], 8, 9]);
log(it.next()); // {value: 1, done: false}
log(it.next()); // {value: 4, done: false}
log(it.next()); // {value: 9, done: false}
log(it.next()); // {value: 16, done: false}
log(it.next()); // {value: 25, done: false}


flatMap의 활용

log(flatMap(L.range, [1, 2, 3])); // [0, 0, 1, 0, 1, 2]


2차원 배열 다루기

flatten을 이용해 2차원 배열을 다뤄보자

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

go(arr, flatten, log); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
go(arr, flatten, filter(a => a % 2), log); // [1, 3, 5, 7, 9]
go(arr, L.flatten, L.filter(a => a % 2), take(3), log); // [1, 3, 5]


지연성 / iterable 중심 프로그래밍 실무적인 코드

데이터를 실무에서 사용하는 구조로 바꾸어 이해해보자

const 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}
    ]
  }
];
  • 위는 4명의 사람과 그 가족에 대한 데이터


go(users, // users 정보로부터
  L.map(u => u.family), // 각 user의 가족들 리스트를 모아
  L.flatten, // 이를 flatten하여 한 배열에 객체를 모은다.
  L.filter(u => u.age < 20), // 20세 미만의 미성년자만
  L.map(u => u.name), // 이름만을 뽑는데,
  take(4), // 4명만 뽑는다.
  log // ["a3", "a4", "b3", "d3"]
)
  • 객체 지향은 데이터 중심인 반면, 함수형 프로그래밍은 이미 작성된 함수들에 데이터를 맞추는 방식
  • 함수가 데이터보다 우선순위가 높다.

댓글남기기