[functional-js] 이전 까지 구현한 함수 설명과 코드 압축 + 간단 버전 함수형 라이브러리

younoah·2021년 8월 10일
0

[functional-js]

목록 보기
9/16

용어 정리

이터러블

이터레이터를 리턴한는 [Symbol.iterator]() 라는 메서드를 가진 값

이터레이터

{ value, done} 객체를 리턴하는 next() 라는 메서드를 가진 값

이터러블에서 이터레이터 확인

const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();

console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { value: undefined, done: true }

이터러블 / 이터레이터 프로토콜

이터러블 객체를 for of 와 전개 연산자, 구조분해 등과 함께 동작하도록 규약

제너레이터

이터레이터를 리턴하는 함수이다.

이터레이터이자 이터러블을 생성하는 함수

순회할 값을 문장으로 표현하게 해준다.

모든 구현한 함수 설명

go : 값을 여러개의 함수들에 순차적으로 적용한 후 결과를 반환하는 함수

pipe : 여러개의 함수들을 하나로 압축하는 함수

curry : 인자가 원하는 갯수만큼 들어왔을 때 받아온 함수를 실행하게 도와주는 함수

map : 이터러블까지 사용가능한 map 함수 (curry 활용)

filter : 이터러블까지 사용가능한 filter 함수 (curry 활용)

reduce : 이터러블까지 사용가능한 reduce 함수 (curry 활용)


아래에서 (지연평가) 표시

인자로 이터러블을 받는다.

제너레이터이므로 결과는 이터러블 객체 값이 아닌 이터레이터이다.

순회가 시작되면 비로소 값이 조회가 된다. 순회 이전까지는 suspended상태이다.

따라서 일반적인 이터러블처럼 값으로써 활용하기 보다

함수형 프로그래밍에서 지연평가하는 함수들의 조합을 위해서 사용한다.

마지막에 take함수를 사용하면 이터러블 객체 값으로써 사용이 가능하다.

혹은 [...iter] 의 형태처럼 이터레이터를 스프레드 문법으로 값들을 배열로 뽑아올수도 있다.

const range = L.range(10)
console.log(range) // suspended... , 값으로써 활욜은 불가

go(L.range(10),
    L.map(n => n + 10),
    L.filter(n => n % 2),
    take(2),
    console.log
); // [11, 13, 15]
// 지연평가하는 함수들의 조합으로 사용
// 최종적으로 take함수를 사용하면 비로소 값으로써 사용가능.

take : 제너레이터 혹은 이터러블에서 이터러블 객체로 값을 원하는 갯수로 뽑아오기 위해 사용하는 함수

지연성 함수가 제너레이터로 구현되다보니 값으로써 사용이 불가능한데 take 함수로 이터러블 값으로 뽑아올수 있다.

takeAll : take(Infinity)

L.range : 제너레이터 방식으로 지정한 범위만큼 값을 하나씩 생성하는 함수 (지연평가)

L.map : 제너레이터 방식으로 조건을 적용하여 값을 하나씩 생성하는 함수 (지연평가)

L.filter : 제너레이터 방식으로 조건에 맞는 값을 하나씩 생성하는 함수 (지연평가)

join : 이터러블까지 사용 가능한 join 함수 (reduce 활용)

find : 이터러블까지 사용 가능한 find 함수 (take 활용)

L.map + takemap 만들기

L.filter + takefilter 만들기

L.flatten : 제너레이터 방식으로 입력받은 2차원 배열의 모든 요소를 하나씩 생성하는 함수 (지연평가)

L.deepFlat : 제너레이터 방식으로 입력받은 다차원 배열의 모든 요소를 하나씩 생성하는 함수 (지연평가)

L.flatMap : 제너레이터 방식으로 입력받은 2차원 배열을 L.map 하고 L.flatten 하여 값을 하나씩 생성하는 함수 (지연평가)

flatMap : 즉시 평가하는 flatMap 함수

모든 구현한 함수 코드

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

// pipe 함수
const pipe =
  (f, ...fs) =>
  (...as) =>
    go(f(...as), ...fs);

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

// map + curry;
const map = curry((f, iter) => {
  const res = [];
  for (const el of iter) {
    res.push(f(el));
  }
  return res;
});

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

// reduce + curry
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;
});

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

const takeAll = take(Infinity)

// 지연평가 L
const L = {};

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

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

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

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

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

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

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

// L.filter + take => filter
// const filter = curry(pipe(L.filter, take(Infinity)));

// 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;
  }
};
// flatten
const flatten = pipe(L.flatten, take(Infinity));

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

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

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

// 테스트1
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

// 테스트2
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}
      ]
   }
];

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

간단 버전 함수형 라이브러리

주의 사항

  • map, filter, take, range, zip, flat, concat 함수는 지연 평가 됩니다.
  • map, filter, take, reduce, find, countBy, groupBy, zip 함수는 인자로 함수 하나를 받으며 실행시 함수를 리턴하는 커링이 적용된 함수입니다. 한 번에 2개 이상의 인자를 넣으면 정상동작하지 않습니다.
// ----라이브러리---- //
// 시작, 끝, 스텝을 기준으로 이터러블을 생성한다.
function* range(start = 0, stop = start, step = 1) {
  if (arguments.length === 1) start = 0;
  if (arguments.length < 3 && start > stop) step *= -1;

  if (start < stop) {
    while (start < stop) {
      yield start;
      start += step;
    }
  } else {
    while (start > stop) {
      yield start;
      start += step;
    }
  }
}

// 이터러블을 순회하면서 함수를 적용한 새로운 이터러블을 반환한다.
function map(f) {
  return function* (iter) {
    for (const a of iter) yield f(a);
  };
}

