mochang2 / development-diary

0 stars 0 forks source link

함수형 프로그래밍 필기 #9

Open mochang2 opened 1 year ago

mochang2 commented 1 year ago
  1. 일급이란?

값으로 다룰 수 있어야 함.
변수에 담을 수 있어야 함. 함수의 인자로 사용될 수 있어야 함.
함수의 결과로 사용될 수 있어야 함.

이를 함수에 적용할 수 있음.

  1. 고차 함수

함수를 값으로 다룸(JS는 함수가 일급이여서 가능함).

  1. 이터러블/이터레이터 프로토콜
  1. 제너레이터/이터레이터
  1. map / filter / reduce 이해하기
function map(convert, iterator) {
    const result = [];

    for (const object of iterator) {
        result.push(convert(object));
    }

    return result;
}

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

console.log(map((product) => product.name, products)); // ['반팔티', '긴팔티', '핸드폰케이스', '후드티', '바지']
console.log(
    map(
        (number) => number,
        function* () {
            yield 1;
            yield 2;
            yield 3;
            yield 4;
        }
    )
); // [1, 2, 3, 4]
function filter(validate, iterator) {
    const result = [];

    for (const object of iterator) {
        if (validate(object)) {
            result.push(object);
        }
    }

    return result;
}

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

console.log(filter((product) => product.price > 20000, products));
// [0: {name: '후드티', price: 30000},
//  1: {name: '바지', price: 25000}]
function reduce(accumulate, initialValue, iterator) {
    let accumulation = initialValue;

    if (!iterator) {
        iterator = accumulation[Symbol.iterator]();
        accumulation = iterator.next().value;
    }

    for (const object of iterator) {
        accumulation = accumulate(accumulation, object);
    }

    return accumulation;
}

const numbers = [1, 2, 3, 4, 5];

console.log(reduce((a, b) => a + b, 0, numbers)); // 15
console.log(reduce((a, b) => a + b, numbers)); // 15

// Array.prototype.join 보다 활용성 높은 join

function join(seperator, iterator) {
    return reduce((a, b) => `${a}${seperator}${b}`, iterator);
}

function* test() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

console.log(join(" - ", new Set([1, 2, 3]))); // 1 - 2 - 3 (순서는 보장 X)
console.log(join(" - ", test())); // 1 - 2 - 3 - 4
  1. go, pipe, curry

현재 위 코드를 사용해서 원하는 값을 표현하려면 뒤에서부터 읽어야 함.

예를 들어 가격이 20000원 미만인 상품들의 가격의 합을 구하고 싶다면 reduce((a, b) => a + b, map((product) => product.price, filter(product => product.price < 20000, products)))와 같은 식으로 말이다.
가독성 좋게 이를 바꿔보고자 함.

function reduce(accumulate, initialValue, iterator) {
    // ...
}

function go(...args) {
    return reduce((accumulation, apply) => apply(accumulation), args);
}

console.log(
    go(
        0,
        (a) => a + 1,
        (a) => a + 10
    )
); // 11
function pipe(f, ...functions) {
    return function (...initialValues) {
        return go(f(...initialValues), ...functions);
    };
}

const addEleven = pipe(
    (a, b) => a + b, // 첫 번째 함수는 여러 개의 인자를 받을 수 있음
    (a) => a + 1,
    (a) => a + 10
);

console.log(addEleven(0, 0)); // 11
function curry(apply) {
    return function (firstArgument, ...args) {
        return args.length // 인자가 2개 이상일 경우 실행하는 예시
            ? apply(firstArgument, ...args)
            : function (...args) {
                  return apply(firstArgument, ...args);
              };
    };
}

const add = curry((a, b) => a + b);
console.log(add(1)); // f (...args) { return apply(firstArgument, ...args)}
console.log(add(2)); // f (...args) { return apply(firstArgument, ...args)}
console.log(add(3)); // f (...args) { return apply(firstArgument, ...args)}
console.log(add(1, 2)); // 3
console.log(add(1)(2)); // 3
  1. range / L.range
