과제가 내일 제출인데 아직도 8일차 강의를 못 들었다. 당연함 어제 그렇게 메소드랑 이것저것 공부하고 나서 7일차를 다시 들으니까 와 함수로 이걸 이렇게 구현할 수 있다고!?!!!!
새롭고 너무 재밌어서 그래서 또 과몰입하고 그것만 보다가 하루가 지나버렸다. 하지만 내일..이 있으니까 내일은 무려 베드테이블도 집에 온다고요. 이제 침대에서 하루종일 코드 치는 거야. 좋았어, 가보자고. 내일은 중국어 수업도 없으니! 아침에 일찍 일어나서 8일차 강의를 듣고 문제 세개를 호다닥 풀고 (미쳤습니까 휴먼?) 어떻게든 되겠지의 마인드로 하고 싶은데 그게 안 되죠? 결론은 일단 8일차 강의 조금이라도 듣고 자보려고 한다. 지연성이 뭔가요, 먹는 건가요? 이제 알아봐야지.. 오늘 인동님과 선협님의 대화를 들으니 나도 꼭 짱이 돼야지!!의 마음만 더 굳세졌다.
하지만 아무래도 싸움은 조금 곤란해요,, 맨날 싸우면 힘드니까 그냥 짱만 되겠어!
일단은 실무에서는 map
, reduce
, curry
만 잘 써도 잘 하는 거라고 하시는데?! 일단 그 세 개도 개념적으로는 이해는 되지만 내가 직접 코드를 짤 때에는 이걸 어떻게 써야 하는지 잘 감이 안 온다. 진짜로 명령형으로 이미 짜놓은 코드는 생각도 그 로직으로만 돌아가서 선언형으로 잘 사고가 안 된다고 해야 되나. 정말, 정말 많이 써봐야 늘 거 같고 애초에 생각을 그쪽으로 많이 할 수 있도록 노력을 해야 될 것 같다. 대신 진짜 코드의 가독성과 간결성은 최고라는 걸 느꼈다. 사실 반복문을 중첩으로 주루룩 작성해놓고 거기에 안에 조건문까지 가득 있으면 이게 한참이나 뭔 소리인가 생각하게 되는데 이미 함수를 변수로 선언해놨기 때문에 특별히 어떤 설명이 보태지지 않아도 이해가 쉽고 이후에 코드 재사용성도 높아져서 정말 효율적인 것 같다.
이 map
함수가 받는 아규먼트는 이터러블 프로토콜을 따른다.
const products = [
{name : '반팔티', price : 15000},
{name : '긴팔티', price : 20000},
{name : '핸드폰케이스', price : 15000},
{name : '후드티', price : 30000},
{name : '바지', price : 25000},
];
// 명령형
let names = [];
for (const p of products) {
names.push(p.name);
}
console.log(names);
// 함수형
const map = (f, iter) => { // f, 보조함수와 iter, 이터러블 받아오기
let res = [];
for (const a of iter) {
res.push(f(a));
// 어떤 값을 받아올 것인지 이 함수에게 완전히 위임을 한다고 보면 된다.
// 왜냐면 가지고 올 프로퍼티 이름이 뭘지 전혀 예상할 수 없으니까
// 뭐가 들어와도 돼야 하니까 그 부분에 대해서는 나중에 다른 함수를 통해서 선언해줌
}
return res;
// 함수형에서는 직접 출력하는 것보다 아규먼트와 리턴값으로 소통하는 것을 권장함
};
map(p => p.name, products);
console.log(map(p => p.name, products));
// names값들이 딱 출력됨 왕 멋지다!!!! (제가 쓴 필기지만 귀여워서 넣어봅니다)
map()
메소드를 사용하면 document.querySelector
는 배열을 반환해주지 않기 때문에 HTML 문서에 대해서는 정상적인 동작이 불가능하다. 그러나 여기서 생성한 map
함수의 경우에는 이터러블 프로토콜을 사용하는 for.. of
문을 통해 작성된 코드이기 때문에 HTML 문서에 대해서도 정상적으로 작동하며 제너레이터를 통해 생성된 값에 대해서도 정상적으로 작동하기 때문에 사실상 모든 것에 대해 이 함수를 적용할 수 있다고 말할 수 있다.
console.log(map(el => el.nodeName, document.querySelectorAll('*')));
// ["HTML", "HEAD", "META", "TITLE", "META", "META", "META", "META", "SCRIPT",
"LINK", "STYLE", "SCRIPT", "SCRIPT", "SCRIPT", "BODY", "SCRIPT"]
const it = document.querySelectorAll('*')[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
function *gen() {
yield 2;
yield 3;
yield 4;
}
console.log(map( a => a + a, gen())); //[4, 6, 8]
// 명령형
let under20000 = [];
for (const p of products) {
if (p.price <= 20000) {
under20000.push(p.price);
}
}
// 함수형
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};
const under20000 = filter(a => a.price <= 20000, products);
console.log(...under20000);
// 위 코드 두 줄을 줄여 바로 이렇게도 쓸 수 있다.
// console.log(...filter(a => a.price < 20000, products));
// 별도의 코드를 작성하지 않고도 새로운 조건만 입력해주면 바로 새로운 값을 반환받을 수 있다.
const over20000 = filter(a => a.price >= 20000, products);
console.log(...over20000);
// 홀수 뽑기!
console.log(filter(a => a % 2, [1, 2, 3, 4, 5])); // 1, 3, 5
// a % 2 == 1 이건 애초에 1이 truthy 값이기 때문에 필요 없다. 그냥 a % 2 이렇게 써도 됨!
const odds = filter(n => n % 2 == 0, function *() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
} ());
// 여기서 소괄호를 사용한 이유는 즉시 이 제너레이터 함수를 실행하여
// 값을 생성하기 위해서라고 생각하는데 IIFE (즉시 실행함수)와는 형태가 다르다.
// [Symbol.iterator]();와 유사한 형태를 띄고 있는 거 같은데
// 소괄호가 정확히 무슨 역할을 하고 무슨 의미인지 궁금하다.
console.log(...odds);
const nums = [1, 2, 3, 4, 5];
// 명령형
let total = 0;
for (const n of nums) {
total += n;
}
console.log(total);
// 함수형
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
const add = (a, b) => a + b;
console.log(reduce(add, 0, nums));
사실 이 reduce
함수는 고민을 많이 했다. 코드 상단에 보면 iter
파라미터가 없을 때의 조건문을 실행하는데 강의 중에 설명에는 iter
가 없는 상황이 아니라 이게 첫번째 실행이라 기존에 누적된 누산기 acc
값이 없어서 그 값을 할당해줘야 하는 문제였는데.
왜 여기서 iter가 없다고 하는지 이해가 안 됨. 초기값이 없을 때 배열의 맨 첫번째 값을 누산기로 빼서 계산을 해주는 건데 왜 iter가 없다고 하는 거지?!! 아!!!!!!!!!!! 파라미터 세개 받을 수 있는데 아규먼트가 두개만 들어오니까 iter가 없는 게 되고 acc에 배열이 할당되어서 그 배열의 첫번째 값만 꺼내서 acc에 할당해주고 그건 iterator로 펼쳐서 iter에 할당해주는 거라는 거죠?!
이렇게 혼자 별안간 깨달았다고 합니다.
위 세 함수가 중첩돼서 사용되면 가독성이 조금 떨어지고 해석에 어려움이 생긴다(이미 복잡하고 어려운데!). 그래서 그럴 때 go
와 pipe
함수를 사용해줄 수 있다.
const go = (...args) => reduce((a, f) => f(a), args);
결국엔 go
함수도 reduce
함수를 통해 함수의 동작들을 축약해주는 형태라고 볼 수 있는 듯하다.
pipe
함수는 함수를 리턴하는 함수다. go
함수와 함께 사용되는데 go
함수와의 차이점은 go
같은 경우에는 즉시 내부에 들어있는 함수들을 실행해서 값을 반환하지만 pipe
는 내부에 들어있는 함수들을 합쳐서 새로운 함수로 반환한다고 보면 된다. 영어 단어 의미 그대로 go
는 진짜 앞으로 그냥 가는 거고 pipe
는 통로를 통과하는 함수 같은 거라고 보면 될 듯 한데 근데, 저 사실 아직 얘 잘 모르겠어요. 더 공부해봐야 될 듯합니다.
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
curry
는 함수를 값으로 다루면서 기존에 받아둔 함수를 내가 원하는 시점에 실행하는 함수라고 한다. 만약 파라미터와 아규먼트의 갯수가 일치하지 않을 때 아규먼트의 갯수가 파라미터의 수를 충족할 때까지 기다렸다가 해당 조건이 만족되는 순간 실행되는 함수이다. 그 전에는 그냥 함수를 반환한다. 그래서 curry
함수는 위에 앞서 언급했던 다른 함수들에 붙여서 사용할 수 있다.
const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
아규먼트의 갯수가 파라미터보다 적은 경우에 함수를 반환하는 예시
const mul = curry((a, b) => a * b);
console.log(mul(2)); // (..._) => f(a, ..._)
그래서 이렇게 사용할 수 있기도 하다.
const mul3 = mul(3); // 그냥 변수에 값이 할당 될 것 같지만 함수가 할당된다.
console.log(mul3(2)); // 6
커리는 왜 이름이 커리가 됐을까? 스테판 커리와 인디안 커리가 생각이 나고 배고프다.
아무튼 궁금해서 찾아봤는데,
커링(currying)
이란 여러 개의 파라미터를 가지고 있는 함수를 하나의 파라미터를 갖는 함수들의 함수열로 전환하는 것을 말하는데 이 개념이 도입된 이후 해스켈 커리(Haskell Curry)에 의해 발전되어 curry
라는 이름이 붙었다고 한다.인디안 커리와 스테판 커리는 관계가 없다고 합니다^_^