왜 커링 (currying) 이 더 좋아?
(A,B) -> B 보다 A -> (B->B) 가 더 좋다?
사실 명령형 파라다임에 적폐인 나로써는, 함수형 파라다임의 체득이란 살아 생전 가능할까 싶다. ㅎㅎ 특히 함수형 파라다임을 체득(머리로 이해가 아닌) 하기 위해서 제일 먼저 만나는 관문을 "커링"이라고 보는데...왜 저 짓을 하냐? 저게 진짜 더 좋긴해? 를 넘어서서 저렇게 하는게 너무 당연하게 느껴지는 퀀텀점프의 경험을 하고 싶다.
* 이 글에서는 부분적용함수와의 차이는 무시한다. 부분적용함수도 커링이라고 친다.
함수형 언어와 함수형 프로그래밍이 다르듯이 파이썬이나 자바스크립트로도 충분히 커링이 가능하다.
// 파이썬
def curry(func, var):
y = var
def f(x):
return func(x, y)
return f
커링을 사용한것 과 안 한 소스부터 일단 보면. (포인트의 이동과 회전에 관한 함수로 이루어졌다)
C 소스 (커링 사용안함)
typedef point (*converter_t) (point);
point trans(double dx, double dy, point pt) {
point result = pt;
result.x += dx;
result.y += dy;
return result;
}
// 원래 sin,cos 이용해야하지만 그냥 이렇다고 치자.
point rotate(double theta, point pt) {
point result = {0,0}
result.x = pt.x + theta
result.y = pt.y + theta
return result
}
point trans_by_config (config_t config, point pt){
return trans(config.x, config.y, point pt);
}
point rotate_by_config (config_t config, point pt){
return rotate(config.theta, point pt);
}point convert_by_config(config_t config, point pt){
return trans_by_config(config, rotate_by_config(config, pt));
}
void map_to_points(converter_t conv, size_t n, point * in, point * out) {
unsigned int i = 0;
for (i = 0 ; i < n ; i++) out[i] = conv(in[i]);
}
Javascript 소스 (커링 사용)
function compose(f, g) { return function(x) { return f(g(x))}}
var trans = function(dx,dy) {
return function(point) {
var result = clone(point)
result.x += dx;
result.y += dy;
return result;
}
}
var transByConfig = function(config){
return trans(config.x , config.y);
}
//rotate 생략
var convertByConfig = function(config) {
return compose(transByConfig(config), rotateByConfig(config));
}
var converted_rect = unit_rect.map(convertByConfig(config));
하스켈 소스 (커링 사용, 타입이 맞아야 한다. 강력한 제한을 둠으로써 완전하게 한다)
type Coord = (Double, Double)
data Config = Config { rotAt :: Coord
, theta :: Double
, ofs :: (Double, Double)
}
type CoordConverter = Coord -> Coord
trans :: (Double, Double) -> CoordConverter
trans (dx, dy) = \(x,y) -> (x+dx, y+dy)
rotate :: Double -> CoordConverter
rotate t = \(x, y) -> ( x + t, y + t)
transByConfig :: Config -> CoordConverter
transByConfig config = trans (ofs config)
rotateByConfig :: Config -> CoordConverter
rotateByConfig config = postTrans . rotate (theta config) . preTrans where
rotateAt = rotAt config
preTrans = trans (rotate pi $ rotateAt)
postTrans = trans rotateAt
convertByConfig :: Config -> CoordConverter
convertByConfig config = transByConfig config . rotateByConfig config
main :: IO ()
main = do
let config = Config { rotAt = (0.5,0.5), theta = pi / 4, ofs = (-0.5, -0.5) }
let unitRect = [(0,0),(0,1),(1,1),(1,0)]
let convertedRect = map (convertByConfig config) unitRect
"하스켈로 배우는 함수형 프로그래밍"이라는 책에서 발췌한 코드인데, 해당 책에는 이렇게 쓰여져 있다.
"무엇인가와 좌표를 받아서 좌표를 반환하는 것보다, 무엇인가를 받아서 좌표를 받아 좌표를 반환하는 함수를 반환하는 것이 보다 일반적이고 간단한 조합 방법을 도입할 수 있게 된다" 라고 나온다.
?? 그냥 위의 C 코드에서 함수포인터 만들때 conifg 매개변수도 시그니처로 넣어주면 되는 거 아냐? 라고 반발심도 생기며, 그냥 함수 만들면 되지, 굳이 실행 시점에 만들어지는 함수가 무슨 의미가 있는지, 뭐가 그렇게 좋은지에 대한 "아~~~하" 체험을 하기가 힘들다. 그냥 " 아 그렇군요" 정도는 가능하지만..
대부분의 커링에 관한 블로그에서도 인자수를 줄여주는 마법(?)을 보여주며, 커링 좋지? 라고 말하고 끝난다.
이건 그냥 그 사람이 커링이 먼지 안 것이지, 그게 정말 좋은지에 대한 설명으로는 부족하다.
이럴 때~~~
하스켈을 해 봐야 한다. 명령형 언어의 경우 그냥 함수를 몇개 더 만들면 된다. add2 함수 까짓거 만들어도 되지만 그건 추상이 아니다. 그것은 구체적이다. 따라서 커리 만드는데 행사코드가 덕지덕지 붙는다면 그냥 add2, add3 만드는게 낫다.즉 애초에 그런언어로 커리를 공부하면 맘에 와닿을리 없다.
add 에 add2,add3 등의 함수들을 직접만드는 것은 재활용이 아니다. 함수 시그니처가 재각각인 것들을 합성하는 것은 불가능하다. 함수를 합성 하기 위해서는 매개변수가 작고 타입이 같을 수록 좋다. 즉 x -> x 인 함수들 끼리는 합성하기가 너무 좋을 것이다. 존재하는 함수를 최대한 재활용하는것이 함수형이다. 하스켈에서는 아예 매개변수를 1개로 강제하였다. 즉 하스켈에서는 함수가 아예 커링화 되어있다. 하스켈은 2개의 입력값을 받는 함수가 없으며 만들 수도 없다. 함수합성에 대한 강력한 의지가 보인다. (콜스택에 매개변수를 항상 넘겨서 호출하는 것보다, 미리 내부에 정해져있으면 효율적이기도 할 것이다)
let add x y = x + y
이러한 함수도 2개의 x y 를 받는 함수가 아니다. 따라서 아래와 같은 식이 개발자의 추가 노력없이 가능하다.
let add x y = x + y
map (add 1) [1,2,3,4]
x 를 1로 먼저 입력되어서 만들어진 함수 add y = 1 + y 를 통해 리스트가 입력되어진다.
결국 함수를 재활용하는 함수 합성을 많이 해 봐야 합수합성의 기본도구인 커링,부분함수가 납득(체득)이 되는것 같다.. 이 글을 썼다고 혹은 읽는다고 체득이 되지 않을 것이다. 백날 "자바/파이썬/자바스크립트/스칼라를 통한 함수형 프로그래밍" 류의 책을 읽는 것 보다, 가장 빠른 길은 하스켈를 공부하고, 하스켈 코드를 짜 보는 것 같다. 프로젝트를 하나 해보면 더 좋고~