함수형 프로그래밍 - 시작2

이전 포스트에서 함수형 프로그래밍의 간략한 소개와 몇가지 예제 코드를 구현했었다. 오늘은 이전 포스트에 이어 다음 내용을 진행하려고 한다.

회원 목록 중 한명 찾기

회원 목록 중 id 값으로 해당 id 값을 가진 회원 한 명을 찾고자 한다.

1
2
3
4
console.log(
filter(users, function (user) { return user.id === 3 })[0]
)
// {id: 3, name: "BJ", age: 32}

filter를 통해 걸러낸 후 [0]으로 user를 얻어냈고 원하는 결과를 얻어냈기는 했다. 위 처럼 filter를 사용하여 찾을 수 있지만 filter 함수는 무조건 list.length 만큼 predicate가 실행되기 때문에 효율적이지 못하고, 동일 조건에 값이 두 개 이상이라면 두 개 이상의 값을 찾는다.

1
2
3
4
5
6
7
8
9
var user;
for (var i = 0, len = users.length; i < len; i++) {
if (users[i].id === 3) {
user = users[i];
break;
}
}

console.log(user);

원하는 user를 찾은 후 break로 for문을 빠져나왔다. 앞선 filter를 통해 찾은 것보다 훨씬 효율적이다.
하지만 위 코드는 재사용이 불가능 하므로 위 코드를 함수로 만들어서 재사용 가능하도록 만들어 보려고한다.

1
2
3
4
5
6
7
8
9
10
function findById(list, id) {
for (var i = 0, len = list.length; i < len; i++) {
if (list[i].id === id) {
return list[i];
}
}
}

console.log(findById(users, 3));
console.log(findById(users, 5));

findById는 list와 id를 받아 루프를 돌다가 id가 동일한 객체를 만나면 그 값을 리턴한다.
만약 동일한 객체를 찾지 못한다면 기본 리턴 값인 undefined 가 리턴된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* findByName
*/
function findByName(list, name) {
for (var i = 0, len = list.length; i < len; i++) {
if (list[i].name === name) {
return list[i];
}
}
}

console.log(findByName(users, 'BJ'));
console.log(findByName(users, 'JE'));

/**
* findByAge
*/
function findByAge(list, age) {
for (var i = 0, len = list.length; i < len; i++) {
if (list[i].age === age) {
return list[i];
}
}
}

console.log(findByAge(users, 28));
console.log(findByAge(users, 25));

findById 와 동일하게 이름과 나이로도 찾을 수 있는 함수를 만들었다. 하지만 위의 세 함수 사이에 중복이 있다는 점이 아쉽다.

1
2
3
4
5
6
7
8
9
10
11
/**
* findBy
*/
function findBy(key, list, val) {
for (var i = 0, len = list.length; i < len; i++) {
if (list[i][key] === val) return list[i];
}
}
console.log(findBy('name', users, 'BJ'));
console.log(findBy('id', users, 2));
console.log(findBy('age', users, 28));

위와 같이 함수에 key 라는 인자를 하나 추가함으로써 세 함수를 공통으로 사용할 수 있게됐다. 위 함수는 key로 value를 얻을 수 있는 객체들을 가진 배열이라면 무엇이든 받을 수 있다. 객체의 key 값이 무엇이든지 간에 찾아줄 수 있으므로 훨씬 많은 경우를 대응할 수 있는 함수가 되었다.
하지만 위 함수에서도 아직 아쉬운 점이 존재한다.

  • key가 아닌 메서드를 통해 값을 얻어야 할 때
  • 두 가지 이상의 조건이 필요할 때
  • ===이 아닌 다른 조건으로 찾고자 할 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function User(id, name, age) {
this.getId = function () {
return id;
}
this.getName = function () {
return name;
}
this.getAge = function () {
return age;
}
}

var users2 = [
new User(1, "ID", 32),
new User(2, "HA", 25),
new User(3, "BJ", 32),
new User(4, "PJ", 28),
new User(5, "JE", 27),
new User(6, "JM", 32),
new User(7, "HI", 24),
]

console.log(findBy('age', users2, 25));
// undefined

user의 나이를 .getAge() 로 얻어내야 하기 때문에 findBy 함수로는 위 상황을 대응할 수 없을을 알 수 있다. 이름에 ‘P’ 가 포함된 user를 찾고 싶다거나 아니가 32이면서 이름이 ‘JM’인 user를 찾고 싶다거나 하는 것도 불가능하다.

값에서 함수로

