함수형 프로그래머가 되고 싶다고? (part 3)
이 문서는 https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7#.ii6um685m 를 번역한 내용입니다.
함수형 프로그래밍 개념을 처음으로 접해본다는 것 자체가 중요하다. 그리고 이 단계가 가장 어렵기도 하다. 하지만, 올바른 관점으로 접근한다면 그렇게 어렵진 않다.
합성 함수
프로그래머들은 게으르다. 개발하고 테스트하고 배포하는 과정을 매번 반복하는 것을 원치 않는다.
우리는 항상 일을 한 번에 끝낼 수 있는 방법을 찾고, 한 번 만든 걸 어떻게하면 코드를 재활용할 수 있을지에 대해 생각한다.
코드 재활용은 보기에는 좋아 보이지만, 달성하기 어려운 개념이다. 코드를 작성할 때 너무 구체화하면 재활용할 수 없다. 그렇다고 너무 느슨한 코드로 바꾸면 처음에 적용한 곳에서 그 코드를 사용하기 어려워진다.
코드를 작고 재활용이 가능한 조각으로 쪼개면 그 코드는 더 복잡한 기능의 조각이 될 수 있다. 마치 건물의 벽돌처럼 말이다. 그렇기 때문에 위의 경우를 포함한 둘 사이의 균형이 필요하다.
함수형 프로그래밍에서 함수는 벽돌과 같은 개념이다. 우리는 매우 구체적인 기능을 하기위해 함수를 작성한다. 그리고 그 함수들을 마치 레고 블럭처럼 조합한다.
이런 걸 합성 함수라고 부른다.
그래서 합성 함수는 어떻게 동작할까? 아래 두 개의 자바스크립트 함수로부터 시작해보자.
var add10 = function(value) {
return value + 10;
};
var mult5 = function(value) {
return value * 5;
};
이 코드는 너무 장황하니까 화살표 함수로 다시 작성해보자.
var add10 = value => value + 10;
var mult5 = value => value * 5;
괜찮아졌다. 이제 우리는 한 개의 값을 입력받고, 그 값에 10을 더하고, 5와 곱한 값을 반환하는 새로운 함수가 필요하다고 가정해보자.
var mult5AfterAdd10 = value => 5 * (value + 10)
굉장히 간단한 예제 임에도 불구하고, 우리는 아무런 사전 지식 없이 이 함수를 작성하는 것을 원치 않는다. 첫째로 우리는 괄호를 빼먹는 것과 같은 실수를 할 수 있다.
둘째로 우리는 이미 10을 더해주는 함수를 가지고 있다. 그리고 5를 곱해주는 함수도 가지고 있다. 우리는 이미 쓰여진 코드를 작성했다.
그래서 add10과 mult5를 사용해서 새로운 함수를 만들어 보자.
var mult5AfterAdd10 = value => mult5(add10(value));
우리는 기존 함수를 사용해서 mult5AfterAdd10라는 함수를 만들었다. 하지만 이거보다 더 나은 방법이 있다.
수학에서 f ∘ g 는 합성 함수다. 그리고 이 개념은 “g와 합성된 f”로 읽을 수 있다. 그래서 (f ∘ g)(x)는 x 값과 함수 g가 호출된 다음 함수 f를 호출한 것과 동일하다. 혹은 간단하게 f(g(x))와 같다.
우리의 예제에서, 우리는 mult5 ∘ add10를 가졌다. 따라서 새로운 함수의 이름을 mult5AfterAdd10로 지었다.
그리고 이게 바로 우리가 한 것이다. 입력값과 add10을 호출한 후에 mult5를 호출했다. 혹은 간단하게 mult5(add10(value))와 동일하다.
네이티브 자바스크립트는 합성 함수를 지원하지 않기 때문에 Elm을 보자.
add10 value =
value + 10
mult5 value =
value * 5
mult5AfterAdd10 value =
(mult5 << add10) value
Elm에서는 « 라는 이항 연산자로 함수끼리 합성한다. 이 연산자는 데이터가 어떻게 흘러가는지 시각적으로 알려준다. 첫째로 입력값이 add10에게 전달되고, 결과값이 mult5에 전달된다.
mult5AfterAdd10의 괄호 부분을 살펴보자. 즉 (mult5 « add10). 입력값을 적용하기 이전에 먼저 함수끼리 합성이 된다는 것을 확신하기 위해 괄호가 존재한다.
이 방법으로 많은 함수들을 합성할 수 있다.
f x =
(g << h << s << r << t) x
x는 t 함수로 전달된 뒤, 결과값이 r로 전달된 후, 그 결과값이 s로 전달된다. (나머지 생략) 만약 이와 비슷한 행위를 자바스크립트에서 했다면, 대략 g(h(s(r(t(x))))) 이런 식으로 생겼을 것이다.
Point-Free Notation
함수를 작성할 때 매개변수를 정의하지 않는 Point-Free 표기법이라고 불리는 방식이 있다. 처음에 이 방식은 이상하게 보이겠지만, 계속 사용하다보면 이 방식의 간결함에 감사하게 될 것이다.
mult5AfterAdd10를 보면 value가 2번 명시된 것을 알 수 있다. 매개변수 리스트에서 한 번, 그리고 사용이 되는 부분에서 한 번 말이다.
-- 1개의 매개변수를 기대하는 함수
mult5AfterAdd10 value =
(mult5 << add10) value
하지만 이 매개변수는 필요하지 않는다. add10이 같은 매개변수를 기대하기 때문이다. 아래의 point-free 방식은 동일한 기능을 한다.
-- 마찬가지로 1개의 매개변수를 기대하는 함수
mult5AfterAdd10 =
(mult5 << add10)
point-free 방식에는 많은 장점이 있다.
첫째로 불필요한 매개변수를 사용하지 않아도 된다. 그렇기 때문에 그것들에 대한 이름을 생각하지 않아도 된다.
둘째로, 훨씬 간결해지기 때문에 읽기에도 편하다. 이 예제는 간단하지만, 매개변수가 엄청 많은 함수를 상상해보자.
Trouble in Paradise
지금까지 합성 함수가 어떻게 동작하는지, 그리고 간결함과 유연성을 위해 Point-Free 표기법으로 어떻게 함수를 정의해야하는지에 대해 살펴봤다.
이제 약간 다른 시나리오에서 이 아이디어들을 사용해보자 그리고 어떻게 되는지 살펴보자. add10을 add로 치환했다고 가정하자.
add x y =
x + y
mult5 value =
value * 5
이 2개의 함수로 어떻게 mult5After10을 작성할까?
계속 읽어나가기 전에 해결 방법에 대해 생각해보자. 너무 깊히는 말고 간단하게 생각해보자.
음 만약 실제로 생각해봤다면 아래와 같은 해결책을 생각해낼지도 모른다.
-- This is wrong !!!!
mult5AfterAdd10 =
(mult5 << add) 10
하지만 정상동작하지 않을 것이다. 왜그럴까? add 함수는 2개의 매개변수를 받기 때문이다.
만약 이게 Elm이라서 명확하지 않다면, 자바스크립트에서 작성해보자.
var mult5AfterAdd10 = mult5(add(10)); // this doesn't work
이 코드는 잘못됐다. 왜 그럴까?
왜냐하면 add 함수는 2개의 매개변수를 받게 되어있는데, 1개만 전달되니까 부정확한 결과가 mult5에 전달되었다. 그 결과 잘못된 결과가 산출되었다.
사실 Elm에서 잘못된 코드는 이미 컴파일 단계에서 알려준다. (Elm의 가장 큰 장점 중 하나)
다시 시도해보자.
var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free
이건 point-free 방식이 아니지만, 사실 이 방식으로도 가능은 하다. 하지만 더이상 함수를 결합하지 않을 것이다. 새로운 함수를 작성할 것이다. 또한, 함수가 점점 더 복잡해지면 (예를 들어 mult5AfterAdd10를 또다른 함수와 조합할 때) 진짜 문제에 처하게 될 것이다.
그래서 함수 합성의 장점은 한계가 있으므로 이 두 개의 함수를 결합(marry)할 수 없게 되었다. 강력한 기능이지만, 너무 아쉽다.
어떻게 이걸 해결할까? 이 문제를 해결하기 위해서 어떤 것이 필요할까?
음. 만약 add 함수에 1개의 매개변수만 보내고, mult5AfterAdd10 호출될 때 두 번째 매개변수를 전달한는 방법이 있다면 정말 훌륭할 것 같다.
커링이라는 방법으로 해결할 수 있다.
아이고 머리야!!!
오늘은 이걸로 충분하다.
다음 문서부터는 커링, 함수형 함수(map, filter, fold 등), 참조 투명성 등에 대해 이야기 할 예정이다.