함수형 언어를 좋아하시는 분들은 Generic Programming에 대해서 어떻게 생각하시나요?
김우승http://www.infopub.co.kr/ebook/pdf/5674-310.pdf
STL Tutorial & Reference Guide 에서 Alexander Stepanov의 서문
솔직히 저는 함수형 언어를 쓰는 경우가 거의 없어서 함수형 언어의 강점을 잘 모르겠더군요.
Generic Programming은 여러모로 Functional Programming과 유사점을 많이 보입니다만 또 분명히 다릅니다. 가장 큰 차이는 iterator겠지요.
Generic Programming은 4대 Programming Paradigm에 들어가지 않아서인지 Functional programming과 비교되는 경우가 많지 않은데 경험이 풍부하신 분들의 고견이 듣고 싶습니다.
C++의 주요 paradigm이었던 OOP에서 GP가 추가되어서인지 OOP와 GP에 대한 비교는 많이 보았습니다만 FP vs. GP 에 대한 글은 보기 어렵더군요.
Alex는 OOP를 싫어하던데 혹시 골수 FP 팬들은 GP를 싫어하지 않을까라는 생각이 듭니다. Alex는 위의 서문에서 past-the-end iterator와 nil을 비교하면서 FP역시 그리 좋아하지 않는 것 같은 느낌입니다.
Functional에서는 리스트 자체가 iterator입니다. next가 아니라 car/cdr를 사용한다는 점만 다를 뿐이죠. C++이나 Java에서 쓰이는 iterator는 이런 car/cdr의 루프 버전으로 봐도 무방하다 생각합니다. 그래서 이런 iterator는 Genric의 특징이 아니라 해당하는 언어에 따른 구현의 특징이라고 봐야할 것 같습니다. 또, list를 인자로 받는 tail-recursion이 있다면 nil이 있다고 별로 달라질 건 없는 것 같군요. 어짜피 루프 방식으로 동작하는 list를 만들어도 sentinel은 있어야 하니까요.
얼핏보면 Functional이 Generic을 싸고 있는 것 같지만, Generic은 지능적 매크로를 통해서 약간의 메타프로그래밍을 가능하게 해주는 도구에 불과합니다. 그렇기 때문에 어떤 언어에서 구현되느냐에 따라서 그 효과나 기능이 천차만별입니다. 단지 동일한 것은 데이터에 관계없이(Generic하게) 알고리즘을 쓰겠다는 것이지요. 그냥 코딩을 하는 입장에서는 이것 저것 따져서 만들어야하는 Functional에 비해서는 Generic을 쓰는 것이 프로그래밍이 좀 더 편하긴 합니다만, 모든 곳에 Generic을 적용할 수는 없는 만큼 그 한계도 명확하다고 보입니다.
뭔가 제가 생각하는 것과는 관점이 많이 다른 것 같네요. 우선 next 예제는 GP의 원조인 C++ STL의 구현방식과는 다릅니다. Design Pattern의 Iterator Pattern을 이야기하시는 것 같네요. STL에서 강조하는 iterator의 특징은 주소체계방식들의 정련(refinement)입니다. 그래서 다양한 iterator 부류가 나오죠. 단순히 list의 car/cdr 방식만 있는 것은 아닙니다.
아마도 Alex는 이런 다양한 iterator를 통해 효율성을 잃지 않는 추상성을 목표로 한 것이라고 생각합니다.
제가 보기에 "컴퓨터과학이 다루는 기존 수학에 없었던 새로운 개념은 주소다."라고 하는 관점이 FP와 가장 크게 대비되는 것 같습니다. FP는 수학의 완벽한 추상성을 컴퓨터 계산모델에서도 잃지 않는 것을 목표로 하지 않나요? 그래서 무한 list 같은 것이 가능한 것을 큰 장점이라고 말하죠.
제가 Java Generics를 GP로 인정하지 않는 것은 Template Metaprogramming이 아닌 iterator 부류때문입니다. 단방향과 양방향만 있거든요. 그리고 이거는 제가 Java Generics를 잘 이해하지 못하는 것뿐일지 모르겠지만 iterator를 reference semantic으로 다루기 때문에 STL과 같은 iterator를 활용한 algorithm 작성이 불가능해 보입니다.
확실히 Alex는 template을 활용한 방식을 추구하는 것 같긴 합니다. 하지만 그게 전부는 아니라고 생각합니다.
물론 iterator의 이런 노출은 비판의 대상이기도 하죠. Programming Ruby에서는 이를 외부반복자라고 표현하더군요. 사실 STL의 iterator는 뭔가 이름을 잘못 지은 것 같다는 생각도 듭니다.
음... 아직 FP에 대한 이해가 부족한 저로서는 과연 효율성의 저하가 없는 FP가 가능한지 의문입니다. 물론 SML이나 OCaML의 속도는 C에 비슷하다는 점은 익히 들었습니다. 하지만 제가 더 주목하는 것은 공간복잡도인데요. 공간복잡도는 시간복잡도에 비해 적게 들거나 많아야 같습니다. 하지만 FP style은 그렇지가 않더군요. 예로 Haskell의 qsort가 있죠. Side effect를 피하려고 하는 FP로 in-place algorithm을 효과적으로 표현한다는 것은 불가능하지 않나요?
잘못아시고 계신게 있는 것 같은데요. Generic은 C++ 이전에도 시도되었습니다. 또한 STL은 Generic을 쓰기는 했지만 STL이 Generic이라 부를 수는 없습니다. STL 철학과 Generic 철학은 다릅니다. 효율성을 잃지 않는 추상성은 STL의 이야기이지 Generic에까지 해당하는 말은 아닙니다. 마찬가지로 iterator도 STL의 도구이지 Generic의 도구가 아닙니다. 즉, Generic과 STL은 다릅니다. 같다고 보고 이야기를 진행하시면 곤란합니다. 그리고 언어마다 구현된 Generic이 옳다 그르다는 생각은 의미가 없습니다. 그른 것이었으면 사람들이 쓰고 있을리 만무하니까요.
굳이 따지자면 효율성이 무엇을 위한 효율성인가요? 만족할만한 성능만 나온다면 고객들은 C++로 만들었든 Haskell로 만들었든 상관하지 않습니다. 하지만 제품이 1개월만에 나오느냐 2개월만에 나오느냐에는 민감하죠. Side-effect가 없으면 프로그램이 복잡하면 복잡할 수록 디버깅 시간은 훨씬 줄어들 수 있습니다. 이것은 곧 제품이 빨리 나올 수 있다는 말이 되구요.
배우는 사람이 가장 경계해야하는 것은 "난 저것을 인정하지 못해", "난 잘모르겠지만 저건 맘에 안들어서 싫어", "난 이걸 쓸 것도 아니니깐 배울필요 없어" 라고 생각하는 겁니다. 먼저 배우고 비판해도 늦지 않습니다. 저는 배우는 시간이 아까운 게 아니라, 저렇게 생각하느라 배우지 않은 시간이 훨씬 아깝습니다.
Generic Programming은 Alex에 의해 처음 시도된 것이라고 보는 것 같은데 그렇지 않나요? 물론 Alex의 첫 시도는 Ada라고 알고 있기는 합니다. 원래 그는 Scheme을 썼었죠.
그리고 Java Generics를 GP라고 인정하지 않는다고 그 가치를 인정하지 않는 것은 아닙니다. 다만 GP를 IP, OOP, FP, LP에 견주는 Programmign Paradigm으로 놓는다면 Java Generics는 GP Paradigm에 있지 않다고 생각할 뿐입니다.
효율성은 언제나 중요한 문제입니다. 상황에 따라서 다양한 우선순위가 존재할 뿐이겠죠. 단순히 현재의 시장논리만으로 무엇을 한다면 학문은 의미가 없습니다. 이것은 FP도 마찬가지겠죠.
이제까지 말했지만 저는 FP를 잘 모릅니다. 그래서 효율성의 저하가 없는 FP가 가능한가에 대해서 묻고 있는 것이죠.
제가 보기에 Alex의 목표는 algorithm 논문을 그대로 따를 수 있는 표현이 가능한가가 아닐까 싶네요. Algorithm을 다룰 때는 type에 대해서 general 하게 다루니까요. 그리고 algorithm 은 효율성을 가장 중요한 point로 다루고 있죠.
당연한 말입니다만, 효율성이 중요하긴 합니다. 하지만, 다시 묻지만, 효율성이 무엇을 위한 무엇을 기준으로한 효율성인가요? 단지 코드가 빠르게 돌아가면 효율성이 좋은 건가요? 웹프로그래밍을 하는데 0.1ms때문에 더 빠르게 동작하는 루프로 바꿔야할까요? Functional의 코드가 느리게 돌아가는 것은 분명합니다. 하지만 그정도는 느려도 되기 때문에 느린겁니다.
빠르기만한 것이 좋다면 C++를 쓰지말고 C를 써야하지 않을까요? C보다는 Assembly가 좋고 이것저것 필요없이 기계어로 직접 코딩하는 것이 가장 좋을것 같네요. 기계어의 런타임 조차도 부담이라면 칩으로 박아버리면 됩니다. 그럼 이렇게 하는 것이 제일 효율적인가요? C++은 칩으로 박지 않아도 될만큼, 굳이 기계어를 쓰지 않아도 될만큼, 굳이 Assembly를 쓰지 않아도 될만큼, 굳이 C를 쓰지 않아도 될만큼, 느려도 되기 때문에 쓰이는 겁니다. FP도 마찬가지입니다.
그리고 다시 반복적으로 말하지만, STL을 Generic으로 잘못알고계십니다. STL과 Template, Generic은 서로 관계는 있지만 각자 다른 개념입니다. 이 두 세가지를 뭉뜽거려서 Generic이라고 부르는 것은 "의견이 다른 것"이 아니라 "잘못된" 행동입니다. 우승님이 STL과 다른 언어의 Generic의 공통점을 뽑아낼 수 있다면 그것이 바로 Generic의 개념에 가까울겁니다.
뭔가 잘못하면 flame war가 될 것 같다는... -_-..
우선 저도 효율성을 언제나 첫 머리에 두지 않는다고 말씀드리구요.
Algorithm에는 언제나 따라다는 것이 점근복잡도일겁니다. FP를 통해 algorithm의 점근시간복잡도와 점근공간복잡도가 유지되는 것이 가능한지 여쭤봅니다...
흐흐. 당연히 프로그래밍인데 algorithm의 점근복잡도가 달라질리가 없지요. 이건 Logic 언어도 마찬가지입니다. 단지 Pure Functional은 list를 기본으로 하기에 array와 같은 상수시간은 어렵습니다. 잘해봐야 nlogn이죠. 물론 C의 nlogn과 Haskell의 nlogn의 속도차이야 당연히 벌어지겠지요?
네, 확실히 제가 잘못 알았네요. 죄송합니다.
C++ 표준 라이브러리의 반복자가 포인터의 정련인 것은 언어의 특징을 따랐기 때문이겠죠. 제너릭 프로그래밍이 꼭 그런 인터페이스의 반복자를 다뤄야할 필요는 없습니다. 물론 자료 구조에 독립적인 알고리즘을 위해서는 다양한 반복자 종류가 있긴 해야겠죠. 하지만 그것이 꼭 포인터의 정련일 필요는 없습니다.
제너릭 프로그래밍 자체는 Python 등의 언어로도 가능합니다. 그런 언어에서는 굳이 제너릭 프로그래밍이라는 말을 만들어낼 필요도 없겠죠. C++에서 제너릭 프로그래밍을 할 때 템플릿을 사용하는 것은 언어 특성에 따라 구현 방식이 드러났을 뿐이지, 그것이 곧 제너릭 프로그래밍이라고 볼 수는 없습니다.
Haskell 표준 라이브러리의 함수들은 제너릭 프로그래밍에 가깝습니다. 다만 C++처럼 굳이 템플릿을 쓸 필요 없이 형식 변수(type variable)를 쓰면 되죠.
함수 선언 이전에 시그너쳐를 썼는데, 사실 굳이 쓸 필요는 없습니다. 그냥 형식 변수 쓰는 방법(그냥
a라고 썼죠)을 보여드리기 위한 예제일 뿐입니다.하스켈에서는 타입 추론과 패턴 매칭이 어느 정도 Generic Programming을 가능하게 하는 것 같습니다.
Generic Programming의 화두는, 데이터의 타입에 관계없이 Generic하게 알고리즘을 쓰겠다는 것으로 알고 있는데요. 기본적으로 Duck타입 언어에선, 어차피 이런 부분이 별도로 필요 없지 않나요? Generic은 일종의 데이터 타입과 관련된 것이고, Functional이나 OOP 같은 것은 이것과는 별개의 패러다임이 아닐지요.
Generic Programming의 좋은 예라고 할 수 있는 C++의 STL의 스택, 큐, 리스트 등만 보더라도, "우리는 정적 타입 언어이지만, 한가지 알고리즘으로 이렇게 여러가지 타입을 샤샤샥 할 수 있다~ 아하하~" 정도가 되겠지만, Perl, Python, Ruby, Lisp 등등의 Duck타입 언어의 입장에선, 그냥 원래 되는거잖아요;;
즉, Functional과 Generic을 비교하려면, 뭔가 다른 점이 있어야 하는데, Functional이든 OOP이든 절차적 방식이든, (1) Duck타입이고, (2) 적당히 루프를 돌 수 있는 리스트만 제공하면, 대충 Generic을 적용할 수 있으니깐, 비교하긴 좀 애매하지 않을까요??
p.s. 4대 패러다임은 절차적, OOP, 함수형.. 나머지 하나는 뭐에요??
Logic Programming 이죠.
Markdown에서 백쿼트로 감싸는 것(
이렇게)은 코드에 쓰는 거고, 강조를 하실 때는하시면 됩니다. HTML으로 나올 때 전자는
<code>태그로 나오고, 후자는<strong>태그로 나와요.고쳤습니다. :-) 사실 그냥 까맣게 한번 해보고 싶어서..;;
첫머리글의 STL Tutorial & Reference Guide의 서문을 다시 읽어보니 확실히 제가 GP에 대해 오해(반복자 정련을 통한 효율성을 잃지 않는 추상성)를 한 것 같군요.
그런데 이해가 안 가는 것은 GP는 Alex에 의해 창조되었다고 하는데 단순히 Generic Type을 다루는 algorithm을 GP라고 한다면 Alex가 GP를 창안했다는 것은 지난친 광고글이었을까요?
위키백과의 Generic Programming에 관한 글을 한번 읽어보시기 바랍니다. '매개변수로 넘길 수 있는 타입'이라는 말도 있고, 저에게는 '코드를 생성하지 않는 메타프로그래밍'이라는 설명이 제일 와닿네요. 딱히 누가 만들었다고 말하기는 힘들 것 같고요. 언어별로 차이가 있긴 하지만 대부분 GP를 할 수 있다고 봐야겠네요.
흠... 이것도 제가 잘못 알고 있었던 거군요. 나참... 제가 좀 이렇습니다. 양해 부탁~. (굽신굽신)
Alex가 혼자 창안한 것은 GP가 아니라 STL Architecture 군요...
Alex가 GP를 생각해낸 것은 맞습니다. 어쩌면 생각해냈기보다 이전에도 있었지만 사람들이 뭔지 모르고 있을 때 정리를 했다고 하는 것이 정확하겠군요. 덕분에 많은 언어에서 Generic을 쓰고 있구요. Alex가 없었다면 C++은 벌써 죽은 언어였겠죠.
재미있는쓰레드네요.
윗글을 읽으며 생각을 해봤는데 몇가지 의문사항, 혹은 정의하고 넘어갔으면 하는 부분들이 생겼습니다. (jong10님의 지적과 상통하며)
한마디로 기존의 FP 또는 duck-typing을 하는 다이나믹언어들과의 경계는 어떤지점일지 궁금하네요.
ps. 민희언니, 미리보기 좀 만들어주면 안되효?
주말에 만들어볼께요;
아겔님이 지적하셨듯이 Generic Programming과 Compile-time Metaprogramming은 구별해야 할 것입니다.
C++/STL이 Compile-time Metaprogramming의 가장 성공적인 예라는 주장은 가능할 듯 합니다. MetaOCaml이나 Template Haskell이 널리 쓰이는 것은 아니니까요. 물론 리습 계열 언어들은 태고적부터 매크로를 잘 써 왔습니다만...
컴파일 시간 코드 생성에서 C++보다 더 나아간 언어라면 D의 메타프로그래밍을 들 수 있겠습니다.
함수형 언어의 polymorphism이 Generic Programming의 많은 부분을 표현할 수 있지만 그 부분집합이라고 보는 게 옳을 것이고, 함수형 언어에 Generic Programming을 결합하려는 시도로는 Haskell의 SYB 같은 접근이 있습니다.