본문 바로가기

카테고리 없음

함수형 프로그래밍2 - 객체를 이터러블로 다루기

객체를 이터러블하게!

객체 → key, value 쌍으로 이뤄진 값
객체는 Object.entries를 통해 배열로 만들수 있었다.
객체를 이렇게 이터러블한 배열로 만듦으로써 동시성 지연성 프로그래밍이 가능하다.

const obj1 = {
    a:1,
    b:2,
    c:5
}

_.go(
    obj1,
    Object.values,
    _.map(a => a+ 1),
    _.reduce((a,b) => a+ b),
    console.log
)

그리고 여기서 지연적으로 동작하는 함수들을 사용하면 obj1 의 모든 key value 쌍을 순회할 필요 없이 평가가 필요한 요소까지만 순회하므로 더 효율적으로 동작할 수 있다. 그렇게 하기 위해서는 먼저 Object.values와 동일한 기능을하면서 지연적으로 동작하는 함수를 작성 해야한다.

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

이렇게 하면 L.take를 사용해서 원하는 부분까지만 yield 하도록 하는것이 가능하다.

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

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

Object.entries도 마찬가지로 지연적으로 동작하게하는 함수를 만들었다.

_.go(
    obj1,
    L.entries,
    L.filter(([k,v])=> v%2),
    _.each(console.log)
)

그래서 이런식으로도 활용이 가능할 것이다. 마지막에 console.log가 아닌 _.each인 이유는 모두 L.filter로부터 평가가 지연된 값을 받았기 때문에 평가가 이뤄지게 해야하기 때문이다.

 

어떤 값이든 이터러블 프로그래밍으로 다루기

위의 예시들을 통해 go의 첫번째 인자가 무슨 값이든 간에 제너레이터 함수를 통해 이터러블하게 만들어서 이터러블 프로그래밍이 가능하도록 만들 수 있음을 알 수 있다. 이번엔 배열에서 객체를 만드는 함수를 작성해보자

const a = [['a', 1], ['b', 2], ['sdf', 2]]
//목표 결과물 {a:1, b:2, sdf:2}

const objectt = entries => _.go(
    entries,
    _.map(([k,v])=> ({[k] : v})),
    _.reduce(Object.assign),
    console.log
)

위와 같이 map과 reduce를 사용해서 Object로 만들어낼 수 있지만 reduce 하나로도 조금 더 깔끔하게 가능하다.

const objectt = entries => _.reduce(
    (obj, [k, v]) => {
        obj[k] = v
        return obj
    }, {}, entries)

이렇게 작성한 object 함수는 Map과도 잘 동작하게 된다.

const map = new Map();
map.set('a', 23);
map.set('b',3243);

console.log(objectt(a));
console.log(objectt(map))

이번엔 object의 각 value에 map을 해서 새로운 object를 만드는 함수를 만들어보자

const mapObject = (f, obj) => _.go(
    obj,
    L.entries,
    _.map(([k,v])=>[k,f(v)]),
    _.map(([k,v])=> ({[k]:v})),
    _.reduce(Object.assign)
)
console.log(mapObject(a=>a+112,obj1));

지금까지 만든 함수들을 이용한다. L.entries를 통해 키와 값을 배열형태로 뽑아내고 각 value들에 f 함수를 적용해서 원하는 값으로 만든다. 이후엔 다시 reduce를 통해 객체로 만들어낸다.


객체에서 원하는 키값만 뽑아내기

원하는 키값만 뽑아서 객체를 만드려면 어떻게 하는게 좋을까?? 강의를 보기전에 혼자 한번 짜봤다.

const pick = (target ,obj) => _.go(
    obj,
    L.entries,
    _.filter(([k,v])=> target.includes(k)),
    constructObject
)

console.log(pick(['b','c','asb'], obj1))

객체의 모든 키값 쌍을 순회하기 때문에 아래의 코드가 더 효율적이다. (수업에서도 내가 짠 거랑 똑같은 로직으로 먼저 보여주시고 이것보다 더 나은 코드가 있다며 이 코드를 알려주셨다.)

const pick2 = (ks, obj) => _.go(
    ks,
    L.map(k => [k, obj[k]]),
    L.reject(([k,v]) => v === undefined),
    constructObject
)

원하는 키 배열을 순회하니 시간복잡도가 더 줄었다고 할 수 있겠다.


객체에 인덱스를 추가하기

const users2 = [
    { id: 5, name: 'AA', age: 35 },
    { id: 10, name: 'BB', age: 26 },
    { id: 19, name: 'CC', age: 28 },
    { id: 23, name: 'CC', age: 34 },
    { id: 24, name: 'EE', age: 23 }
  ];
  