앞서 만들었던 filter나 map처럼, 인자로 키와 값 대신 함수를 사용해 보려고 한다. 그렇게 하면 모든 상황에 대응 가능한 find 함수를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function find(list, predicate) {
for (var i = 0, len = list.length; i < len; i++) {
if (predicate(list[i])) return list[i];
}
}

console.log(
find(users2, function (u) { return u.getAge() === 25 }).getName()
);
console.log(
find(users, function (u) { return u.name.indexOf('P') !== -1 })
);
console.log(
find(users, function (u) { return u.age === 32 && u.name === 'JM' })
);
console.log(
find(users2, function (u) { return u.getAge() < 30 }).getName()
);

find의 인자로 key와 val 대신 predicate 함수 하나를 받았다. 값 대신 함수를 받았다. 덕분에 if 안쪽에서 할 수 있는 일이 정말 많아졌다. 메서드를 사용하거나 두가지 이상의 조건을 사용하는 것도 잘 동작한다.
find는 이제 배열에 어떤 값이 들어 있든 사용할 수 있게 되었다. 함수형 자바스크립트는 이처럼 다형성이 높은 기법을 많이 사용하며 이러한 기법은 정말 실용적이다

filter, map, find 함수들은 들어온 데이터가 무엇이든지 루프들 돌리거나 분기를 만들거나 push를 하거나 predicate를 실행하거나 등의 자기 할 일을 한다. find는 전달 받을 데이터와 데이터의 특성에 맞는 보조 함수(predicate)도 함께 전달받는다. 들어온 데이터의 특성은 보조 함수가 대응해 주기 때문에 find 함수는 데이터의 특성에서 완전히 분리될 수 있다. 이러한 방식은 다형성을 높이며 동시에 안정성도 높인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 다형성
*/
console.log(
map(
filter(users, function (u) { return u.age >= 30 }),
function (u) { return u.name }
)
);

console.log(
map(
filter(users2, function (u) { return u.getAge() > 30 }), // 메서드 실행으로 변경
function (u) { return u.getName() } // 메서드 실행으로 변경
)
);

함수를 만드는 함수와 find, filter 조합하기

User등의 커스텀 객체가 아닌 자바스크립트 기본 객체로 만들어진 users를 사용한 예제로 다시 돌아가 보자. 함수로 함수를 만들어 find 함수와 함께 사용하면 코드를 더욱 간결하게 만들 수 있다.

1
2
3
4
5
6
7
8
9
function bmatch1(key, val) {
return function (obj) {
return obj[key] === val;
}
}

console.log(find(users, bmatch1('id', 1)));
console.log(find(users, bmatch1('name', 'BJ')));
console.log(find(users, bmatch1('age', 28)));

bmatch1의 실행 결과는 함수다. key와 val을 미리 받아서 나중에 들어올 obj와 비교하는 익명 함수 클로저로 만들어 리턴한다. bmatch1을 통해 id, name, age를 비교하는 predicate 3개를 만들어 find에 넘겼다.
bmatch1은 함수를 리턴하기 때문에 filter나 map과도 조합이 가능하다.

1
2
console.log(filter(users, bmatch1('age', 32)));
console.log(map(users, bmatch1('age', 32)));

bmatch1은 하나의 key에 대한 value만 비교할 수 있다. 여러 개의 key에 해당하는 value들을 비교하는 함수를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function object(key, val) {
var obj = {};
obj[key] = val;
return obj;
}

function match(obj, obj2) {
for (var key in obj2) {
if (obj[key] !== obj2[key]) return false;
}
return true;
}

function bmatch(obj2, val) {
if (arguments.length == 2) obj2 = object(obj2, val);
return function (obj) {
return match(obj, obj2);
}
}

console.log(
match(find(users, bmatch('id', 3)), find(users, bmatch('name', 'BJ')))
);
console.log(
find(users, function (u) { return u.age === 32 && u.name === 'JM' })
);
console.log(
find(users, bmatch({ name: 'JM', age: 32 }))
);

이제는 (key, val)와 ({key: val}) 두 가지 방식으로 사용할 수 있다. ({key: val}) 방식을 사용하면 두 가지 이상의 값이 모두 동일한지도 확인할 수 있다. bmatch1을 bmatch로 발전시키면서 유용한 함수인 match와 object도 만들어졌다. 이처럼 작은 기능을 하는 함수로 쪼개거나 재조합하는 식으로 코드를 발전시키는것도 좋은 방법이다.

find를 조금만 고치면 값 비교만 하는 Array.prototype.indexOf보다 활용도가 훨씬 높은 findIndex를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* findIndex
*/
function findIndex(list, predicate) {
for (var i = 0, len = list.length; i < len; i++) {
if (predicate(list[i])) return i;
}

return -1;
}

