[JS] 자바스크립트 함수형 프로그래밍(인프런) 정리 (STEP 49)
함수형 프로그래밍, 함수형 프로그래밍으로의 전환
STEP 49
- 작성자: Wol-dan (@pul8219)
- 스터디 주제: FrontEnd 면접 스터디 https://gitlab.com/siots-study/topics/-/wikis/%EC%8B%AC%ED%99%941
- 공부 범위: STEP 49 자바스크립트로 알아보는 함수형 프로그래밍 - 인프런 강의
- 기한: 08/14(토) ~ 08/17(화)
- 📋 스터디 문서 목록 바로가기
자바스크립트로 알아보는 함수형 프로그래밍 - 인프런 강의 강의를 보고 정리한 내용입니다.
프로그래밍 패러다임
- 명령형 프로그래밍:
어떻게(How)
의 관점에서 프로그래밍 하는 것- 절차지향 프로그래밍: ex) C, C++
- 객체지향 프로그래밍: ex) C++, Java, C#
- 선언형 프로그래밍:
무엇을(What)
의 관점에서 프로그래밍 하는 것- 함수형 프로그래밍
함수형 프로그래밍
- 순수함수를 만드는 것
- 모듈화 수준을 높이는 것
- 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임
순수함수
- Side Effect가 없는 함수를 의미. 즉 함수의 실행이 외부에(외부의 상태 등) 영향을 끼치지 않는 함수를 말함
- 평가시점에 상관없이 동일한 인자가 들어왔을 때 동일한 결과를 제공할 수 있는 함수(함수의 실행 순서에 좌우되지 않는다는 장점이 있음)
- 리턴값 외에는 외부와 소통하는 부분이 없는 함수
일급 함수
자바스크립트에서는 함수를 값으로 다룰 수 있다. 아래와 같은 조건을 만족하기 때문에 자바스크립트에서 함수를 일급 함수
라고 하기도 한다.
- 함수를 변수에 담을 수 있다.
- 함수를 매개변수로 넘길 수 있다.
- 함수를 함수에서 반환할 수 있다.
add_maker 예제
function add_maker(a) {
return function (b) {
return a + b;
};
}
const add10 = add_maker(10);
console.log(add10(20)); // 30
const add5 = add_maker(5);
console.log(add5(10)); // 15
const add15 = add_maker(15);
console.log(add15(10)); // 25
클로저
: add_maker 호출이 끝난 시점에도 변수 a에 접근할 수 있으므로 add_maker 내부 리턴문에 있는 함수는클로저
이다.일급 함수
: 함수를 리턴하고 있으므로일급 함수
개념이 쓰였음을 알 수 있다.순수 함수
: add_maker 내부에서 리턴하는 함수는순수 함수
이다. a라는 변수를 참조할 뿐 변경하고 있지 않다. 항상 동일한 값을 가리키는 a라는 값에 b를 더하는 순수 함수인 것이다. 또한 add10 와 같이 함수를 만들고 나면 어느 시점에 호출을 하던 일관성 있는 결과를 낸다.
이처럼 함수를 값으로 다루고, 순수 함수를 정의하고, 함수의 평가시점이 언제이든 상관없게 프로그래밍하는 것을 함수형 프로그래밍
이라고 한다.
함수형 프로그래밍 방식으로 전환하기
예제를 통해 코드를 함수형 프로그래밍 방식으로 전환해보자.
const users = [
{ id: 1, name: "ID", age: 36 },
{ id: 2, name: "BJ", age: 32 },
{ id: 3, name: "JM", age: 32 },
{ id: 4, name: "PJ", age: 27 },
{ id: 5, name: "HA", age: 25 },
{ id: 6, name: "JE", age: 26 },
{ id: 7, name: "JI", age: 31 },
{ id: 8, name: "MP", age: 23 },
];
기존의 명령형 프로그래밍
/* 명령형 코드 */
// 1) 30세 이상인 users를 찾자.
const temp_users = [];
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 30) {
temp_users.push(users[i]);
}
}
console.log(temp_users);
// 2) 30세 이상인 users의 names를 수집한다.
const names = [];
for (let i = 0; i < temp_users.length; i++) {
names.push(temp_users[i].name);
}
console.log(names);
// 3) 30세 미만인 users를 찾자.
const temp_users2 = [];
for (let i = 0; i < users.length; i++) {
if (users[i].age < 30) {
temp_users2.push(users[i]);
}
}
console.log(temp_users);
// 4) 30세 미만인 users의 ages를 수집한다.
const ages = [];
for (let i = 0; i < temp_users2.length; i++) {
ages.push(temp_users2[i].age);
}
console.log(ages);
_filter
, _map
으로 리팩토링
명령형 코드에서 _filter
를 이용해 1), 3) 의 중복을 없애고, _map
을 이용해 2), 4)의 중복을 없애보자.
function _filter(list, predi) {
const new_list = [];
for (let i = 0; i < list.length; i++) {
if (predi(list[i])) {
new_list.push(list[i]);
}
}
return new_list;
}
function _map(list, mapper) {
const new_list = [];
for (let i = 0; i < list.length; i++) {
new_list.push(mapper(list[i]));
}
return new_list;
}
const over_30 = _filter(users, function (user) {
return user.age >= 30;
});
console.log(over_30);
const under_30 = _filter(users, function (user) {
return user.age < 30;
});
console.log(under_30);
const over_30_names = _map(over_30, function (user) {
return user.name;
});
console.log(over_30_names);
const under_30_ages = _map(under_30, function (user) {
return user.age;
});
console.log(under_30_ages);
// users가 아닌 다른 데이터도 _filter를 활용할 수 있다.
/*
console.log(_filter([1,2,3,4], function(num){ return num % 2; }));
console.log(_filter([1,2,3,4], function(num){ return !(num % 2); }));
*/
- 받아둔 predi 함수에
users[i]
를 적용하는 식으로 동작한다. (응용형 프로그래밍) _filter
,_map
모두 데이터가 어떤 형태인지는 관심이 없다. 재사용성이 높다.
응용형 프로그래밍(=함수)
- 함수가 함수를 받아서 원하는 시점에 해당 함수가 알고있는 인자를 적용하는 식으로 프로그래밍하는 것
// 함수형 프로그래밍에서는 위 코드와 같은 대입문을 줄이고 아래처럼 함수를 중첩하는 방식을 많이 쓴다.
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
function (user) {
return user.name;
}
)
);
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
function (user) {
return user.age;
}
)
);
_each
로 리팩토링
_each
함수를 만들어 _filter
, _map
함수의 중복을 없애보자. 배열의 길이만큼 for 루프를 도는 코드가 중복되어있다.
// 수정된 _filter, _map 함수. 그리고 추가된 _each 함수
function _filter(list, predi) {
const new_list = [];
_each(list, function (val) {
if (predi(val)) new_list.push(val);
});
return new_list;
}
function _map(list, mapper) {
const new_list = [];
_each(list, function (val) {
new_list.push(mapper(val));
});
return new_list;
}
function _each(list, iter) {
for (let i = 0; i < list.length; i++) {
iter(list[i]);
}
return list;
}
- for 루프를
_each
함수에 넣고_each
를_filter
,_map
에서 사용하도록 코드를 수정했다.
다형성
-
자바스크립트의
map()
,filter()
는 Array.prototype의 메서드이다.- 순수함수가 아니다.
- 객체의 상태에 따라 결과가 달라진다.
- 메서드는 객체지향 프로그래밍이다. (
array.map()...
객체로 시작한다. -> 함수로 시작하는 함수형 프로그래밍과의 차이) - 메서드는 해당 클래스의 인스턴스에서만 사용할 수 있다. 예를 들어
map()
은 배열(array)이 아니면 사용할 수 없다. array-like 객체에는map()
을 사용할 수 없다. - 우리가 만든
_map
,_filter
와는 분명한 차이가 있다. - 메서드들은 해당 클래스에 종속되어 다양한 형(type)을 지원하기 어렵다.(다형성을 지원하기 어렵다.)
-
배열 같지만 배열이 아닌 것들(Array-like)
document.querySelectorAll()
의 반환값은 배열이 아닌 NodeList
-
함수형 프로그래밍의 높은 다형성
- 우리가 만든
_map()
을 사용하면 Array-like 객체도 사용할 수 있다. length, key:value 형태만 만족하면 어떤 타입이든 가능하다. - 함수형 프로그래밍에서는 함수를 먼저 만들고 함수에 맞는 데이터를 구성해 함수에 적용하는 식으로 프로그래밍한다. 때문에 다형성을 지원하기 쉽다.
- 우리가 만든
외부 다형성
array_like, arguments, document.querySelectorAll와 같이 돌릴 수 있는 객체를 돌리는 것은 고차 함수인 _map
, _filter
, _each
가 어떻게 구현됐느냐에 따라 결정된다. (외부 다형성)
/* Array-like 객체에 map 메서드를 사용할 수 없다. */
console.log(
document.querySelectorAll("*").map(function (node) {
return node.nodeName;
})
); // 에러 출력 Uncaught TypeError: document.querySelectorAll(...).map is not a function
/* 우리가 만든 _map 함수에는 Array-like 객체도 사용할 수 있다. */
console.log(
_map(document.querySelectorAll("*"), function (node) {
return node.nodeName;
})
); // 정상적으로 동작한다.
내부 다형성
배열안에 어떤 값이 들어있더라도 수행할 수 있게 하는 역할은 보조 함수(predi
, iter
, mapper
)에 달려있다. (내부 다형성)
함수형 프로그래밍에서 전달되는 보조 함수는 모두 콜백함수로 보기보다 하는 역할에 따라 다음과 같이 이름을 분류해 불러줘야 한다.
- callback 함수: 어떤 일이 다 끝난 후 실행하는 함수
- predi(cate): 조건을 리턴하는 함수
- iter(ator): 루프를 돌면서 반복적으로 실행되는 함수
- mapper: 무언가와 무언가를 매핑하는 함수
커링 _curry
, _curryr
함수와 인자를 다루는 기법. 함수의 인자를 하나씩 적용해 나가다가, 필요한 인자가 모두 채워지면 함수본체를 실행한다.
자바스크립트는 커링을 지원하지 않지만 일급함수가 지원되고 평가시점을 마음대로 다룰 수 있기 때문에 커링을 구현할 수 있다.
function _curry(fn) {
return function (a) {
return function (b) {
return fn(a, b);
};
};
}
var add = _curry(function (a, b) {
return a + b;
});
var add10 = add(10);
console.log(add10(5)); // 15
console.log(add(5)(3)); // 8
- 미리 받아두었던 함수 본체(여기선
fn
)를 원하는 시점까지 미뤄뒀다가 모든 인자를 받으면 실행한다.
_curry
의 변형
function _curry(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(a, b);
};
};
}
_curryr
add
말고 sub
도 만들어보자.
var sub = _curry(function (a, b) {
return a - b;
});
console.log(sub(10, 5)); // 5
var sub10 = sub(10);
console.log(sub10(5)); // 5
위 코드는 10-5
을 계산하는데 sub10
이라는 이름 때문에 5-10
을 나타내는 표현처럼 보인다. 이를 수정하기 위해 오른쪽에서부터 인자를 적용해나가는 curryr
이라는 함수를 만들어보자.
function _curryr(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(b, a);
};
};
}
var sub = _curryr(function (a, b) {
return a - b;
});
console.log(sub(10, 5)); // 10 - 5 = 5
var sub10 = sub(10);
console.log(sub10(5)); // 5 - 10 = -5
sub에 인자를 2개를 줄 땐 a-b가 되지만, 인자를 하나씩 줄 땐 5에 sub10이라는 표현에 맞게 b-a가 되도록 curryr을 작성했다.
_get
함수
_get
함수는 object에 있는 값을 안전하게 참조하기 위해 사용된다.
객체에 특정 key에 해당하는 값이 없더라도 에러가 아닌 undefined를 출력하도록 작성할 수 있다.
function _get(obj, key) {
return obj == null ? undefined : obj[key];
}
var user1 = users[0];
console.log(user1.name);
console.log(_get(user1, "name"));
// console.log(users[10].name); // 에러
console.log(_get(users[10], "name")); // undefined
_get
에 _curryr
적용
function _curryr(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(b, a);
};
};
}
var user1 = users[0];
// _get에 _curryr 적용
var _get = _curryr(function (obj, key) {
return obj == null ? undefined : obj[key];
});
console.log(_get("name")(user1));
var get_name = _get("name");
console.log(get_name(user1));
console.log(get_name(users[3]));
console.log(get_name(users[4]));
_curryr
이 적용된 _get
을 사용하면, 위에서 봤던 _map
관련 코드를 더 간단하게 수정할 수 있다.
✨Before
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
function (user) {
return user.name;
}
)
);
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
function (user) {
return user.age;
}
)
);
✨After
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
_get("name") // ✏️
)
);
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
_get("age") // ✏️
)
);
지금까지 완성된 users 관련 전체 코드
const users = [
{ id: 1, name: "ID", age: 36 },
{ id: 2, name: "BJ", age: 32 },
{ id: 3, name: "JM", age: 32 },
{ id: 4, name: "PJ", age: 27 },
{ id: 5, name: "HA", age: 25 },
{ id: 6, name: "JE", age: 26 },
{ id: 7, name: "JI", age: 31 },
{ id: 8, name: "MP", age: 23 },
];
function _filter(list, predi) {
const new_list = [];
_each(list, function (val) {
if (predi(val)) new_list.push(val);
});
return new_list;
}
function _map(list, mapper) {
const new_list = [];
_each(list, function (val) {
new_list.push(mapper(val));
});
return new_list;
}
function _each(list, iter) {
for (let i = 0; i < list.length; i++) {
iter(list[i]);
}
return list;
}
function _curryr(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(b, a);
};
};
}
var _get = _curryr(function (obj, key) {
return obj == null ? undefined : obj[key];
});
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
_get("name") // ✏️
)
);
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
_get("age") // ✏️
)
);
_reduce
function _reduce(list, iter, memo){
...
}
_reduce([1,2,3,4], add, 0);
_reduce
에 위처럼 인자를 줬을 때 0(초기값)+1+2+3+4=10
이라는 결과를 얻고싶다고 해보자. 그럼 _reduce
를 어떻게 작성해야할까?
// _reduce를 이런식으로 동작하게 만들어야한다.
function _reduce(list, iter, memo) {
return iter(iter(iter(0, 1), 2), 3);
}
// 동작 방식을 자세히 보면 이렇다.
memo = add(0, 1);
memo = add(memo, 2);
memo = add(memo, 3);
return memo;
// 위 코드를 재귀적으로 표현하면 아래와 같다.
add(add(add(0, 1), 2), 3);
function _reduce(list, iter, memo){}
의 인자
list
: Array나 Array-like 객체iter
: 재귀적으로(연속적으로) 반복할 함수memo
: 시작값
결국 _reduce
는 list를 돌면서 두번째 인자로 받는 iter함수를 재귀적으로(연속적으로) 호출하면서 어떤 값으로 축약해나가기 위한 함수이다. _map
, _filter
함수들은 list를 받고 배열(Array)을 리턴했다면 _reduce
는 list를 이용해 값을 뽑아내거나 객체를 뽑아내기 위해 사용한다. _reduce
의 장점은 복잡하거나 어려운 로직을 간단히 구현할 수 있도록 해준다는 것이다.
위에서 만들어놓은 add()
, _each()
를 사용해서 _reduce
를 구현해보자.
function _each(list, iter) {
for (let i = 0; i < list.length; i++) {
iter(list[i]);
}
return list;
}
function _curry(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(a, b);
};
};
}
var add = _curry(function (a, b) {
return a + b;
});
// _reduce 관련 코드 ⭐
function _reduce(list, iter, memo) {
_each(list, function (val) {
memo = iter(memo, val);
});
return memo;
}
console.log(_reduce([1, 2, 3, 4], add, 0)); // 10
이제 _reduce
를 호출할 때 세번째 인자인 memo(초기값)
가 생략되어도 동작할 수 있도록 해보자.
우선 list의 첫 번째 원소가 초기값(memo)이 되도록 해야한다. 그리고 만약 배열의 길이가 3이라면 총 두 번을 돌면서 결과를 내도록 구현해야한다.
// 아까와 달리 이런식으로 구현하기
add(add(1, 2), 3);
// _reduce 함수에 if문 코드를 추가했다.
function _reduce(list, iter, memo) {
if (arguments.length == 2) {
memo = list[0];
list = list.slice(1);
}
...
}
그런데 이렇게 작성하게 되면 slice()
가 Array의 메서드이기 때문에, list
에 배열(Array)이 아닌 Array-like 객체가 오게될 때 _reduce
를 사용할 수 없게 된다.
_map
, _filter
를 만들었을 때 Array-like 객체도 처리할 수 있었던 것처럼 _reduce
도 Array-like 객체를 처리할 수 있도록 해결해보자.
우선 slice()
메서드를 배열이 아닌 객체에도 쓸 수 있는 방법을 알아보자
/* slice를 call로 호출해보자 */
// 배열이 아닌 Node list 객체(Array-like)를 변수 a에 담는다.
var a = document.querySelectorAll("*");
console.log(a); // [html, head, meta, title, script, ...]
a.slice(1); // a가 배열이 아니기 때문에 에러가 발생한다.
// ex 1
// slice 메서드를 변수에 따로 저장한다.
var slice = Array.prototype.slice;
// slice를 call로 호출하며 this에 a를 바인딩한다.
slice.call(a, 2); // [meta, title, script, ...]
// 이번엔 에러를 발생시키지 않고 slice된 배열을 리턴한다.
// slice.call(a,2)의 결과로 Array가 반환됨을 알 수 있다.
slice.call(a, 2).constructor; // function Array() {...}
// ex 2
// 배열은 아니지만 배열같은 '객체'도 다뤄보자.
var a = { 0: 1, 1: 20, 2: 30, length: 3 };
a[0]; // 1
a[1]; // 20
slice.call(a, 1); // [20, 30]
이렇게 slice()
에 call()
을 사용하면 _reduce
에서도 Array-like(argument, node list 객체 등)도 처리할 수 있다.
이제 이걸 우리가 작성하던 코드에 적용해보자. slice 작업은 _rest
라는 함수에서 하도록 작성했다.
function _each(){
...
}
function _curry(fn){
...
}
var add = _curry(...);
var slice = Array.prototype.slice;
// num: 자를 숫자 -> 생략되면 기본값 1을 갖도록 함
function _rest(list, num) {
return slice.call(list, num || 1);
}
function _reduce(list, iter, memo) {
if (arguments.length == 2) {
memo = list[0];
list = _rest(list);
}
_each(list, function (val) {
memo = iter(memo, val);
});
return memo;
}
console.log(_reduce([1, 2, 3, 4], add, 0)); // 10
// 이렇게 memo값을 생략한 경우에도 10이 정상적으로 출력되어야한다.
console.log(_reduce([1, 2, 3, 4], add)); // 10
_pipe
_pipe
함수들을 인자로 받아 그 함수들을 연속적으로 실행해주는 함수이고 이 함수를 리턴한다. 만들고자하는 _pipe
함수의 모습은 다음과 같다.
function _pipe(...){
...
}
var f1 = _pipe(
function(a) { return a + 1; }, // 1+1
function(a) { return a * 2; } // 2*2 (a에 윗 라인의 결과가 들어가서)
)
console.log( f1(1) );
_pipe
는 결국 reduce
다. _pipe
의 추상화된 버전이 reduce이다. _pipe
는 reduce가 특화된 버전이라 볼 수 있다.
_pipe
는 함수들 이라는 배열을 reduce로 축약한다.
function _pipe() {
// 인자로 받은 함수들을 변수에 저장
var fns = arguments;
// _pipe는 함수를 리턴해야한다.
// 나중에 실행될 함수를 리턴한다.
return function (arg) {
// 내부 리턴문에서는 _reduce를 사용한다.
// fns를 돌면서, 첫 번째 함수에 인자를 적용한 결과를 리턴해나가고 이를 반복하는 식으로 동작한다.
return _reduce(
fns,
function (arg, fn) {
return fn(arg);
},
arg
);
};
}
var f1 = _pipe(
function (a) {
return a + 1;
}, // 1+1
function (a) {
return a * 2;
} // 2*2 (a에 윗 라인의 결과가 들어가서)
);
console.log(f1(1)); // 4
_go
_go
함수는 pipe 함수인데 즉시 실행이 되는 함수이다.(_go
는 _pipe
의 즉시 실행 버전으로 보면됨) 첫 번째 인자로는 real 인자를 받고, 두 번째 인자부턴 함수를 받아 즉시실행하는 함수이다. 다음과 같이 동작하는 _go
를 구현해보자.
_go(
1,
function (a) {
return a + 1;
},
function (a) {
return a * 2;
},
console.log
);
// 1부터 시작해서 1+1 -> 2*2 -> console에 출력
아까 만들었던 _pipe
를 사용하면 _go
는 간단하게 구현할 수 있다.
function _go(arg) {
// 함수들을 변수에 저장해야한다. pipe와 달리 첫 번째 인자를 제외해야하므로 아까 만들었던 _rest를 사용한다.
var fns = _rest(arguments);
// 여러 함수들을 받기 위해 apply를 사용한다.
return _pipe.apply(null, fns)(arg);
}
_go(
1,
function (a) {
return a + 1;
},
function (a) {
return a * 2;
},
console.log
); // 4
users에 _go
적용하기
아래 코드는 우리가 초반에 작성했던 users 관련 코드이다. 코드의 흐름이 내부(_filter
)에서 바깥(_map
)으로 실행되어 읽기 어려운 면이 있다. 이를 더 함수형 프로그래밍답게 바꿔보자. _go
를 적용하면 된다.
✨ Before
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
_get("name")
)
);
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
_get("age")
)
);
✨ After
_go(
users,
function (users) {
return _filter(users, function (user) {
return user.age >= 30;
});
},
// 여기엔 위에서 걸러진 users들이 인자에 담길 것이다.
function (users) {
return _map(users, _get("name"));
},
console.log
); // ["ID", "BJ", "JM", "JI"]
_go(
users,
function (users) {
return _filter(users, function (user) {
return user.age < 30;
});
},
// 여기엔 위에서 걸러진 users들이 인자에 담길 것이다.
function (users) {
return _map(users, _get("age"));
},
console.log
); // [27, 25, 26, 23]
그런데 위 코드에 우리가 만들었던 _curryr
을 사용하면 _go
호출을 더 간단하게 만들 수 있다.
우선 _map
, _filter
, _each
에 _curryr
을 적용해보자.
// _map, _filter, _each에 _curryr 적용
var _map = _curryr(_map),
_filter = _curryr(_filter);
console.log(
_map([1, 2, 3], function (val) {
return val * 2;
})
); // [2,4,6]
// 위 코드를 curryr을 사용하면 함수가 먼저 오고 또 호출하면서 인자를 넘겨주도록 작성할 수 있다.
console.log(
_map(function (val) {
return val * 2;
})([1, 2, 3])
);
_go(
users,
_filter(function (user) {
return user.age >= 30;
}),
_map(_get("name")),
console.log
); // ["ID", "BJ", "JM", "JI"]
_go(
users,
_filter((user) => user.age < 30), // 화살표 함수 사용
_map(_get("age")),
console.log
); // [27, 25, 26, 23]
- curryr 을 사용하면 인자를 하나만 줬을 경우 인자가 오른쪽부터 적용된 또다른 함수를 리턴하게 된다.
_go
는 함수들을 받아서 연속적으로 실행하며 인자들을 그다음 함수로 전달하는 식으로 동작한다.- 함수가 함수를 실행하고 함수가 함수를 리턴하는 식으로 프로그래밍하는 것이 함수형 프로그래밍이다. 물론 그 함수들이 순수함수여야한다.
다형성 높이기, _keys
, 에러
1) _each
에 null 넣어도 에러 안나게
함수형 프로그래밍에서 예외적인 데이터가 들어와도 에러가 발생하지 않도록 해야한다.
// 아래 코드는 에러가 난다. _each 내부에서 list의 length를 참조하는데 list가 null이면 에러가 나기 때문이다.
_each(null, console.log);
// 에러가 나지 않게 바꿔보자
//만들어두었던 _get를 사용하기
var _get = _curryr(function (obj, key) {
return obj == null ? undefined : obj[key];
});
//_each 고치기
var _length = _get("length");
function _each(list, iter) {
for (let i = 0, len = _length(list); i < len; i++) {
iter(list[i]);
}
return list;
}
_each(null, console.log); // 이제 에러가 나지 않는다.
console.log(
_map(null, function (v) {
return v;
})
);
console.log(
_filter(null, function (v) {
return v;
})
);
// _map과 _filter도 이렇게 수정된 _each를 사용하기 때문에 null을 전달하면 에러가 나지 않을 것이다.
_go(
[1, 2, 3, 4],
_filter(function (v) {
return v % 2;
}),
_map(function (v) {
return v * v;
}),
console.log
); // [1,9] // why❓
_go(
null,
_filter(function (v) {
return v % 2;
}),
_map(function (v) {
return v * v;
}),
console.log
); // [] // null을 주더라도 에러 발생 x
2) _keys
만들기
3) _keys
에서도 _is_object
인지 검사하여 null 에러 안나게
Object.keys
를 좀 더 안전하게 쓸 수 있도록 _keys
함수를 만들어보자.
// Object.keys 예시
console.log(Object.keys({ name: "ID", age: 33 })); // "name", "age"
console.log(Object.keys([1, 2, 3, 4])); // ["0", "1", "2", "3"]
console.log(Object.keys(10)); // []
// console.log(Object.keys(null)); // 에러❗ -> 이걸 해결해보자.
function _is_object(obj) {
return typeof obj === "object" && !!obj;
}
// null에 typeof 연산자를 쓰면 "object"가 반환된다. 따라서 뒤에 !!obj 라는 조건을 붙인다. !!obj의 obj에 null 이 들어갈 경우 !null(true) -> !true(false)가 되어 false가 된다.
function _keys(obj) {
// object가 아니면 빈 배열을 반환하도록
return _is_object(obj) ? Object.keys(obj) : [];
}
console.log(_keys({ name: "ID", age: 33 })); // "name", "age"
console.log(_keys([1, 2, 3, 4])); // ["0", "1", "2", "3"]
console.log(_keys(10)); // []
console.log(_keys(null)); // [] // 아까와 달리 에러가 발생하지 않는다.
4) _each
외부 다형성 높이기
_is_object
, _keys
를 이용하면 _each
를 간단하게 고칠 수 있다.
_each
의 첫 번째 인자에 배열(Array)이 아닌 데이터(key:value 쌍으로 이루어진 객체 등)를 넣어 돌리고 싶을 때를 해결하기 위함이다.
function _is_object(obj) {
return typeof obj === "object" && !!obj;
}
function _keys(obj) {
// object가 아니면 빈 배열을 반환하도록
return _is_object(obj) ? Object.keys(obj) : [];
}
var _length = _get("length");
function _each(list, iter) {
var keys = _keys(list);
for (let i = 0, len = keys.length; i < len; i++) {
// keys는 null이 들어가도 빈 배열을 뱉도록 되어있기 때문에 에러가 나지 않고 코드가 잘 흘러간다.
iter(list[keys[i]]);
}
return list;
}
_each(
{
13: "ID",
19: "HD",
29: "YD",
},
function (name) {
console.log(name);
}
);
// 인자로 전달된 객체는 length도 없고 key가 0,1,2도 아니라 _each가 수정되기 전이라면 제대로 동작하지 않았을 것이다. 하지만 _each에 _keys를 적용함으로써 제대로 동작하게 되었다.
// _map이나 _filter도 _each를 사용하므로 이러한 객체가 전달되어도 제대로 동작할 것이다.
console.log(
_map(
{
13: "ID",
19: "HD",
29: "YD",
},
function (name) {
return name.toLowerCase();
}
)
); // ["id", "hd", "yd"]
_go(
{
13: "ID",
19: "HD",
29: "YD",
},
_map(function (name) {
return name.toLowerCase();
}),
console.log
); // ["id", "hd", "yd"]
_go(
users,
_map(function (user) {
return user.name;
}),
_map(function (name) {
return name.toLowerCase();
}),
console.log
); //["id", "bj", "jm", "pj", "ha", "je", "ji", "mp"]
_go(
null,
_map(function (user) {
return user.name;
}),
_map(function (name) {
return name.toLowerCase();
}),
console.log
); // [] // null을 넣어도 빈 배열이 반환될 뿐 프로그램은 자연스럽게 흘러간다.(함수의 연속실행에 큰 문제를 주지 않아 좋음 -> 다형성 극대화)
_go(
{
1: users[0],
3: users[2],
5: users[4],
},
_map(function (user) {
return user.name.toLowerCase();
}),
console.log
); // 어떤 데이터가 오는지에 맞춰 보조함수를 개발자가 수정할 수 있다. (다형성, 유연성 극대화)
STEP 49 전체 코드(다형성 높이기 부분에서 2)
부터는 포함 안되어있음)
const users = [
{ id: 1, name: "ID", age: 36 },
{ id: 2, name: "BJ", age: 32 },
{ id: 3, name: "JM", age: 32 },
{ id: 4, name: "PJ", age: 27 },
{ id: 5, name: "HA", age: 25 },
{ id: 6, name: "JE", age: 26 },
{ id: 7, name: "JI", age: 31 },
{ id: 8, name: "MP", age: 23 },
];
function _filter(list, predi) {
const new_list = [];
_each(list, function (val) {
if (predi(val)) {
new_list.push(val);
}
});
return new_list;
}
function _map(list, mapper) {
const new_list = [];
_each(list, function (val) {
new_list.push(mapper(val));
});
return new_list;
}
// 다형성 높이기 부분을 위해 잠시 주석
// function _each(list, iter) {
// for (let i = 0; i < list.length; i++) {
// iter(list[i]);
// }
// return list;
// }
// _get
var _get = _curryr(function (obj, key) {
return obj == null ? undefined : obj[key];
});
var _length = _get("length");
function _each(list, iter) {
for (let i = 0, len = _length(list); i < len; i++) {
iter(list[i]);
}
return list;
}
// 1) 30세 이상인 users를 찾자.
// 2) 30세 이상인 users의 names를 수집한다.
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
function (user) {
return user.name;
}
)
);
// 3) 30세 미만인 users를 찾자.
// 4) 30세 미만인 users의 ages를 수집한다.
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
function (user) {
return user.age;
}
)
);
/* 커링 */
// curry
function _curry(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(a, b);
};
};
}
var add = _curry(function (a, b) {
return a + b;
});
var add10 = add(10);
console.log(add10(5));
// curryr
function _curryr(fn) {
return function (a, b) {
return arguments.length === 2
? fn(a, b)
: function (b) {
return fn(b, a);
};
};
}
var sub = _curryr(function (a, b) {
return a - b;
});
console.log(sub(10, 5)); // 5
var sub10 = sub(10);
console.log(sub10(5)); // -5
// 다형성 높이기 부분을 위해 잠시 주석(위에 _get, _each 때문에)
// // _get
// var _get = _curryr(function (obj, key) {
// return obj == null ? undefined : obj[key];
// });
var user1 = users[0];
console.log(_get(user1, "name"));
var get_name = _get("name");
console.log(get_name(user1));
// 1) 30세 이상인 users를 찾자.
// 2) 30세 이상인 users의 names를 수집한다.
console.log(
_map(
_filter(users, function (user) {
return user.age >= 30;
}),
_get("name")
)
);
// 3) 30세 미만인 users를 찾자.
// 4) 30세 미만인 users의 ages를 수집한다.
console.log(
_map(
_filter(users, function (user) {
return user.age < 30;
}),
_get("age")
)
);
/* _reduce */
// add, _each 적용
// function _reduce(list, iter, memo) {
// _each(list, function (val) {
// memo = iter(memo, val);
// });
// return memo;
// }
// console.log(_reduce([1, 2, 3, 4], add, 0)); //10
// memo값 생략되어도 동작하도록
// array-like 값에도 _reduce를 사용할 수 있도록
var slice = Array.prototype.slice;
function _rest(list, num) {
return slice.call(list, num || 1);
}
function _reduce(list, iter, memo) {
if (arguments.length === 2) {
memo = list[0];
list = _rest(list);
}
_each(list, function (val) {
memo = iter(memo, val);
});
return memo;
}
console.log(_reduce([1, 2, 3, 4], add)); // 10
console.log(_reduce([1, 2, 3], add)); // 6
/* pipe */
function _pipe() {
var fns = arguments;
return function (arg) {
return _reduce(
fns,
function (arg, fn) {
return fn(arg);
},
arg
);
};
}
var f1 = _pipe(
function (a) {
return a + 1;
},
function (a) {
return a * a;
}
);
console.log(f1(1)); //4
/* go */
function _go(arg) {
var fns = _rest(arguments);
return _pipe.apply(null, fns)(arg);
}
_go(
1,
function (a) {
return a + 1;
},
function (a) {
return a * a;
},
console.log
); //4
// users에 _go 적용
_go(
users,
function (users) {
return _filter(users, function (user) {
return user.age >= 30;
});
},
function (users) {
return _map(users, _get("name"));
},
console.log
); // ["ID", "BJ", "JM", "JI"]
_go(
users,
function (users) {
return _filter(users, function (user) {
return user.age < 30;
});
},
function (users) {
return _map(users, _get("age"));
},
console.log
); // [27, 25, 26, 23]
// _map, _filter, _each에 _curryr 적용 -> users에 _go를 적용한 걸 더 간단하게 만들기
var _map = _curryr(_map),
_filter = _curryr(_filter);
console.log(
_map(function (val) {
return val * 2;
})([1, 2, 3])
); // [2,4,6]
_go(
users,
_filter(function (user) {
return user.age >= 30;
}),
_map(_get("name")),
console.log
); // ["ID", "BJ", "JM", "JI"]
_go(
users,
_filter(function (user) {
return user.age < 30;
}),
_map(_get("age")),
console.log
); // [27, 25, 26, 23]
/* 다형성 높이기, _keys, 에러 */
// _each의 list에 null 넣어도 에러나지 않게 -> _get 적용
_each(null, console.log); // 에러x
console.log(
_map(null, function (v) {
return v;
})
); // 에러x
console.log(
_filter(null, function (v) {
return v;
})
); // 에러x
_go(
[1, 2, 3, 4],
_filter(function (v) {
return v % 2;
}),
_map(function (v) {
return v * v;
}),
console.log
); // [1,9] // why❓
_go(
null,
_filter(function (v) {
return v % 2;
}),
_map(function (v) {
return v * v;
}),
console.log
); // [] // null을 주더라도 에러 발생 x
Comment
팀원들 결과물
-
@eyabc