const add = (a, b) => a + b;

const range = (l) => {
    let i = -1;
    let result = [];

    while (++i < l) {
        console.log(i, "range"); // 출력 O
        result.push(i);
    }

    return result;
};

const list = range(4);

log(list); // [0, 1, 2, 3]
// log(reduce(add, list)); // 6
const L = {};
L.range = function* (l) {
    let i = -1;
    while (++i < l) {
        console.log(i, "L.range"); // 출력 X, list.next()를 통해 평가가 될 때만 실행됨, 내부적으로 더 효율적으로 동작
        yield i;
    }
};

const list = L.range(4);
log(list); // L.range {<suspended>} <= iterator
// log(reduce(add, list)); // 6
  1. take(결과를 만드는 함수)
const take = (l, iter) => {
    const res = [];

    for (const object of iter) {
        res.push(object);
        if (res.length === l) return res;
    }

    return res;
};

log(take(5, range(100000))); // [0, 1, 2, 3, 4] 오래 걸림, 비효율적
log(take(5, L.range(100000))); // [0, 1, 2, 3, 4] L.range(5)랑 같은 시간이 걸림, 효율적
  1. L.map, L.filter
function map(convert, iterator) {
    const result = [];

    for (const object of iterator) {
        result.push(convert(value));
    }

    return result;
}

const L = {};
L.map = function* (convert, iterator) {
    for (const object of iterator) {
        yield convert(object);
    }
};

const iterator = L.map((number) => number + 10, [1, 2, 3]);
console.log(iterator.next().value); // 11
console.log(iterator.next().value); // 12
console.log(iterator.next().value); // 13
function filter(validate, iterator) {
    const result = [];

    for (const object of iterator) {
        if (validate(object)) {
            result.push(object);
        }
    }

    return result;
}

const L = {};
L.filter = function* (validate, iterator) {
    for (const object of iterator) {
        if (validate(object)) {
            yield object;
        }
    }
};

const iterator = L.filter((number) => number % 2, [1, 2, 3]);
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 3
  1. L.entries, makeQueryString(객체를 쿼리스트링으로 변환시키는 함수)
const L = {};
L.entries = function* (object) {
    for (const key in object) {
        yield [key, object[key]];
    }
};
// go와 pipe에 curry 적용됐다고 가정
function makeQueryString(object) {
    return go(
        object,
        Object.entries, // L.entries도 가능
        map(([key, value]) => `${key}=${value}`), // L.map도 가능
        reduce((string1, string2) => `${string1}&${string2}`)
    );
}

// or

const makeQueryString = pipe(
    Object.entries,
    map(([key, value]) => `${key}=${value}`), // L.map도 가능
    reduce((string1, string2) => `${string1}&${string2}`)
);

console.log(makeQueryString({ limit: 10, offset: 10, type: "notice" }));
  1. map, filter 다시 만들기
function map(apply, iterator) {
    return go(iterator, L.map(apply), take(Infinity));
}
function filter(validate, iterator) {
    return go(iterator, L.filter(validate), take(Infinity));
}
  1. L.flatten
function isIterable(object) {
    return object && object[Symbol.iterator];
}

const L = {};
L.flatten = function* (iterable) {
    for (const maybeObject of iterable) {
        if (isIterable(maybeObject)) {
            for (const value of maybeObject) {
                yield value;
            }
            // 또는 yield* maybeObject;
        } else {
            yield maybeObject;
        }
    }
};

console.log([...L.flatten([[1, 2], 3, 4, [5, 6]])]); // [1, 2, 3, 4, 5, 6]
  1. L.flatMap(flat과 map을 동시에, 시간 복잡도는 동일), flatMap
L.flatMap = curry(pipe(L.map, L.flatten));
const flatMap = curry(pipe(L.map, flatten));
  1. Promise