// 이터러블을 순회하면서 함수를 적용한 값이 참인 값들만 하여 이터러블을 반환한다.
function filter(f, iter) {
  return function* (iter) {
    for (const a of iter) if (f(a)) yield a;
  };
}

// 주의! 강의의 take와 다르다.
// 이 take는 단순하게 n개까지 걸러서 이터레이터를 반환한다.
function take(limit) {
  return function* (iter) {
    for (const a of iter) {
      yield a;
      if (--limit === 0) break;
    }
  };
}

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

// map과 달리 이터러블을 반환하지 않고 원소를 돌면서 실행만 시켜준다.
function each(f) {
  return function (iter) {
    for (const a of iter) f(a);
    return iter;
  };
}

// 값을 여러개의 함수들을 적용하여 결과를 반환한다.
function go(arg, ...fs) {
  return reduce((arg, f) => f(arg))(arg, fs);
}

// 이터러블에서 가장 맨 처음 원소를 반환한다.
const head = ([a]) => a;

// 이터러블에서 조건에 맞는 첫번째 값을 반환한다.
const find = f => iter => head(filter(f)(iter));

// 객체와 키를 전달받아 객체에서 해당 키를 카운트업한다. 객체를 반환한다.
function inc(parent, k) {
  parent[k] ? parent[k]++ : (parent[k] = 1);
  return parent;
}

// 이터러블을 순회하면서 각 이터러블 값에 주어진 함수를 적용한 결과값을 키로하여 키들과 키들의 갯수들을 객체 형태로 반환한다.
// {키: 갯수, 키: 갯수, ...} 형태
const countBy = f => iter => reduce((counts, a) => inc(counts, f(a)))({}, iter);

// 이터러블 자기 자신을 그대로 반환하는 함수
const identity = a => a;

// 모든 요소들을 카운트하여 결과를 객체로 반환한다.
const count = countBy(identity); //

// 이터러블을 순회하면서 각 이터러블 값에 주어진 함수를 적용한 결과값을 키로하고 키의 값으로 해당 키에 속한 원소들을 원소로 하는 배열을 갖는다.
// {키: [...키에 속한 원소들], 키: [...키에 속한 원소들], ...} 형태
const groupBy = f => iter =>
  reduce((group, a, k = f(a)) => ((group[k] = group[k] || []).push(a), group))(
    {},
    iter
  );

// 키밸류 전개하여 이터러블로 반환
function* entries(obj) {
  for (const k in obj) yield [k, obj[k]];
}

// 밸류를 전개하여 이터러블로 반환
function* values(obj) {
  for (const k in obj) yield obj[k];
}

// 펼칠수 있는 구조인지 확인, 존재하고 && 이터러블이고 && 문자열이 아닐 때 펴칠수있는 구조이다.
const isFlatable = a =>
  a != null && !!a[Symbol.iterator] && typeof a !== 'string';

// 2차원 이터러블 쳘처서 이터러블로 반환
function* flat(iter) {
  for (const a of iter) isFlatable(a) ? yield* a : yield a;
}

// 2차원 이터러블로 변환
// zip([1, 2, 3, 4, 5])([6, 7, 8, 9, 10]) -> [1, 6], [2, 7], [3, 8], [4, 9], [5, 10] 을 생성하는 이터레이터를 반환
function zip(a) {
  return function* (b) {
    a = a[Symbol.iterator]();
    b = b[Symbol.iterator]();
    while (true) {
      const { value, done } = a.next();
      const { value: value2, done: done2 } = b.next();
      if (done && done2) break;
      yield [value, value2];
    }
  };
}

// 여러개 이터러블들을 하나의 배열로 합친다.
function concat(...args) {
  return flat(args);
}

// ---- 예제 ----//
console.log([
  ...go(
    range(10),
    filter(a => a % 2),
    take(3)
  ),
]);
// [1, 3, 5]

go(
  range(10),
  filter(a => a % 2),
  take(4),
  iter => [...iter],
  arr => console.log(arr)
);
// [1, 3, 5, 7]

go(
  range(10),
  filter(a => a % 2),
  take(3),
  map(a => a * 10),
  reduce((a, b) => a + b),
  console.log
);
// 90

console.log(reduce((a, b) => `${a}${b}`)('10', [1, 2, 3]));
// 10123

// 이터러블을 돌면서 age - (age % 10)를 구하고 이 것들을 키로하여 카운트한다.
const users = [{ age: 10 }, { age: 13 }, { age: 20 }, { age: 23 }, { age: 27 }];
go(
  users,
  countBy(({ age }) => age - (age % 10)),
  console.log
);
// {10: 2, 20: 3}

// 이터러블을 돌면서 age - (age % 10)를 구하고 이 것들을 키로하고 값으로는 키에 속한 원소들을 배열 형태로 하여 반환한다.
go(
  users,
  groupBy(({ age }) => age - (age % 10)),
  console.log
);
// // {10: [{ age: 10 }, { age: 13 }], 20: [{ age: 20 }, { age: 23 }, { age: 27 }]}

go(flat([10, [20, 3], [4], 5]), take(2), iter => console.log([...iter]));
// [10, 20]

go(concat(10, [20, 3], [4], 5), take(4), each(console.log));
// 10 20 3 4

go([6, 7, 8, 9, 10], zip([1, 2, 3, 4, 5]), iter => [...iter], console.log);
// [ [ 1, 6 ], [ 2, 7 ], [ 3, 8 ], [ 4, 9 ], [ 5, 10 ] ]
profile
console.log(noah(🍕 , 🍺)); // true

0개의 댓글