// javascript 에서 리스트를 순회하는 법
const log = console.log;
function f(list, length) {
for (const a of list) {
log(list);
}
}
function main() {
f([1,2,3,4,5],2)
}
main();
일반적으로 개발자가 작성하는 코드는 다음과 같습니다.
// 리스트에서 홀수를 length 만큼 뽑아서 제곱한 후 모두 더하기
const log = console.log;
function f(list, length) {
let i = 0;
let acc = 0;
for (const a of list) {
if(a % 2) {
acc = acc + a * a;
if (++i == length) break;
}
}
log(acc);
}
function main() {
f([1,2,3,4,5],1);
f([1,2,3,4,5],2);
f([1,2,3,4,5],3);
}
거의 전부를 표현했다고 볼 수 있습니다. 프로그램을 작성하는 모든 추상화되어있는 로직(기능)이 여기서 나올 수 있습니다. if 를 사용해서 제어를 한다든지, 연산을 한다든지, for문을 최적화 하기 위해, 시간복잡도를 좋게 하기 위해 break 를 사용한다던가, 외부에 영향을 주는 등의 log를 찍거나 하는 행위를 할 수 있습니다.
if 를 함수형 프로그래밍으로 변환
일단, 함수형 프로그래밍에서 if 같은 경우는 if를 한번만 사용할 경우 filter 라고 합니다.
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
위의 함수는 위의 if(a % 2) 를 특정조건일 경우 yield 를 할 수 있도록 하고, 일급함수로 받은 함수를 사용하여, 어떤 조건일 때 필터링을 할 것인지 위임하는 형태로 구성하게 됩니다.
// 리스트에서 홀수를 length 만큼 뽑아서 제곱한 후 모두 더하기
const log = console.log;
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
}
function f(list, length) {
let i = 0;
let acc = 0;
for (const a of filter(a => a % 2, list)) {
acc = acc + a * a;
if (++i == length) break;
}
log(acc);
}
function main() {
f([1,2,3,4,5],1);
f([1,2,3,4,5],2);
f([1,2,3,4,5],3);
}
main();
if문을 없애고, list 를 넣는 곳에 filter 연산에 대한 다양성을 대체할 수 있게 됩니다.
a * a 를 함수형 프로그래밍으로 변환
어떤 특정한 값이 다른 값으로 바꾸는 작업을 함수형 프로그래밍에서 map 이라는 함수를 통해 추상화되어있습니다.
function *map(f,list) {
for (const a of list) {
yield f(a);
}
}
으로 변환할 시에, 다음과 같이 변경할 수 있게 됩니다.
// 리스트에서 홀수를 length 만큼 뽑아서 제곱한 후 모두 더하기
const log = console.log;
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
}
function *map(f,list) {
for (const a of list) {
yield f(a);
}
}
function f(list, length) {
let i = 0;
let acc = 0;
for (const a of map(a => a * a, filter(a => a % 2, list))) {
acc = acc + a;
if (++i == length) break;
}
log(acc);
}
function main() {
f([1,2,3,4,5],1);
f([1,2,3,4,5],2);
f([1,2,3,4,5],3);
}
main();
++i 를 함수형 프로그래밍으로 변환
++i 는 1씩 증가하는 명령어 함수인데요. 명령어 코드는 실제 구체적으로 어떻게 할 것인지를 구술하는 명령어인데요. 실제 서술하는 서술형 함수입니다.
여담으로 순회가 가능한 값을 list 나 배열로 부르지않고 이터러블이라고 부릅니다. 좀더 추상화레벨에 높은 순회가 가능한 객체를 말합니다.
function take (length, iter) {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == length) return res;
}
return res;
}
break 문을 걸 수도 있지만, 함수형 프로그래밍은 계속 return 하는 형태가 좋습니다.
// 리스트에서 홀수를 length 만큼 뽑아서 제곱한 후 모두 더하기
const log = console.log;
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
}
function *map(f,list) {
for (const a of list) {
yield f(a);
}
}
function take (length, iter) {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == length) return res;
}
return res;
}
function f(list, length) {
let i = 0;
let acc = 0;
for (const a of take(length, map(a => a * a, filter(a => a % 2, list)))) {
acc = acc + a;
}
log(acc);
}
function main() {
f([1,2,3,4,5],1);
f([1,2,3,4,5],2);
f([1,2,3,4,5],3);
}
main();
외부세상 일은 외부세상에서
함수형 프로그래밍에서 굉장히 중요한 것이 있습니다.
f([1,2,3,4,5],1);
외부에서 어떤 함수에게 메시지를 전달하여, 그 함수가 어떤 외부 세상에 영향을 끼치는 것보다, 최대한 인자와 리턴값으로 소통하고 외부세상에 영향을 끼치는 것은 외부세상에서 하도록 하는 식으로 권장합니다.
function f(list, length) {
let i = 0;
let acc = 0;
for (const a of take(length, map(a => a * a, filter(a => a % 2, list)))) {
acc = acc + a;
}
return acc;
}
function main() {
log(f([1,2,3,4,5],1));
log(f([1,2,3,4,5],2));
log(f([1,2,3,4,5],3));
}
이터레이터 or 이터러블 에 대한 함수형 프로그래밍
function reduce(f, acc, iter) {
for (const a of iter) {
acc = f(acc,a);
}
return acc;
}
acc 를 a 를 더하여 계속 acc를 축약한 후에 해당 값을 return 할 것인데, 그 축약하는 것을 외부로 위임하는 것입니다.
function f(list, length) {
return reduce (
acc => acc + a,
0,
take(length, map(a => a * a, filter(a => a % 2, list))));
}
}
const log = console.log;
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
}
function *map(f,list) {
for (const a of list) {
yield f(a);
}
}
function take (length, iter) {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == length) return res;
}
return res;
}
function reduce(f, acc, iter) {
for (const a of iter) {
acc = f(acc,a);
}
return acc;
}
function f(list, length) {
return reduce (
(acc, a) => acc + a,
0,
take(length, map(a => a * a, filter(a => a % 2, list))));
}
function main() {
log(f([1,2,3,4,5],1));
log(f([1,2,3,4,5],2));
log(f([1,2,3,4,5],3));
}
main();
이렇게 되면, 단순한 함수하게 표현가능합니다.
const log = console.log;
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
}
function *map(f,list) {
for (const a of list) {
yield f(a);
}
}
function take (length, iter) {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == length) return res;
}
return res;
}
function reduce(f, acc, iter) {
for (const a of iter) {
acc = f(acc,a);
}
return acc;
}
const add = (a,b) => a + b;
const f = (list, length) =>
reduce (
(acc, a) => acc + a,
0,
take(length, map(a => a * a, filter(a => a % 2, list))));
function main() {
log(f([1,2,3,4,5],1));
log(f([1,2,3,4,5],2));
log(f([1,2,3,4,5],3));
}
main();
함수형 프로그래밍 언어의 가장 기초적인 추상화 단계가 끝이 났습니다.
const add = (a,b) => a + b;
const f = (list, length) =>
reduce (
add,
0,
take(length, map(a => a * a, filter(a => a % 2, list))));
함수형 프로그래밍 언어로 바꾸면 매우 읽기가 쉬워졌는데요. 오른쪽에서 왼쪽으로 가면서 읽으면됩니다.
list 를 가지고, (a % 2) 라는 조건으로 필터링을 한다음에,
a * a 라고 map(ping)하여, 하나씩 값을 대입하여 바꾸고,
그중에 length 만큼만 take (꺼내서)
0 부터,
모두다 add (더해서) reduce (결과를) 내어라.
라는 뜻을 가지게 됩니다. 그리고 조금더 편하게 만들면,
reduce (add, 0,
take(length,
map(a => a * a,
filter(a => a % 2, list))));
필터하고, 맵핑하고 가져와서 reduce하면된다. 가 됩니다.
여기까지가 17분 40초 분량입니다.
함수형 프로그래밍은 함수도 값이니까 함수도 축약할 수 있습니다.
go는 리스트 프로세싱으로
go(10, a=> a +1, a=> a+10, log);
10을 1을 더하고 10을 더한후 log에 출력해라는 list 를 값으로 다루면서 적절하게 평가가 가능한 구조로 진행됩니다.
const go = (a, ...fs) => reduce((a, f) => f(a), a, fs);
go(10, a=> a+10, a=>a+1, log)
reduce 같은 경우에 acc 를 생삭하고 f 와 iter 만 넘길 경우, 념겨진 파라미터(arguments)가 2개가 되므로,
function reduce(f, acc, iter) {
if(arguments.length == 2) {
iter = acc[Symbol.iterator]();
acc = iter.next().value; // 첫번째에 있는 값을 꺼내서 넣겠다.
}
for (const a of iter) {
acc = f(acc,a);
}
return acc;
}
이렇게 만들면, reduce가 재귀함수처럼 사용할 수 있게 됩니다.
const go = (a, ...fs) => reduce((a, f) => f(a), a, fs);
function reduce(f, acc, iter) {
if(arguments.length == 2) {
iter = acc[Symbol.iterator]();
acc = iter.next().value; // 첫번째에 있는 값을 꺼내서 넣겠다.
}
for (const a of iter) {
acc = f(acc,a);
}
return acc;
}
처럼 사용가능합니다. 조금더 읽기 편하게 좌측부터 읽을 수 있도록 다음과같이 처리가 가능합니다.
const f2 = (list, length) => go(
list,
list => filter(a=>a%2,list),
list => map(a=>a*a,list),
list => take(length, list),
list => reduce(add,list)
)
filter 를 먼저 처리한 후 mapping 후, take 한 후에 reduce 를 처리해라 라고 말입니다. 해당 스크립트는 main 과 main2 로 분리하여 최종 처리하였습니다.
const log = console.log;
function *filter(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
}
function *map(f,list) {
for (const a of list) {
yield f(a);
}
}
function take (length, iter) {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == length) return res;
}
return res;
}
const go_old = (a, ...fs) => reduce((a, f) => f(a), a, fs); // arguments 값을 변경할 수 있으면 다같이 무떵그려저 값을 전달할 수 있음.
const go = (...fs) => reduce((a, f) => f(a), fs);
function reduce(f, acc, iter) {
if(arguments.length == 2) {
iter = acc[Symbol.iterator](); // acc 가 iter 임 (2번째값)
acc = iter.next().value; // 첫번째에 있는 값을 꺼내서 넣겠다.
}
for (const a of iter) {
acc = f(acc,a);
}
return acc;
}
const add = (a,b) => a + b;
const f = (list, length) =>
reduce (
(acc, a) => acc + a,
0,
take(length, map(a => a * a, filter(a => a % 2, list))));
function main() {
log(f([1,2,3,4,5],1));
log(f([1,2,3,4,5],2));
log(f([1,2,3,4,5],3));
}
main();
const f2 = (list, length) => go(
list,
list => filter(a=>a%2,list),
list => map(a=>a*a,list),
list => take(length, list),
list => reduce(add,list)
)
function main2() {
log(f2([1,2,3,4,5],1));
log(f2([1,2,3,4,5],2));
log(f2([1,2,3,4,5],3));
}
main2();
여기까지가 23분 까지의 내용입니다.
커링(curry)
const curry = f => (a, ...bs) => bs.length ? f(a, ...bs) : (...bs) => f(a, ...bs);
curry은 함수를 받아서, 일다 인자를 받아본 후에, 인자가 두개 이상 들어왔을 경우, 그 인자를 모두 받아서 처리하도록 하고, 인자가 하나일 경우, 그 다음에 인자를 받는 함수를 호출 할 수 있도록 합니다.
const curry = f => (a, ...bs) => bs.length ? f(a, ...bs) : (...bs) => f(a, ...bs);
const add = curry((a,b) => a+b);
이렇게 처리할 수 있게 됩니다. 그럴 경우 기존의 로직들도 curry를 감싸준다면,
const f2 = (list, length) => go(
list,
list => filter(a=>a%2)(list),
list => map(a=>a*a)(list),
list => take(length)(list),
list => reduce(add)(list)
)
한번에 보내던것을 두번에 끊어서 보낼 수 있게 됩니다. list를 받아서, filter 를 그대로 list 로 전달한다는 말이 되므로, list 를 받는 부분과 전달하는 부분을 지워도 된다는 말이 됩니다.
const f2 = (list, length) =>
go(
list,
filter(a=>a%2),
map(a=>a*a),
take(length),
reduce(add)
);
이처럼 조금더 간략하게 됩니다.
지연평가
만들어진 위의 go 함수는 지연적으로 평가가 됩니다. filter 와 map 은 *(제너레이터)로 구현이 되어있습니다. 제너레이터는 return 이 아니라 yield를 하도록 되어있는데, yield 를 사용한다는 것은 지연적으로 평가하라는 말입니다.
var it = map(a => a+1, [1,2,3]);
it.next();
it.next 를 호출해야지만 처리가 되지 var it 으로는 아무런 실행이 되지 않습니다. 그렇기 때문에 처음 만들어진 시간복잡도가 동일하다는 것을 의미합니다.
const L = {};
filter 와 map 은 지연적으로 동작하니까, L 이라는 prefix를 달아줘서, 레이지한 함수라고 선언합닏나.
L.filter = curry(function *(f,list) { // 제네러이터 함수
for (const a of list) {
if(f(a)) yield a;
}
});
L.map = curry(function *(f,list) {
for (const a of list) {
yield f(a);
}
});
L.range = function *(stop) {
let i = -1;
while(++i < stop) yield i;
};
range 를 만들어서 range(Infinity) 로 무한대를 선언한 후에, 200개만 빼올 수 있도록 구현할 수 있습니다.
log(f2([L.ragne(Infinity)],3));
라고 선언하여도 3번의 결과값을 찾은 후 종료됩니다. 35분 부터는 실전 학습….