단순히 콜백 지옥을 해결하는 기능이 아님.
함수형 프로그래밍 관점에서 볼 때 비동기를 일급으로 다룰 수 있게 해줌.
콜백은 내부 컨텍스트에서 외부의 실행 결과를 받아서야만 지속적으로 실행할 수 있고,
Promise는 실행 결과를 외부에서도 받아 지속적으로 실행할 수 있다.

const log = console.log;

const delay100 = (a) =>
    new Promise((resolve) => setTimeout(() => resolve(a), 100));
const go = (a, f) => (a instanceof Promise ? a.then(f) : f(a));
const add5 = (a) => a + 5;

const n1 = 10;
log(go(go(n1, add5), log)); // 15 / undefined

const n2 = delay100(10);
log(go(go(n2, add5), log)); // Promise {<pending>} / 15
  1. go, pipe, reduce에서 비동기 제어
const handleAsynchronous = (value, apply) =>
    value instanceof Promise ? value.then(apply) : apply(value);

function reduce(accumulate, initialValue, iterator) {
    let accumulation = initialValue;

    if (!iterator) {
        iterator = accumulation[Symbol.iterator]();
        accumulation = iterator.next().value;
    }

    return handleAsynchronous(accumulation, function recurse(accumulation) {
        let current;

        while (!(current = iterator.next()).done) {
            const value = { current };
            accumulation = accumulate(accumulation, value);

            if (accmulation instanceof Promise) {
                return accmulation.then(accumulate);
            }
        }

        return accumulattion;
    });
}

go(
    Promise.resolve(1),
    (a) => a + 10,
    (a) => Promise.reject("error~~"),
    (a) => console.log("----"),
    (a) => a + 1000,
    (a) => a + 10000,
    log
).catch((a) => console.log(a));
  1. C.reduce, C.take, C.takeAll, C.map, C.filter
const delay1000 = (a) =>
    new Promise((resolve) => {
        console.log("hi~");
        setTimeout(() => resolve(a), 1000);
    });

go(
    [1, 2, 3, 4, 5],
    L.map((a) => dealy1000(a * a)),
    L.filter((a) => a % 2),
    reduce(add),
    console.log // 1초마다 hi가 출력, 5초 후에 add 결과 반환
);
const C = {};
C.reduce = (f, accumulation, iterator) =>
    iterator
        ? reduce(f, accumulation, [...iterator]) // 스프레드 연산을 통해 지연되었던 평가를 다시 앞당겨서 평가할 수 있음
        : reduce(f, [...accumulation]);

go(
    [1, 2, 3, 4, 5],
    L.map((a) => dealy1000(a * a)),
    L.filter((a) => a % 2),
    C.reduce(add),
    console.log // 1초만에 모든 hi가 출력, 1초 후에 add 결과 반환
);
const C = {};

function noop() {}

const catchNoop = ([...arr]) => (
    arr.forEach((a) => (a instanceof Promise ? a.catch(noop) : a)), arr
);

C.reduce = curry(
    (f, acc, iter) =>
        iter ? reduce(f, acc, catchNoop(iter)) : reduce(f, catchNoop(acc)) // Promise.reject()를 이후 실행에서 명시적으로 catch할 수 있도록 수정
);

C.take = curry((l, iter) => take(l, catchNoop(iter)));

C.takeAll = C.take(Infinity);

C.map = curry(pipe(L.map, C.takeAll));

C.filter = curry(pipe(L.filter, C.takeAll));
mochang2 commented 1 year ago
  1. 명령형 프로그래밍 습관 지우기 - 만능 reduce No!
const users = [
    { name: "AA", age: 35 },
    { name: "BB", age: 26 },
    { name: "CC", age: 28 },
    { name: "CC", age: 34 },
    { name: "EE", age: 23 },
];

console.log(_.reduce((total, u) => total + u.age, 0, users));

위와 같은 형식(user에는 age property가 존재한다는 것을 알아야 함) 보다는 아래와 같은 형식.
reduce의 초기값을 주지 않는 형태가 일반적으로 덜 복잡함.

const users = [
    { name: "AA", age: 35 },
    { name: "BB", age: 26 },
    { name: "CC", age: 28 },
    { name: "CC", age: 34 },
    { name: "EE", age: 23 },
];

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