_.indexBy = (f, iter) => _.reduce((obj,a) => (obj[f(a)] = a, obj), {}, iter)
console.log(_.indexBy(a => a.id,users2))

해당 users2에서 원하는 값을 가져오려면 순회를 하면서 동작하는 find가 필요하다. 보다 더 효율적으로 원하는 값을 가져올 수 있도록 인덱스를 넣어주는 함수를 만들었다.

만약 필터링도 하고 싶다면 L.entries와 filter 함수를 이용해서 원하는 조건에 맞는 값만 남기도록 하는 것도 가능할 것이다.

 

사용자 정의 객체를 이터러블로 다루기

Map과 Set은 일종의 사용자 정의 객체이다.

let m = new Map();
m.set('a', 1);
m.set('as', 12);
m.set('asd', 123);
let miter = m[Symbol.iterator]();
console.log(miter.next());
console.log(miter.next());
console.log(miter.next());

Symbol.iterator를 했을때 key와 value가 함께 나오는 것을 확인한 후 ( iter.next()의 결과가 어떤 형태인지 확인 후) 다음과 같이 작성 가능하다.

_.go(
    m,
    L.filter(([k,v])=>v%2),
    e => new Map(e),
    console.log
)

 

이제 객체지향과 함께 이터러블 프로그래밍을 해보자. 큰 틀은 객체지향적으로 하고 클래스 내부의 메소드들을 구현할 때에 이터러블 프로그래밍을 적용하여 보다 더 효율적으로 코딩할 수 있을 것이다.

class Model {
    constructor(attrs = {}){
        this._attrs = attrs;
    }

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

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

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

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

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

const coll = new Collections();
coll.add(new Model({id : 1, name : 'ads'}));
coll.add(new Model({id : 2, name : 'lsh'}));
coll.add(new Model({id : 4, name : 'l'}));
coll.add(new Model({id : 21, name : 'h'}));
coll.add(new Model({id : 212, name : 's'}));

console.log(coll.at(1).get('id')) // 결과 : 2

이렇게 객체지향적으로 설계되어있는 코드가 있다. 해당 Collections 객체를 순회하면서 동작하는 코드를 작성해보면

_.go(
    L.range(3),
    L.map(i => coll.at(i)),
    L.map(i => i.get('id')),
    _.each(console.log)
)

하지만 위 코드는 숫자에 의존해야하고 재사용성도 좋지 않다. 이것보다는 Collections 객체 자체에서 이터러블하게 동작하도록 구현하는 것이 좋다. 강의의 맨 첫시간에 배웠던 이터레이터에 답이 있다. 바로 Collections 객체 내부에 Symbol.iterator를 구현해주면 된다.

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

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

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

    *[Symbol.iterator]() {
        for(const a of this._models) yield a;
    }
}


console.log([...coll]);

위와같이 이터러블 프로토콜을 따르도록 해줌으로써 아래 전개연산자도 제대로 동작하게 된다. 객체지향과 이터러블이 합쳐지는 순간이다....! 😲😲 근데 위의 Symbol.iterator 함수를 좀 더 간단하게 구현할 수 있다.

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

 

응용편

객체지향 프로그래밍에서 이터러블, 함수형 프로그래밍을 좀 더 활용해보자.

class Product extends Model {

}

class Products extends Collections {
    totalPrice() {
        let total = 0;
        this._models.forEach(i => {
            total += i.get('price');
        })
        return total;
    }
}

const product = new Products();
product.add(new Product({id : 1, price : 100}))
product.add(new Product({id : 2, price : 300}))
product.add(new Product({id : 3, price : 700}))
console.log(product.totalPrice());

price의 합계를 구하는 totalPrice 함수는 좀 더 이터러블하게 바꿀 수 있다. Collection의 Symbol.iterator에서 _models 배열을 순회하도록 했기 때문에 아래와 같이

class Products extends Collections {
    totalPrice() {
        return _.go(
            this,
            L.map(p => p.get('price')),
            _.reduce((a,b)=>a+b)
        )
    }
}

이렇게 go함수의 첫번째 인자로 this를 줘도 된다. 이와같은 방식으로 사용자 정의 객체도 이터러블하게 프로그래밍할 수 있다. 하지만 더 많은 함수 만들어서 기본 값을 다루는 것이 함수형 프로그래밍에서는 더 유리하다고 한다.

 

관련글

https://www.inflearn.com/course/%ED%95%A8%EC%88%98%ED%98%95_ES6_%EC%9D%91%EC%9A%A9%ED%8E%B8/dashboard 를 공부하며 정리한 글입니다.