이터러블 프로그래밍
홀수 n개 더하기를 명령형으로 구현해보면 아래와 같이 효율적으로 가능하다.
function f1(limit, list) {
let acc = 0;
for(const a of list) {
if(a % 2) {
const b = a * a;
acc+=b;
limit--;
if(limit == 0) break;
}
}
return acc;
}
let a= f1(3, [1,2,3,4,5,6,7,8,9,10]);
console.log(a);
위의 코드를 한번 생각해보면 이터러블한 객체를 순회하며 특정 조건에 부합할 때(filter) 값을 조작하여(map) 결과를 만들어내는데(reduce) 특정 조건에서 break문을 통해 빠져나오고 있다.(take) 이를 적용해서 코드를 아래와 같이 바꿀 수 있다.
function f1(limit, list) {
let acc = 0;
for (const a of L.take(3, L.map(a => a*a,L.filter(a => a % 2, list))))
acc += a;
return acc;
}
특정 조건에 맞는 요소만 뽑아서 map을 적용하고 원하는 만큼만 뽑아내도록 take를 통해 break의 역할을 하도록 했다. 그리고 축약을 위해 reduce를 적용한다
const add = (a,b)=>a+b;
function f1(limit, list) {
return reduce(add,
L.take(3,
L.map(a => a*a,
L.filter(a => a % 2, list))))
}
이렇게 이터러블한 객체를 어떤 값으로 축약할 때 사용하는 것이 reduce 이다. 선언적으로 되어있어서 굉장히 읽기가 더 편하다. 이것보다 더 읽기 쉽게 만들자면 실행되는 것을 위에서 아래로 순서대로 진행되도록 go 함수를 사용할 수 있겠다.
function f1(limit, list) {
go(
list,
L.filter(a => a %2),
L.map(a => a*a),
L.take(limit),
reduce(add),
console.log
)
}
위의 과정을 통해서 명령형적으로 짜던 코드를 선언적으로 짤 수 있게 된다.
만약 if를 통해 조건에 맞는거만 담고싶다 → filter
값을 연산에서 담고 싶으면 → map
break 기능 or 시간 복잡도를 줄이고 싶다 → take
모든 값을 하나로 축약하는 연산을 해야한다 → reduce
while문을 선언형으로(range)
function f2(end) {
let i =0 ;
while(i < end) {
console.log(i++);
}
}
//선언적으로 바꾸기
function f2(end) {
_.each(console.log, L.range(end));
}
위의 each함수를 사용하게 되면 순수한 대상의 영역과 해당 대상에게 어떠한 작업을 하는 영역으로 구분을 짓는것이 가능하다. each 함수의 결과는 두번째 인자로 전달된 이터러블 객체가 그대로 반환된다. 그래서 첫번째 인자인 함수에서 어떠한 변화가 일어날 것으로 예측이 가능하고 이를 통해 구분을 짓게 된다.
별그리기 및 구구단에 응용
C언어에서 2차원 배열을 처음 배울때 꼭 거치는 실습 별그리기.. 이를 선언적으로 LISP한 스타일로 구현하면 어떻게 될까?
_.go(
_.range(1, 6),
_.map(_.range), // [[0],[0,1],[0,1,2],[0,1,2,3],[0,1,2,3,4],[0,1,2,3,4,5] ...]
_.map(_.map(a => '*')), // [[*], [*,*], [*,*,*] ...]
_.map(_.reduce((a,b) => `${a}${b}`)), // [[*], [**], [***] , ...]
_.reduce((a,b) => `${a}\n${b}`),
console.log
)
이렇게 map 안에 map을 둬서 이차원 배열을 생성할 수 있다. 그리고 이전 강의에서 있었던 join을 쓰면 좀 더 편하게 만들 수 있다.
_.go(
_.range(1, 6),
_.map(_.range),
_.map(_.map(a => '*')),
_.map(join('')),
join('\n'),
console.log
)
이번엔 구구단을 구현해보자. 먼저 2~9 까지 들어있는 리스트를 뽑아서 map 내부에 go 함수를 돌린다. go 함수에서는 1~9 리스트를 만들고 각 값에다가 곱셈을 해준다. 언더바가 아니고 L 로 바꾸면 지연적으로 동작하는 함수가 될것이다.
_.go(
_.range(2, 10),
_.map(a => _.go(
_.range(1,10),
_.map(b=>b*a))
),
console.log
)
reduce 함수 사용해보기
const users = [
{name:'AA', age:43},
{name:'BB', age:13},
{name:'C', age:32},
{name:'D', age:6},
{name:'A', age:1},
{name:'LAS', age:3},
];
//아래와 같이 reduce를 사용하여 user들의 age를 더할 수 있다.
console.log(
_.reduce((a,b) => a+b.age,0,users);
)
위의 reduce는 아쉬운점이 몇가지 있다. 시작값을 주고 있다는 것 그리고 users의 구조를 분석해서 입력해줘야한다는 점이다. 이를 바꿔주기 위해서 reduce내부에 map을 넣어보자.
console.log(
_.reduce((a,b) => a+b,L.map(u=>u.age, users))
)
이렇게 하면 보다 더 간결한 코드가 된다. 이렇게 해두면 코드조각들을 나누면서 아래와 같이 조합과 분리가 가능하다.
const ages = L.map(u=>u.age);
console.log(
_.reduce((a,b) => a+b, ages(users))
)
//curry = f ⇒ (a, ...) ⇒ .length ? f(a,...) : (...) ⇒ f(a, ..._)
ages에 인자를 따로따로 받을 수 있는것은 L.map에 curry가 적용되었기 때문이다. 이렇게 쪼개가면서 재사용되기 쉽거나 간결한 함수를 만들어가는 것이 보다 더 이터러블한 프로그래밍 방법이다.
해당 예시에 대해 정리하면 우선 시간복잡도에서는 차이가 나지 않는다. 하지만 가독성이 훨씬 좋아지고 reduce함수가 자신의 역할에만 충실하도록 구현이 되면서 지연평가가 가능한 L.map을 사용하면서 array를 만들며 동작하지 않는다는 이점이 있다.
1. obj → query string 함수 만들기
const obj1 = {
a:1,
b:undefined,
c : 'cc',
d : "DD"
}
function query1(obj) {
let res = '';
for(const k in obj) {
const v = obj[k];
if(v === undefined) continue;
if(res !='') res += '&';
res += k + '=' + v;
}
return res;
}
query1 함수는 obj1의 key value 쌍을 k=v&k=v&k=v 형태로 만들어주는 함수이다. 위 함수를 함수형적 사고를 통해 바꿔보자. 먼저 obj를 key-value 쌍의 이터러블한 객체로 만든다. 그리고 undefined를 걸러내기위한 filter를 사용한다. 다음으로 map을 사용해서 k=v로 만들고 reduce을 사용해서 결과값들을 합쳐 하나로 만들어낸다.
function query2(obj) {
return (
_.reduce((a,b)=> `${a}&${b}` ,
_.map(([k,v]) => `${k}=${v}`,
_.filter(([_,v]) => v !== undefined , //안쓰는 변수는 _라고 함
Object.entries(obj)))))
}
위의 코드는 조금 더 수정이 가능하다. 이전 강의에서 만들었던 join을 쓰고 go 함수를 통해 가독성을 더 높일 수 있다.
const join = _.curry(
(sep, iter) => _.reduce((a,b)=> a+sep+b, iter)
)
const query3 = (obj) => _.go(
obj,
Object.entries,
_.filter(([_,v]) => v !== undefined),
_.map(([k,v]) => `${k}=${v}`),
join('&')
)
이렇게 명령형 스타일의 코드를 간결하게 풀어낼 수 있다. reduce만을 사용해서 만드는 코드도 보여주셨는데 명령형때와 비교했을때 여전히 복잡했다. 그러므로 reduce 단독으로 사용하는 것 보다는 다른 filter map등의 함수들과 조합해서 만들었을때 비로소 함수형 프로그래밍의 이점을 볼 수 있다.
2. string → obj 함수 만들기
const split = _.curry((sep, str) => str.split(sep));
const queryToObj = _.pipe(
split('&'),
_.map(split('=')),
_.map(([k,v]) => ({[k]:v})),
_.reduce((obj, a) => Object.assign(obj,a))
)
console.log(queryToObj("a=1&sf=23"))
복잡한 기능을 위해 작은 기능을 하는 함수들을 먼저 구현하고 조합해나가면 원하는 기능을 좀 더 빠르게 개발하는 것이 가능하다. (함수형 프로그래밍의 장점은 사고의 분리에 있는 것 같다.)
안전한 합성
1. map
올바른 인자가 넘어가지 않을 경우에, 아무 값도 넘어가지 않을 경우를 처리하기 위해서 어떻게 합성해야할까? 모나드의 관점에서 아래와 같이 가능하다.
const f = x => x + 10;
const g = x => x - 52;
// f(g(4))
_.go(
[4],
L.map(fg),
_.each(console.log)
)
이렇게 인자들을 배열로 감싸준 후에 배열 내부에 값이 없으면 실행되지 않도록 하고 map을 활용해서도 안전한 함수 합성이 가능하다.
2. L.filter
const users = [
{name:'AA', age:43},
{name:'BB', age:13},
{name:'C', age:32},
{name:'D', age:6},
{name:'A', age:1},
{name:'LAS', age:3},
];
const user = _.find(u => u.name == 'BB', users);
if(user) console.log(user);
find 함수를 쓰면 위와같이 원하는 값을 찾을 수 있다.
_.each(console.log, L.take(1, L.filter(u => u.name == 'BB',users)));
find 대신 L.filter를 쓰면 하나의 표현식으로 깔끔하게 작성이 가능하다. 또한 아무런 인자가 들어오지 않았을 경우에도 잘 동작하게 된다.
_.go(
users,
L.filter(u => u.name == "no name"),
L.map(u => u.age),
L.take(1),
_.each(console.log)
)
관련글
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 를 공부하며 정리한 글입니다.
'프론트엔드 > 자바스크립트' 카테고리의 다른 글
함수형 프로그래밍2 - 추상화 레벨 높이기 (0) | 2021.08.10 |
---|---|
함수형 프로그래밍2 - 프론트단에서 응용 (0) | 2021.08.10 |
this 와 클로저 (0) | 2021.07.30 |
jQuery 엘리먼트 제어 기초 (0) | 2021.01.27 |
jQuery 간단 정리 (0) | 2021.01.26 |