이 글은 Charles Scalfani의 So You Want to be a Functional Programmer (Part 4)를 번역한 게시물입니다.
Thank you Charles Scalfani! Thanks to your writing, I can grow into a better developer.
함수형 프로그래밍의 개념을 이해하기 위해 내딛는 첫걸음은 매우 중요하다. 매우 힘든 첫걸음이지만 올바른 관점으로 접근한다면 힘들어할 필요가 없다.
이전 게시물 : Part 1, Part 2, Part 3
커링
Part 3의 마지막 부분에서 mult5 함수는 1개의 파라미터를 받았고, add 함수는 2개의 파라미터를 받았기 때문에 mult5 함수와 add10 함수를 합성할 때 문제가 발생했었다.
우리는 모든 함수가 하나의 파라미터만 받도록 제한함으로써 이 문제를 해결할 수 있다.
나를 믿어라. 이건 그렇게 나쁜 방법이 아니다
우리는 2개의 파라미터를 사용하지만 한 번에 1개만 사용하는 add 함수를 작성한다. 커링 함수는 이것을 가능하게 해 준다.
커링 함수는 한 번에 1개의 파라미터만 받는 함수이다.
커링 함수는 우리가 mult5 함수와 합성하기 전에, 첫 번째 파라미터를 add 함수에게 준다. 그러고 나서 mult5AfterAdd10 함수가 호출될 때, add 함수는 두 번째 파라미터를 받게 된다.
이제 자바스크립트에서 add 함수에 커링을 적용한 것을 살펴보자.
var add = x => y => x + y
이 add 함수는 지금은 하나의 파라미터만 받고 나중에 또 다른 하나의 파라미터를 받는 함수이다.
자세히 말하면, add 함수는 1개의 파라미터 x를 받고, 1개의 파라미터 y를 받는 함수를 리턴하는데, 이는 결국 x와 y를 합하는 결과를 리턴하게 된다.
이제 우리는 이 add 함수를 이용하여 개선된 mult5AfterAdd10 함수를 만들 수 있다.
var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));
이 합성함수는 2개의 파라미터 f와 g를 받는다. 그리고 x라는 1개의 파라미터를 받는 함수를 리턴한다. 이 함수가 호출될 때 g함수에 x를 적용한 후에 그 결과를 f함수에 적용한다.
그래서 우리가 한건 정확히 뭘까? 단순한 구식 add 함수를 커링 방식의 버전으로 바꿨다. 첫 번째 파라미터인 10을 상위 함수에 전달할 수 있고, 마지막 파라미터는 mult5AfterAdd10이 호출될 때 전달될 것이다. 때문에 add 함수는 좀 더 유연해졌다.
이쯤 되면, Elm에서 커링을 이용한 add 함수를 어떻게 만들지 궁금할 것이다.
궁금할 필요 없다. Elm를 포함한 다른 함수형 언어에서는 모든 함수가 자동으로 컬링 된다.
그래서 add 함수는 동일하다.
add x y =
x + y
아래 코드가 바로 Part 3에서 작성되었어야 할 mult5AfterAdd10 함수다.
mult5AfterAdd10 =
(mult5 << add 10)
Elm은 자바스크립트 같은 명령형 언어를 이긴다. 왜냐하면 Elm은 커링과 합성 같은 함수적인 것들에 최적화되어있기 때문이다.
커링과 리팩터링
파라미터가 많은 함수인 일반 버전을 만든 다음, 파라미터가 적은 함수인 특정 버전을 만들기 위해 리팩터링 할 때 커링은 다시 한번 빛난다.
예를 들어, 문자열에 단일 괄호 세트와, 이중 괄호 세트를 추가하는 함수가 아래에 있다.
bracket str =
"{" ++ str ++ "}"
doubleBracket str =
"{{" ++ str ++ "}}"
사용 방법은 다음과 같다.
bracketedJoe =
bracket "Joe"
doubleBracketedJoe =
doubleBracket "Joe"
우리는 이제 bracket과 doubleBracket을 생성할 수 있다.
generalBracket prefix str suffix =
prefix ++ str ++ suffix
하지만 이제 generalBracker을 사용할 때마다 bracket을 넘겨줘야 한다.
bracketedJoe =
generalBracket "{" "Joe" "}"
doubleBracketedJoe =
generalBracket "{{" "Joe" "}}"
사실 우리가 정말로 원하는 것은 일거양득(the best of both worlds)이다.
만약 우리가 generalBracket의 파라미터의 순서를 바꾼다면, 커링 함수라는 사실을 활용하여 bracket과 doubleBracket을 생성할 수 있다.
generalBracket prefix suffix str =
prefix ++ str ++ suffix
bracket =
generalBracket "{" "}"
doubleBracket =
generalBracket "{{" "}}"
먼저 static으로 고정할 부분(prefix와 suffix)을 첫 번째 파라미터에 넣고, 변경될 가능성이 높은 파라미터(str)를 가장 마지막에 배치하면 쉽게 generalBracket의 특정 버전을 생성할 수 있다.
파라미터의 순서는 완전한 커링 활용을 위해서 중요하다.
또한 bracket과 doubleBracket은 Point-Free 표기법 즉, str 파라미터가 함축되었다는 것에 주목하자. bracket과 doubleBracket 모두 마지막 파라미터를 기다리고 있는 함수이다.
이제 우리는 이전과 동일하게 사용할 수 있다.
bracketedJoe =
bracket "Joe"
doubleBracketedJoe =
doubleBracket "Joe"
일반적인 함수형 함수
함수형 언어들에서 사용하는 일반적인 함수 3가지를 알아보자.
그전에, 아래의 자바스크립트 코드를 살펴보자.
for (var i = 0; i < something.length; ++i) {
// do stuff
}
이 코드는 큰 문제가 있다. 버그는 아니다. 그 문제는 바로 boilerplate 코드다. 즉, 준비 코드를 계속 반복해서 작성해야 한다는 것이다.
만약 당신의 코드가 Java, C#, 자바스크립트, PHP, Python 같은 명령형 언어에서 작성한 코드라면, 당신은 함수형 언어보다 더 많은 boilerplate 코드를 작성하고 있는 자기 자신을 발견할 것이다.
그러니 boilerplate를 없애보자. boilerplate를 하나 또는 두 함수 안에 넣고 다시는 for-loop를 작성하지 않도록 해보자. 음... 사실 명령형 언어가 아닌 함수형 언어를 사용하기 전까지 불가능하다고 보면 된다.
things라는 배열을 변경해보자.
var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
things[i] = things[i] * 10; // MUTATION ALERT !!!!
}
console.log(things); // [10, 20, 30, 40]
안돼! 불변하지 않잖아!!
다시 시도해 보자. 이번엔 things가 불변할 것이다.
var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]
우리는 things를 불변하게 만들었지만, newThings는 불변하지 않다.
일단 우리는 이 사실을 무시하고 넘어가자. 위의 코드는 명령형 프로그래밍 언어인 자바스크립트로 작성한 것이니까. 우리가 함수형 언어(예: Elm)를 사용한다면 불변할 것이다.
여기서의 요점은 이러한 함수들이 어떻게 동작하는지 이해하고, 우리 코드의 소음을 줄일 수 있도록 돕는 것이다.
이 코드를 가지고 함수에 넣자. 첫 번째, 공통 함수 map을 호출할 것이다. 왜냐하면 이 함수는 이전 배열의 각 요소를 새 배열의 새 값에 매핑시켜주기 때문이다.
var map = (f, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
newArray[i] = f(array[i]);
}
return newArray;
};
함수 f가 전달되어 map함수가 array에 있는 각 요소에 원하는 작업을 할 수 있다는 것에 주목하자.
이제 map 함수를 이용하기 위해 이전 코드를 다시 작성해보자.
var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);
for-loop가 없다!. 그리고 훨씬 읽기 쉬워졌고, 추론하기도 쉽다.
기술적으로 map 함수 안에 for-loop가 있다. 그러나 적어도 더 이상 boilerplate 코드를 작성할 필요가 없어졌다.
이제 또 다른 공통 함수인 filter를 작성해보자.
var filter = (pred, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
if (pred(array[i]))
newArray[newArray.length] = array[i];
}
return newArray;
};
어떻게 pred 함수가 배열의 요소를 유지하고 싶으면 True, 요소를 버리고 싶으면 False를 리턴하는지 살펴보자.
아래의 filter 함수가 홀수를 필터링하는 동작을 살펴보자.
var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]
새로운 filter 함수를 사용하는 것은 직접 for-loop 반복문을 작성하는 것보다 매우 간단하다.
마지막 공통 함수형 함수는 reduce이다. reduce는 리스트를 갖고 와서 하나의 값으로 줄이기 위해 사용되지만, 실제로는 훨씬 더 많은 것을 할 수 있다.
아래 함수는 함수형 언어에서 fold라고 불린다.
var reduce = (f, start, array) => {
var acc = start;
for (var i = 0; i < array.length; ++i)
acc = f(array[i], acc); // f() 함수는 파라미터 2개를 받는다.
return acc;
});
reduce함수는 f라는 감축 함수와 초기 값, 그리고 배열을 사용한다.
감축 함수인 f는 2개의 파라미터, array의 최근 요소, 그리고 누산기인 acc를 받는다. f함수는 이 파라미터들을 사용하여 각 반복마다 새로운 누산기를 생성한다.
반복이 끝나면, 최종 누산기의 값이 리턴된다.
아래는 fold가 어떻게 동작하는지를 이해하기 위한 예제이다.
var add = (x, y) => x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15
add함수는 2개의 파라미터를 받아서 합한다.
reduce 함수는 초기값이 0부터 시작하고, 합치기 위해 배열 values에 이 값을 전달한다. reduce함수 안에서, 값이 반복됨에 따라 합이 누적된다. 마지막 총 누적된 결괏값인 sumOfValues가 리턴된다.
map, filter, 그리고 reduce 함수는 boilerplate인 for-loop 반복문을 작성할 필요 없이 배열을 조작할 수 있게 해 준다.
함수형 언어에서 map, filter, reduce는 훨씬 유용하다. 생성에서의 loop 조차 없고, 재귀로만 이루어져 있기 때문이다. 반복 함수들은 프로그래밍에 그렇게 많은 도움을 주진 못한다. 하지만 필수적이다.
머리 아파! 이제 한계야!
오늘은 여기까지.
이후 게시물에서는 참조 투명성, 실행 순서, 타입 등에 대해 이야기하려고 한다.
다음 게시물 : Part 5
글에 번역 오류가 있으면 알려주세요 감사합니다.