함수형 프로그래밍 with JS ④

:D ·2023년 5월 13일
0

해당 강의를 듣고 정리한 글입니다.... 🐿️
https://www.inflearn.com/course/functional-es6

섹션 7. 지연성 2

queryStr 함수 만들기

query string을 만드는 함수를 만들어보자.

const queryStr = (obj) =>
    go(
        obj,
        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

go 대신 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

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

arrayjoin 함수는 array 프로토타입에만 있는 함수이다. reduce는 이터러블 객체를 순회하면서 축약을 할 수 있기 때문에 더 다형성이 높다. 그래서 reduce를 통해 join 함수를 만들어보자.

const join = curry((sep = ',', iter) => reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
    Object.entries,
    map(([k, v]) => `${k}=${v}`),
    join('&'),
    console.log
);

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

지금 만든 join 함수는 조합성, 다형성이 높다. 또한 map 대신에 저번에 만들었던 L.map을 사용해 지연평가도 가능하다.

Object.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' });

find

reduce, take 함수는 결과값을 만들어 내는 함수이다. joinreduce를 통해 결과값을 만들어냈다면, 이번에는 take 함수를 통해 결과값을 만들어내는 find 함수를 만들어보자.

find는 조건에 해당되는 특정 값을 하나만을 가져오는 함수이다. L.filtertake을 사용하면서 전체배열 순회 -> filter -> take로 하나의 배열을 가져오는 방식이 아니라 배열의 원소를 하나씩 꺼내오고 -> filter -> take 여기서, 값 1개를 찾는다면 더이상 배열을 순회하지 않는다. => 훨씬 효율적이다!

const find = (f, iter) => go(iter, L.filter(f), take(1), console.log); // [ { age: 25 } ]
find((u) => u.age < 30, users);

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

L.map,L.filter를 통해서 map,filter 함수를 만들어보았다.

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

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

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

const filter = curry((f, iter) => go(L.filter(f, iter), take(Infinity), console.log)); 

map((a) => a + 10, L.range(4)); // [ 10, 11, 12, 13 ]
filter((a) => a % 2, L.range(4)); // [ 1, 3 ]

take(Infinity)가 중복되니까 takeAll로 함수를 추출하면, 이렇게 작성할 수 있다.


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

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

const map = curry(pipe(L.map, takeAll, console.log)); 

const filter = curry((f, iter) => go(L.filter(f, iter), takeAll, console.log)); 

map((a) => a + 10, L.range(4)); // [ 10, 11, 12, 13 ]
filter((a) => a % 2, L.range(4)); // [ 1, 3 ]

L.flatten, flatten

flatten 함수를 만들어볼건데, flatten 함수는 아래의 코드처럼 다 펼쳐서 하나의 배열로 만드는 함수이다.

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

일차원 배열로 변형해주고, iter.next()로 원하는 만큼 값을 반환해주는 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;
        } else yield a;
    }
};

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

즉시 평가하는 flatten 함수도 만들어보았다!

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

또 이렇게 작성하면 원하는 3개의 값만 가져올 수 있다.

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

L.flatMap, flatMap

flatMapflattenmap을 동시에 하는 함수이다.

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

이렇게만 사용하면 flatten과 동일한데, flatMap은 아래처럼 하는것도 가능하다.

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

map을 한 후에 flatten을 한 것과 flatMap은 동일하게 동작한다.

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

그럼에도 불구하고 flatMap이 있는 이유는 mapflatten이 비효율적으로 동작하기 때문이다.
안에 있는 모든 값들을 순회하면서 새로운 배열을 만든 후 , 새로운 배열을 또 전체 순회하면서 배열을 담기 때문에 약간의 비효율이 발생한다.

하지만, flatMapflatten + map은 시간복잡도가 동일하기 때문에 엄청난 효율이 생겼다고는 할 수 없다. 그러나 앞에 있는 3개만 필요하다거나 하면 좀 더 효율적이고, array로만 동작하는 것이 아니기 때문에 좀 더 다형성이 있다.

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

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

즉시평가하는 flatMap 함수도 만들 수 있다.

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

2차원 배열 다루기

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

이렇게 작성하면 지연적으로 동작하기 때문에 필요한 값까지만 순회한다. 여기에 reduce를 사용할 수 도 있고, 다양하게 조합이 가능하다.

이터러블 중심 프로그래밍 실무적인 코드

우리가 공부했던 내용들이 왜 필요하고, 실무에서는 어떻게 사용할까?

예시

이차원 배열과 마찬가지로 users의 데이터를 함수의 조합으로 상황에 맞게 가져온다. 이런식으로 실무에서 꽤 많이 쓰인다고 한다.!

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.flatMap(u => u.family),
    L.filter(u => u.age > 20),
    L.map(u => u.age),
    take(4),
    reduce(add),
    console.log);

객체지향은 데이터를 우선적으로 준비하고 메서드들을 나중에 작성해 나간다면, 함수형 프로그래밍은 이미 만들어져 있는 함수들의 조합으로 데이터를 구성한다.

profile
강지영입니...🐿️

0개의 댓글