console.log(findIndex(users, bmatch({ name: 'JM', age: 32 }))) // 5
console.log(findIndex(users, bmatch({ age: 36 }))) // -1
console.log(findIndex(users, bmatch('id', 2))); // 1

고차함수

앞서 구현했던 filter, map, find, findIndex, bvalue, bmatch 같은 함수들은 모두 고차 함수다.
고차 함수란, 함수를 인자로 받거나 함수를 리턴하는 함수를 말한다. 당연히 둘 다 하는 경우도 고차 함수다. 보통 고차 함수는 함수를 인자로 받아 필요한 때에 실행하거나 클로저를 만들어 리턴한다.

Underscore.js는 유명한 함수형 자바스크립트 라이브러리다. Underscore.js 의 _.map, _.filter, _.find, _.findIndex는 iteratee와 predicate가 사용할 인자를 몇 가지 더 제공한다. 재료가 많으면 더 다양한 로직을 만들 수 있다. 앞서 구현했던 고차 함수들을 Underscore.js에 가깝게 고쳐 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
_.map = function (list, iteratee) {
var new_list = [];
for (var i = 0, len = list.length; i < len; i++) {
new_list.push(iteratee(list[i], i, list))
}
return new_list;
}

_.filter = function (list, predicate) {
var new_list = [];
for (var i = 0, len = list.length; i < len; i++) {
if (predicate(list[i], i, list)) new_list.push(list[i])
}
return new_list;
}

_.find = function (list, predicate) {
for (var i = 0, len = list.length; i < len; i++) {
if (predicate(list[i], i, list)) return list[i]
}
}

_.findIndex = function (list, predicate) {
for (var i = 0, len = list.length; i < len; i++) {
if (predicate(list[i], i, list)) return i
}
return -1;
}

원래는 iteratee(list[i])처럼 한 개의 인자를 넘겼지만, 이제는 iteratee(list[i], i, list) 처럼 두 개의 인자를 추가했다. 이제 iteratee와 predicate 함수가 받는 인자가 많아져 좀 더 다양한 일을 할 수 있게 되었다. predicate도 iteratee와 동일하다.

1
2
3
4
5
6
7
8
9
10
console.log(
_.filter([1, 2, 3, 4], function (val, idx) {
return idx > 1;
})
) // [3, 4]
console.log(
_.filter([1, 2, 3, 4], function (val, idx) {
return idx % 2 === 0;
})
) // [1,3]

function identity

정말 쓸모 없어 보이는 이상한 함수 하나를 소개한다. 이것은 Underscore.js에 있는 함수이기도 하다.

1
2
3
_.identity = function (v) { return v };
var a = 10;
console.log(_.identity(10)) // 10

받은 인자를 그냥 그대로 뱉는 함수다. _.identity 같은 아무런 기능이 없는 함수는 대체 언제 사용해야 하는 걸까?

1
2
console.log(_.filter([true, 0, 10, 'a', false, null], _.identity));
// [true, 10, 'a']

_.filter를 _.identity와 함께 사용했더니 Truthy Values만 남았다. 이렇게 놓고 보니 _.identity가 생각보다 실용적으로 보인다. _.identity를 다른 고차 함수와 조합하는 식으로 아래와 같은 유용한 함수들을 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
_.some = function (list) {
return !!_.find(list, _.identity);
}

_.every = function (list) {
return _.filter(list, _.identity).length === list.length;
}

console.log(_.some([0, null, 2])); // true
console.log(_.some([0, null, false])); // false

console.log(_.every([0, null, true])); // false
console.log(_.every([{}, true, 2])); // true

_.some은 배열에 들어 있는 값 중 하나라도 긍정적인 값이 있으면 true, 하나도 없다면 false를 리턴한다.
_.every는 모두 긍정적인 값이어야 true를 리턴한다. _.some, _.every는 if나 predicate 등과 함께 사용할 때 매우 유용하다.
_.every는 filter를 사용했기 때문에 항상 루프를 끝까지 돌게 된다. 정말 쓸모 없어 보이지만 함수 두 개를 더 만들면 로직을 개선할 수 있다.

연산자 대신 함수로

1
2
3
4
5
6
function not(v) { return !v; }
function beq(a) {
return function (b) {
return a === b;
}
}

!를 써도 되는데 not이 왜 필요할까? ===로 비교하면 되는데 beq는 왜 필요할까? 굳이 not과 beq를 함수로 만들 필요가 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
_.some = function (list) {
return !!_.find(list, _.identity);
}