const getAges = L.map((u) => u.age);

console.log(_.reduce(add, getAges(users)));

만약 유저 조건이 필요하다면 중간에 filter를 추가하면 됨.

  1. 안전하게 합성하기

모나드적 개념을 활용하면 안전하게 합성할 수 있음(아래 예시와 같은 경우 fg의 인자가 숫자가 아니어도).
(사실 인터프리터 언어여서 생기는 문제기도 함)

const f = (x) => x + 10;
const g = (x) => x - 5;
const fg = (x) => f(g(x));

_.go(10, fg, console.log); // 15

_.go(undefined, fg, console.log); // NaN

_.go([], L.map(fg), _.each(console.log)); // 15

_.go([10], L.map(fg), _.each(console.log)); // 15

find 대신 filter(L.filter) 쓰는 방법도 하나의 방법임.

const users = [
    { name: "AA", age: 35 },
    // { name: 'BB', age: 26 },
    { name: "CC", age: 28 },
    { name: "DD", age: 34 },
    { name: "EE", age: 23 },
];

const user = _.find((u) => u.name == "BB", users);
if (user) {
    // undefined가 아닐 경우에 대한 조건문이 추가되어야 함
    console.log(user.age);
}

_.go(
    users,
    L.filter((u) => u.name == "BB"),
    L.map((u) => u.age),
    L.take(1),
    _.each(console.log)
);
  1. 어떠한 값이든 이터러블 프로그래밍으로 다루기
L.keys = function* (obj) {
    for (const k in obj) {
        yield k;
    }
};

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

L.entries = function* (obj) {
    for (const k in obj) {
        yield [k, obj[k]];
    }
};
const object = (entries) =>
    _.go(
        entries,
        L.map(([k, v]) => ({ [k]: v })),
        _.reduce(Object.assign)
    );
  1. OOP의 대체재 개념인가?

모델 설계는 OOP 적으로 해도, 그 안에 메서드는 결국 로직이 필요함.
해당 로직을 if는 filter로, 변형은 map으로, 결과는 take이나 reduce로 대체할 수 있음.
결론: OOP와 섞어서 쓸 수 있는 개념임. 대체재가 아님.

  1. Model, Collection 클래스를 이용한 이터러블 프로토콜
class Model {
    constructor(attrs = {}) {
        this._attrs = attrs;
    }

    get(k) {
        return this._attrs[k];
    }

    set(k, v) {
        this._attrs[k] = v;
        return this;
    }
}

class Collection {
    constructor(models = []) {
        this._models = models;
    }

    at(idx) {
        return this._models[idx];
    }

    add(model) {
        this._models.push(model);
        return this;
    }

    *[Symbol.iterator]() {
        yield* this._models;

        // 또는
        // for (const model of this._models) {
        //     yield model;
        // }
    }

    // 또는
    // [Symbol.iterator]() {
    //     return this._models[Symbol.iterator]();
    // }
}

const coll = new Collection();
coll.add(new Model({ id: 1, name: "AA" }));
coll.add(new Model({ id: 3, name: "BB" }));
coll.add(new Model({ id: 5, name: "CC" }));
console.log(coll.at(2).get("name")); // CC
console.log(coll.at(1).get("id")); // 3

_.go(
    coll,
    L.map((m) => m.get("name")),
    _.each(console.log)
);

_.go(
    coll,
    _.each((m) => m.set("name", m.get("name").toLowerCase()))
    _.each(console.log) // aa, bb, cc
);
  1. takeWhile, takeUntil
_.go(
    [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0],
    L.takeWhile((a) => a), // 1 ~ 8까지만
    _.each(console.log)
);

_.go(
    [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0],
    L.takeUntil((a) => a), // 1까지만
    _.each(console.log)
);

_.go(
    [0, false, undefined, null, 10, 20, 30],
    L.takeUntil((a) => a), // 10까지만
    _.each(console.log)
);