Array.prototype.join 보다 다형성이 높은 join 함수
map filter take reduce 함수는 아래를 참고하자.
### range, map, filter, take, reduce 중첩 사용
<script>
const range = l => {
let i = -1;
let res = [];
while (++i < l) {
res.push(i);
}
return res;
};
const map = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
res.push(f(a));
}
return res;
});
const filter = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) res.push(a);
}
return res;
});
const take = curry((l, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
res.push(a);
if (res.length == l) return res;
}
return res;
});
const reduce = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
} else {
iter = acc[Symbol.iterator]();
}
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
acc = f(acc, a);
}
return acc;
});
go(range(10),
map(n => n + 10),
filter(n => n % 2),
take(2),
log);
</script>
아래의 reduce가 있는 코드의 경우에는 array에 있는 join함수를 떠올릴 수 있다.
array에 있는 join함수는 array prototype에만 붙어있는 함수인데, reduce는 이터러블 객체를 다 순회하면서 축약을 할 수 있기 때문에 다양성이 높은 join함수라고 볼 수 있다.
<script>
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
reduce((a, b) => `${a}&${b}`)
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
</script>
join함수를 reduce를 통해서 만들어서 아랫부분을 간결하게 하고 해당하는 함수를 재사용가능하게 해보자.
join 같은 경우 sep를 받고 iter 이터러블 값을 받는다. 그리고 위에코드처럼 reduce를 하는데 받은 sep를 a와 b사이에 넣는다.
const join = (sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter);
join같은 경우 기본값이 ,으로 되어있어 기본 파라미터로 ,를 주고 pipe라인에서 조합하기 좋게 curry를 적용해서 결과를 얻어낸다. 이렇게 함수형프로그래밍은 pipe 사이에 있는 함수들을 꺼내서 조합성과 재사용성이 높게 프로그래밍 할 수가 있다.
<script>
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
</script>
// limit=10&offset=10&type=notice
원래 join같은 경우에는 아래와 같이 결과를 만들 수 있다. 즉 사이에 특정 seperator를 줘서 만들 수 있는 메소드이다.
앞에 있는 값이 반드시 배열이여야만 사용할 수 있다.
[1, 2, 3, 4].join()
"1,2,3,4"
[1, 2, 3, 4].join(' - ');
"1 - 2 - 3 - 4"
위에서 만든 join 같은 경우에는 배열이 아닌것도 사용할 수가 있다.
받는 값을(iter) reduce를 통해서 축약을 하기 때문에 그렇다. 예를 들면 아래와 같은 이터레이터를 만드는 제너레이터 함수를 정의한다면 기존의 join 같은 경우에는 log결과를 만들 수가 없다.(error가 난다.)
function* a() {
yield 10;
yield 11;
yield 12;
yield 13;
}
log(a().join(','));
// 2prac.html:180 Uncaught TypeError: a(...).join is not a function
위에서 만들어진 join을 쓰게 되면 결과를 만들 수가 있다. 그래서 훨씬 조합성이 높고 reduce를 통해서 축약했기 때문에 이터러블 프로토콜을 따르고 있다는 것이다.
function* a() {
yield 10;
yield 11;
yield 12;
yield 13;
}
log(join(' - ', a()));
// 10 - 11 - 12 - 13
즉 이터러블 프로토콜을 따르고 있다는 것은 join에게 가기전에 만들어지는 값들을 지연할 수 있다는 이야기이다.
reduce를 통해 하나씩 next를 통해 앞에서 만들어준 이터레이터의 결과를 꺼낼 것이기 때문이다.
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
function* a() {
yield 10;
yield 11;
yield 12;
yield 13;
}
log(join(' - ', a()));
그래서 L.map이여도 결과를 잘 만들수가 있다.
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
L.map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice
entries같은 경우에도 똑같이 지연을 시킬 수 있는데 위에서 내려온 a를 그대로 리턴한다고 보고 a를 출력을 해보면
계산을 다 마쳐서 만들어져서 배열을 join할 수가 있다.
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
function (a) {
console.log(a);
return a;
},
join('&')
);
//(3) ["limit=10", "offset=10", "type=notice"]
//2prac.html:174 limit=10&offset=10&type=notice
//2prac.html:184 10 - 11 - 12 - 13
그리고 L.map을 사용해서 연산이 되지않는 상태의 이터레이터를 join으로 주어서 join이 안쪽에 next를 통해 하나씩 a b를 풀어서 그 결과를 그 때 연산을 하는 식으로 미룰 수가 있다. 즉 위에서 const join으로 만든 것이array prototype보다 훨씬 다양성이 높다고 할 수 있다.
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
L.map(([k, v]) => `${k}=${v}`),
function (a) {
console.log(a);
return a;
},
join('&')
);
// Generator {<suspended>}
// 2prac.html:171 limit=10&offset=10&type=notice
// 2prac.html:181 10 - 11 - 12 - 13
객체지향적으로 클래스를 기반으로 추상화를 하는 거보다 훨씬 유연한 방식에 join함수라는 것을 알 수가 있다. 그리고 연산을 다루는 재밌는 기법도 활용할 수가 있다.
그래서 아래와 같이 연산을 마쳐서 깨트린 결과를 주는 것이 아니라
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
function (a) {
console.log(a);
return a;
},
join('&')
);
//(3) ["limit=10", "offset=10", "type=notice"]
//2prac.html:174 limit=10&offset=10&type=notice
//2prac.html:184 10 - 11 - 12 - 13
해당하는 함수를 적용하기로 약속된 즉 준비된 미뤄져있는 결과를 join에게 던져도 된다는 것이다.
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
L.map(([k, v]) => `${k}=${v}`),
function (a) {
console.log(a);
return a;
},
join('&')
);
// Generator {<suspended>}
// 2prac.html:171 limit=10&offset=10&type=notice
// 2prac.html:181 10 - 11 - 12 - 13
L.map역시도 위에서 내려주는 값이 이터레이터 여도 되기 때문에 Object.entries자체도 즉시 결과를 줄 수 있지만,
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
a => (console.log(a), a),
L.map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
역시 이터레이터로 결과를 흘려보낼 수도 있다. L.entries가 있다면 obj를 받는 제너레이터를 정의를 한다. obj를 받아서 for in 문을 사용해서 key를 꺼내고 yield를 하면서 key와 obj의 value를 꺼내주면 entries가 준비가된 제너레이터 함수가 되는 것이다.
<script>
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(
Object.entries,
a => (console.log(a), a),
L.map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
function* a() {
yield 10;
yield 11;
yield 12;
yield 13;
}
log(join(' - ', a()));
</script>
이러한 제너레이터 함수를 이용해서 결과를 만들면 해당하는 obj를 평가했을 때 next를 통해서 결과를 뽑아주기 때문에
아래와 같이 만들어진 애로 전달을 했을 때
아직 연산되지 않은 이터레이터가 L.map으로 전달되고
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,
a => (console.log(a), a),
L.map(([k, v]) => `${k}=${v}`),
join('&')
);
여기까지도 아직 연산되지 않은 값으로 미뤄져서 간 다음에 join을 통해 만들어 지는 것이다.
그래서 결론
array prototype의 join보다 위에서 만든 join이 훨씬 다형성이 높고 재밌는 것들을 할 수 있는 join이고 reduce의 성질이기도 하다.
이렇게 queryStr을 만들어 보았다.
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('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice
// 2prac.html:181 10 - 11 - 12 - 13
const join = curry((sep, iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
<script>
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('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
// function* a() {
// yield 10;
// yield 11;
// yield 12;
// yield 13;
// }
// log(join(' - ', a()));
</script>
// limit=10&offset=10&type=notice
<출처 : 유인동 함수형 프로그래밍과 JavaScript ES6+>
https://www.inflearn.com/course/functional-es6/dashboard
함수형 프로그래밍과 JavaScript ES6+ - 인프런 | 강의
ES6+와 함수형 프로그래밍을 배울 수 있는 강의입니다. 이 강좌에서는 ES6+의 이터러블/이터레이터/제너레이터 프로토콜을 상세히 다루고 응용합니다. 이터러블을 기반으로한 함수형 프로그래밍,
www.inflearn.com