_.every = function (list) {
return beq(-1)(_.findIndex(list, not));
}

console.log(_.some([0, null, 2, 0])); // true
console.log(_.some([0, null, false])); // false

console.log(_.every([0, null, true])); // false
console.log(_.every([{}, true, 2])); // true

not은 연산자 !가 아닌 함수이기 때문에 _.findIndex와 함께 사용할 수 있다. list의 값 중 하나라도 부정적인 값을 만나면 predicate가 not이므로 true를 리턴하여 해당번째 i 값을 리턴하게 된다. 중간에 부정적인 값을 한 번이라고 만나면 루프가 중단된다. 만일 부정적인 값이 하나도 없다면 -1을 리턴한다.
_.every는 쓸모 없어 보이는 정말 작은 함수 not 덕분에 로직이 개선되었다. 좀 더 함수를 쪼개보다. 함수가 가능하면 한 가지 일만 하게끔 말이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function positive(list) {
return _.find(list, _.identity);
}

function negetiveIndex(list) {
return _.findIndex(list, not);
}

_.some = function (list) {
return not(not(positive(list)));
}

_.every = function (list) {
return beq(-1)(negetiveIndex(list));
}

함수 합성

함수를 쪼갤수록 함수 합성은 쉬워진다. 다음은 다양한 함수 합성 기법 중 하나인 Underscore.js의 _.compose다. _.compose는 오른쪽의 함수의 결과를 바로 왼쪽의 함수에게 전달한다. 그리고 해당 함수의 결과를 다시 자신의 왼쪽의 함수에게 전달하는 고차 함수다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_.compose = function () {
var args = arguments;
var start = args.length - 1;
return function () {
var i = start;
var result = args[start].apply(this, arguments);
while (i--) result = args[i].call(this, result);
return result;
}
}

var greet = function (name) { return 'hi: ' + name; };
var exclaim = function (statement) { return statement.toUpperCase() + '!'; };
var welcome = _.compose(greet, exclaim);
console.log(welcome('moe'));

welcome을 실행하면 먼저 exclaim을 실행하면서 “moe”를 인자로 넘겨준다. exclaim 의 결과는 대문자로 변환된 “MOE!”이고 그 결과는 다시 greet의 인자로 넘어가 최종 결과로 “hi: MOE!”를 리턴한다.

1
2
_.some = _.compose(not, not, positive);
_.every = _.compose(beq(-1), negetiveIndex);

_.compose로 _.some과 _.every를 더 간결하게 표현했다. 맨 오른쪽의 함수가 인자를 받아 결과를 만들고 결과는 다시 그 왼쪽의 함수에게 인자로 전달된다. 오른쪽에서 부터 왼쪽으로 연속적으로 실행되어 최종 결과를 만든다.

값 대신 함수로, for와 if 대신 고차 함수와 보조 함수로, 연산자 대신 함수로, 함수 합성 등 앞서 설명한 함수적 기법들을 사용하면 코드도 간결해지고 함수명을 통해 로직을 더 명확히 전달할 수 있어 읽기 좋은 코드가 된다.


Conclusion

저번 포스트에서는 함수를 조합해서 사용한다는 것에 정확한 이해가 부족했는데, 이번 포스트를 통해 어느정도의 이해는 된것같다. 이번 포스트에서 제일 크게 느낀점은 “기능 단위로 최대한 작게 함수를 쪼개 놓고 이 함수들을 조합하여 하나의 고차 함수를 사용한다.” 이다.
if나 for 같은 로직도 함수 단위로 쪼개면서 재사용성을 고려해서 만들어 놓는 다면 코드의 품질 뿐만 아니라 가독성도 좋아지고 유지 보수도 좋아질 것이라는 생각이 들었다. 운영되고 있는 프로젝트에서 에러가 발생해서 고쳐야 하는 경우나 리팩토링을 해야하는 경우 복잡한 로직이라면 이해하기 쉽지 않은 경우가 있다.
내가 짜놓은 코드라도 그때 당시의 고려했던 점이나 여러 경우를 전부 기억하지 못해 수정에 어려움이 있을때가 많다. 이때 만약 함수 단위로 구현해 놓았으면 에러가 발생한 부분이나 고쳐야 할 부분을 함수만 파악하고 고치면 되기때문에 편할 것 같다는 생각이 들었다.

합성함수 (_.compose)의 arguments 부분이 아직 정확하게 이해 되지는 않지만 뒤에서 arguments에 대해 다시 다뤄준다고 했으니 합성함수의 동작방식만 이해하고 넘어가야겠다.

참조: 함수형 자바스크립트 프로그래밍

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.