Eric Kidd가 Ruby의 함수형 표현력, 매크로 유사 기능, 그리고 라이브러리·커뮤니티·성장세를 바탕으로 Lisp와 비교해 살펴본 글과 긴 토론.
Eric Kidd라는 개발자이자 가끔 창업가가 올리는, 소프트웨어에 관한 무작위 코드 조각, 프로젝트, 그리고 단상들입니다. 연락하기를 원하시면 언제든지 환영합니다!
2005년 12월 03일 • 작성자 Eric Kidd
몇 년 전, 저는 Ruby를 보고는 무시하기로 했습니다. Ruby는 Python만큼 인기 있지도 않았고, Lisp만큼 강력하지도 않았습니다. 그렇다면 왜 신경 써야 할까요?
물론 이 기준을 뒤집어 볼 수도 있습니다. 만약 Ruby가 _Lisp_보다 더 인기 있고, _Python_보다 더 강력하다면 어떨까요? 그 정도면 Ruby가 흥미로워질까요?
이 질문에 답하기 전에, 먼저 Lisp를 그렇게 강력하게 만드는 것이 무엇인지 정해야 합니다. Paul Graham은 Lisp의 장점에 대해 아주 훌륭하게 글을 쓴 적이 있습니다. 하지만 논의를 위해, 저는 그것을 두 가지로 압축해 보고 싶습니다.
알고 보니 Ruby는 함수형 언어로서도 꽤 잘 비교되고, 매크로도 제가 생각했던 것보다 더 잘 흉내 냅니다.
밀도 높은 언어는 난해하게 만들지 않고도 간결하게 말할 수 있게 해 줍니다. 한 번에 프로그램의 더 많은 부분을 볼 수 있고, 버그가 숨을 곳도 그만큼 줄어듭니다. 일정 수준을 넘어서면 프로그램을 더 밀도 높게 만드는 유일한 방법은 더 강력한 추상화를 사용하는 것입니다.
특히 강력한 추상화 중 하나는
lambda
입니다.
lambda
를 사용하면 즉석에서 새 함수를 만들고, 다른 함수에 전달하고, 나중에 쓰려고 저장해 둘 수도 있습니다. 예를 들어 리스트의 각 숫자를 두 배로 만들고 싶다면, 다음과 같이 쓸 수 있습니다.
(mapcar (lambda (n) (* n 2)) mylist)
mapcar
는
mylist
의 각 원소를 변환하여 새로운 리스트를 만듭니다. 이 경우 변환은 “각 값 _n_에 대해, _n_에 2를 곱하라”라고 읽을 수 있습니다. JavaScript에서는
lambda
를
function
으로 쓰는데, 어쩌면 이쪽이 조금 더 분명할 수도 있습니다.
map(function (n) { return n*2 }, mylist)
물론 이것은
lambda
로 할 수 있는 일의 극히 일부일 뿐입니다. 이런 프로그래밍 스타일을 선호하는 언어를 함수형 언어라고 부르는데, 함수 자체를 다루기 때문입니다. 밀도 높은 함수형 언어는 정말로 아주 간결할 수 있고, 읽는 법만 익히면 꽤 명확하기도 합니다.
그렇다면 함수형 프로그래밍 측면에서 Ruby는 Lisp와 비교해 어떨까요? Paul Graham의 대표적인 예제, 누산기를 만드는 함수를 살펴봅시다.
(defun foo (n) (lambda (i) (incf n i)))
이 코드는 Ruby에서는 약간 더 짧고, 표기법도 C 해커들에게 더 익숙합니다.
def foo(n) lambda {|i| n+=i} end
acc = foo 3
acc.call(1) # --> 4
acc.call(10) # --> 14
acc.call(0) # --> 14
그런데 Ruby에는 타이핑을 더 줄여 주는 흥미로운 특수한 경우가 있습니다.
lambda
를 인자로 받는 아주 우스운 함수를 생각해 봅시다.
;; 각 자연수마다 한 번씩 'fn'을 호출한다.
(defun each-natural-number (fn)
(loop for n from 1 do (funcall fn n)))
;; 1, 2, 3... 출력
(each-natural-number
(lambda (n) (format t "~D~%" n)))
이제 Ruby에서도 같은 함수를 쓸 수는 있습니다.
def each_natural_number(fn)
n = 0
loop { fn.call(n += 1) }
end
each_natural_number(lambda {|n| puts n })
하지만 더 잘할 수 있습니다.
yield
를 사용해
lambda
와
fn
을 없애 봅시다.
def each_natural_number
n = 0
loop { yield n += 1 }
end
each_natural_number {|n| puts n }
맞습니다.
yield
는 특수 목적용 해킹이고, 단 하나의
lambda
를 받는 함수에서만 동작합니다. 하지만 함수형 코드가 많은 곳에서는
yield
가 엄청난 이득을 줍니다. 비교해 봅시다.
[1,2,3].map {|n| n*n }.reject {|n| n%3==1 }
(remove-if (lambda (n) (= (mod n 3) 1))
(mapcar (lambda (n) (* n n))
'(1 2 3)))
큰 프로그램에서는 이런 차이가 누적됩니다. (Lisp를 변호하자면,
lambda
를 더 간결하게 만드는 reader macro를 쓸 수는 있습니다. 하지만 실제로는 거의 그렇게 하지 않습니다.)
이쯤 되면 Lisp 해커들은 이렇게 말하고 있을 것입니다. “좋아,
lambda
에 대한 좋은 문법은 괜찮다. 하지만 매크로는?” 그리고 이건 아주 좋은 질문입니다. Lisp 매크로는 다음과 같은 함수입니다.
Lisp 매크로의 가장 흔한 용도는
lambda
를 조금 덜 타이핑하게 해 주는 것입니다.
(defmacro with-each-natural-number (n expr)
`(each-natural-number (lambda (,n) ,expr)))
(with-each-natural-number n
(format t "~D~%" n))
defmacro
는 리스트를 인자로 받아 다른 리스트를 반환하는 함수를 정의합니다. 이 예제에서는 컴파일러가
with-each-natural-number
를 볼 때마다 매크로가 호출됩니다. Lisp의 “backquote” 문법을 사용해 템플릿으로부터 리스트를 빠르게 만들고, 그 안에 _n_과 _expr_를 채워 넣습니다. 그런 다음 그 리스트를 컴파일러에 다시 넘깁니다.
물론 이 매크로는 Ruby에서는 쓸모가 없을 것입니다. 우리가 Ruby에서는 그런 문제를 애초에 갖고 있지 않기 때문입니다.
Lisp 매크로의 두 번째로 흔한 용도는 무언가를 정의하기 위한 미니 언어를 만드는 것입니다.
;; 가상의 "Lisp on Rails"를 사용해
;; 데이터베이스 바인딩을 생성한다.
(defmodel <order> ()
(belongs-to <customer>)
(has-many <item> :dependent? t))
Ruby on Rails를 사용하면 다음처럼 쓸 수 있습니다.
class Order < ActiveRecord::Base
belongs_to :customer
has_many :items, :dependent => true
end
여기서
belongs_to
는 클래스 함수입니다. 호출되면
Order
에 여러 멤버 함수를 추가합니다. 구현은 꽤 지저분합니다만, 인터페이스는 훌륭합니다.
어떤 매크로 비슷한 기능이 진짜인지 시험하는 방법은, 그것이 미니 언어를 만드는 데 얼마나 자주 쓰이느냐입니다. 그리고 여기서 Ruby는 좋은 점수를 얻습니다. Rails 외에도 Rake(Makefile 작성용), Needle(컴포넌트 연결용), OptionParser(명령행 옵션 파싱용), DL(C API와 대화하기 위한 것), 그리고 셀 수 없이 많은 다른 것들이 있습니다. Ruby 프로그래머들은 _모든 것_을 Ruby로 씁니다.
물론 고급 Lisp 매크로 중에는 Ruby로 쉽게 옮길 수 없는 것들도 많습니다. 특히 실제로 미니 언어를 _컴파일_하는 매크로는 아직 등장하지 않았습니다. 다만 충분한 노력이 있다면 가능할지도 모릅니다. (Ryan Davis는 ParseTree와 RubyInline으로 이 방향에서 꽤 희망적인 작업을 해 왔고, 저도 관련 기법을 발견하는 대로 글을 쓸 생각입니다.)
그렇다면 Lisp가 여전히 Ruby보다 더 강력하다면, 왜 Lisp를 쓰지 않을까요? Lisp로 프로그래밍하는 데 대한 전형적인 반론은 다음과 같습니다.
압도적인 반론은 아니지만, 분명히 고려할 가치는 있습니다.
한때 Common Lisp의 표준 라이브러리는 아주 거대하다고 여겨졌습니다. 하지만 오늘날 기준으로 보면 고통스러울 정도로 작아 보입니다. Java의 매뉴얼은 벽 한쪽을 채우고, Perl의 CPAN 아카이브에는 상상할 수 있는 거의 모든 것을 위한 모듈이 있습니다. 이에 비하면 Common Lisp에는 네트워크와 통신하는 표준 방법조차 없습니다.
마찬가지로 Lisp 프로그래머는 드뭅니다. 보스턴 근처라면 거의 마법에 가까운 일을 해내는 노련한 해커들의 작은 풀이 있습니다. 다른 곳에는 호기심 많은 젊은 해커들이 드문드문 흩어져 있을 뿐입니다. 하지만 Lisp는 늘 소수 언어였습니다.
반면 Ruby는 인기가 빠르게 상승하고 있습니다. 큰 원동력은 Rails인 듯하며, 본격적인 가속은 2004년 말에 시작됐습니다. 회사를 시작하려고 한다면, 잠재적 직원마다 Rails에 푹 빠져 있다는 이야기가 거의 클리셰 수준입니다. Rails는 곧 일반적인 웹 컨설팅으로 스며들 것이고, 거기서부터는 언젠가 대기업으로도 들어갈 것입니다.
Ruby는 또한 좋은 표준 라이브러리를 갖출 만큼 충분히 오래 존재해 왔고, 부가 라이브러리의 거대한 아카이브도 발전시켰습니다. 웹 페이지를 내려받고, RSS를 파싱하고, 그래프를 만들고, SOAP API를 호출해야 한다면 이미 준비가 되어 있습니다.
이제 강력한 언어와 인기 있는 언어 중 하나를 고를 수 있다면, 강력한 쪽을 선택하는 것이 아주 훌륭한 판단일 수도 있습니다. 하지만 강력함의 차이가 작다면, 인기 있다는 사실은 온갖 좋은 장점을 가져옵니다. 2005년의 저라면 Lisp 대신 Ruby를 택할지 꽤 오래 고민할 것입니다. 아마 최적화된 코드가 필요하거나, 완전한 컴파일러처럼 동작하는 매크로가 필요할 때만 Lisp를 고를 것 같습니다.
(이 글의 초기 초안을 검토해 준 Michael Fromberger에게 감사드립니다.)
이 글에 대해 연락하기를 원하시나요? 아니면 다른 읽을거리를 찾고 있다면, 인기 글 목록도 있습니다.
Excellent Article님이 2005년 12월 03일에 작성:
Eric, 훌륭한 글 감사합니다. 저는 몇 년 동안 Lisp를 이것저것 만져 보았고, 그것이 더 강력하다는 데는 동의하지만, 당신이 든 여러 이유 때문에 제 사이트들은 Ruby로 만들기로 결정했습니다.
저는 늘 언어 도구 선택을 다시 평가하고 있고, 사이트를 Lisp로 만들지 않은 것이 실수였는지 궁금해하곤 했습니다. (지금도 여전히 개발 중이지만, 2006년 초쯤 Zifus를 눈여겨봐 주세요.)
저는 최근 reddit 개발자들이 언젠가 사이트를 다시 작성하는 것을 고려하고 있다고 말한 글을 읽었습니다. 그가 Lisp의 구체적인 어려움으로 든 것은, 다른 언어에서는 금방 등장하는 라이브러리들이 커뮤니티가 더 작기 때문에 Lisp에서는 더 늦게 나타난다는 점이었습니다.
그가 잠재적인 경쟁자들(저 같은 사람들)을 혼란시키려고 둘러댄 것인지는 모르겠지만, 결국 그건 중요하지 않습니다. 요점 자체는 타당하니까요. 반면 저는 프레임워크로서의 Rails의 발전에 엄청나게 만족했고, 이 멋진 프레임워크 덕분에 개인 생산성에도 완전히 감탄했습니다.
훌륭한 글입니다. 아직 Zifus에 올릴 수는 없어 아쉽지만, 곧, 정말 곧 가능할 겁니다.
zlxcgrogdss 님이 2005년 12월 03일에 작성:
당신의 다음 사설이 기대되는군요. 제목은 아마 “왜 Java는 괜찮은 Forth인가” 정도가 될까요. 운이 좋다면 “왜 어셈블리는 괜찮은 바이너리인가”도 가능하겠네요.
제가, 음, 넌지시 말하고 싶은 것은 Ruby는 Lisp가 아니라는 점입니다. Lisp가 Lisp인 이유는 코드가 그 기본 데이터 구조로 표현되기 때문입니다. Ruby는 그렇지 않습니다. (Ruby 코드가 문자열로 되어 있고, 문자열이 기본 데이터 구조라고 주장하지 않는 한 말이죠. 물론 그건 순전한 궤변이라는 데 동의하시겠지요.) 이 속성이 없는 Lisp는 둥글지 않은 바퀴와 같습니다.
이 차이와, 그로부터 따라오는 모든 함의 때문에, Ruby는 Lisp의 대체물이 될 수 없습니다. 제가 보기엔 당신은 Ruby를 “a Lisp”라고 잘못 부르면서 사실상 그 대체물이라는 뜻을 말하려던 것 같군요. Ruby는 멋진 언어지만, 너무 흥분할 필요는 없습니다.
bill a 님이 2005년 12월 03일에 작성:
저는 사실 거의 반대 경로를 탔습니다. Ruby를 버리고 Lisp를 택했지요.
좋은 지적들이 있고, 실제로 Ruby가 진짜 Lisp는 아니지만 가장 Lisp다운 언어라고는 생각합니다. (CL, Scheme, Dylan은 제외하고요.) 하지만 블록이 매크로에 놀랄 만큼 가까이 갈 수는 있어도, 끝까지 가게 해 주지는 못합니다. 당신이 지적했듯이 Ruby에서 매크로 비슷한 코드를 구현하면 내부적으로는 대체로 지저분해집니다. Ruby의 문법은 규칙적이지 않아서 매크로로 다룰 수가 없습니다. 그래서 원하는 일을 하도록 여러 추상화를 억지로 끼워 맞춰야 합니다. 블록에 전달된 코드를 리스트로 처리해 다른 것으로 단순히 변환하는 대신, 미니 언어의 요소들을 어떤 클래스의 메서드로 정의해야 하지요. 이때 Ruby의 OO가 지나치게 전면에 나서는 점이 번거롭습니다. (물론 많은 defmacro도 결국 지저분해지긴 하지만, 적어도 개념적으로는 더 깔끔합니다.) 그리고 사용자가 정의한 언어와 구조들은 전부 “do” 키워드로 구분되어야 하므로 바로 눈에 띕니다.
Lisp의 많은 점들은, 그 언어가 역시 Lisp가 아닌 이상 다른 어떤 언어도 절대 얻지 못합니다. Emacs 키 조합으로 sexp를 따라 이동할 수 있는 능력은 값으로 따질 수 없습니다. Lisp의 단순한 구조와 임의 문법의 부재 덕분에 훌륭한 편집 지원을 제공하기도 쉽습니다. 상호작용적 개발이라는 개념 전체도 대단합니다. 또 대부분의 구현에서 네이티브 코드로 컴파일할 수 있는 점도 훌륭합니다. Lisp는 꽤 성숙한 언어이고, 다소 특이한 점이 몇 가지 있긴 해도 50년의 역사는 프로그래밍 언어를 훌륭하게 만드는 데 큰 힘이 됩니다.
제가 Ruby에서 불편한 점은 그 특유의 자잘한 특성들입니다. Common Lisp에도 역사적 찌꺼기가 많기로 유명한데 그게 무슨 말이냐고 할 수도 있겠지요. 하지만 저는 임의 문법의 틀로 다시 머리를 맞추는 것이 정말 어렵습니다. 결국 그것은 쓸모가 없습니다. 매크로를 쓰지 못하게 하고, 편집기가 언어를 완벽하게 지원하지 못하게 하고, 필요한 만큼 간결하게 표현하는 것도 막습니다.
당신의 lambda 예제는 실제로 Lisp보다 Ruby에서 더 짧습니다. 하지만 이 상황에서 Ruby의 OO는 그렇게 깔끔하지 않습니다.
(defmethod sum-children ((c item)) (loop for child in (children-of item) sum (height-of child))) (mapcar #'sum-children list-o-children) 이건 Ruby에서는 그렇게 쉽게 되지 않는다고 생각합니다. 제가 생각할 수 있는 최선은 이렇습니다.
class Item def sum_children children.inject( 0 ) { |c, s| c.height + s } end end list_o_children.map { |c| c.sum_children } 자식들의 높이를 합산한다는 개념을 다른 데 넘길 함수로 (쉽게?) 다루는 것은 불가능합니다.
또한 블록은 호출 가능한 코드 이상이 아닙니다. 적절한 메서드를 정의하고 블록을 특별한 컨텍스트에서 평가함으로써 어느 정도 구조를 부여할 수는 있겠지만, 그것은 매크로가 제공하는 코드 변환과는 다릅니다. 예를 들어 서로 다른 표현식을 서로 다른 컨텍스트에서 선택적으로 평가할 수는 없습니다. 다음을 보세요.
(let ((x 45)) (defclass foo () ((name :initform x)))) 이 코드는 동작합니다. defclass가 initform만 lambda-expression으로 확장해서 x 변수를 클로저로 잡기 때문입니다. 다른 것은 그 컨텍스트에서 평가되지 않습니다. 또 중요한 점은, defclass가 단순한 매크로이고 let이 내장 special form임에도 둘이 전혀 다르게 보이지 않는다는 것입니다.
지금 멈추지 않으면 며칠이고 계속 쓸 것 같군요. let이 왜 멋진지, 왜냐하면 변수의 스코프를 절대적인 정밀도로 제한할 수 있기 때문이라든가(이건 Ruby에서는 불가능합니다), Marco Baringer가 순수 Lisp로 continuations를 구현한 이야기라든가 쓰려 했습니다. 요약하자면, 맞아요. Ruby도 그렇게 나쁜 건 아니고, 어쩌면 정말 “괜찮은” 수준일 수도 있습니다. 하지만 Lisp가 우위에 있습니다.
Eric 님이 2005년 12월 03일에 작성:
Lisp의 관점에서 볼 때 Ruby의 가장 성가신 특성은 대체로 스코프 규칙과 관련이 있습니다. 메서드, 변수, 상수는 각각 미묘하게 다른 동작을 합니다. 블록 로컬에 대해서는 Matz조차도 꽤 망가져 있다고 인정하죠.
Ruby 버전의 LET을 생각해 봅시다.
def let(*args) yield *args end let(1,2,3) {|x,y,z| z }
이 코드는 변수들이 바인딩되지 않은 상태라면 원하는 그대로 동작합니다. 세 개의 지역 변수를 가진 새 스코프를 도입하니까요. 반면 변수 중 하나라도 이미 바인딩되어 있었다면, Ruby는 기존 바인딩을 덮어써 버립니다. 좋지 않지요.
Lisp의 컴파일 타임 매크로 확장을 Ruby의 metaclass 해킹과 맞바꾸면서 생기는 가장 큰 한계는 시작 시간입니다. Rails는 FCGI와 온디맨드 클래스 로딩으로 이 문제를 피해 갑니다.
이런 골칫거리에도 불구하고, 저는 Lisp 매크로를 Ruby로 포팅하는 데 꽤 좋은 성과를 얻었습니다. 지금 시점에서는 _On Lisp_에 나오는 매크로의 절반 정도는 큰 어려움 없이 다시 구현할 수 있을 것 같습니다. 또 4분의 1 정도는 ParseTree를 쓰면 무식하게 밀어붙일 수 있습니다만, 그건 (논쟁의 여지는 있지만) 반칙일 수도 있겠지요. 앞으로 몇 달 동안 이런 기법들에 대해 더 이야기하겠습니다.
Anonymous 님이 2005년 12월 03일에 작성:
안녕하세요. 여기서는 당신이 LISP라고 쓸 때 항상 Common Lisp를 의미했다고 가정하겠습니다. (그쪽에서는 대문자로 쓰지 않습니다. PERL이나 JAVA도 그렇지 않듯이요.)
Before answering this question, we should decide what makes LISP so powerful. Paul Graham has written eloquently about LISP’s virtues. But, for the sake of argument, I’d like to boil them down to two things:
LISP is a dense functional language.
LISP has programmatic macros.
죄송하지만, 그건 현실을 반영하지 않습니다. 만약 그 두 가지가 Common Lisp를 쓰는 주된 이유였다면, 아마 대부분의 사람들은 쓰지 않았을 겁니다.
덧붙여 말하면 Paul Graham의 Common Lisp 스타일은 분명히 전형적이지 않습니다. (“Graham Crackers”로 검색해 보세요.) 그의 Scheme 배경과 애정이 분명히 드러나고, 보통은 Common Lisp를 그렇게 유용하게 만드는 다른 점들에 대해서는 잘 언급하지 않습니다. (Scheme와 달리 Common Lisp에서는 부작용을 피하고 반복 대신 재귀를 사용하는 일이 오히려 드문 편입니다.)
뿌리가 그렇다 하더라도, Common Lisp는 그다지 “함수형” 언어가 아닙니다. 필요한 기능은 모두 갖추고 있지만, 명령형 구성과 OOP가 언어에서 큰 비중을 차지합니다. Common Lisp Object System이 없다면 그것은 Common Lisp가 아니며, condition system과 다른 여러 요소들(완전한 수치 체계, 타입 시스템 등) 없이는 지금처럼 강력하지도 않을 것입니다.
Ruby가 함수형 측면에서는 CL과 맞먹을 수 있다는 점에는 의심이 없습니다. 하지만 그것만으로는 별로 멀리 가지 못합니다. Common Lisp는 정말 먼 길을 걸어왔고, 많은 사람들이 그것을 매크로와 고차 함수로만 축소해 보려 하지만, 그런 시각은 사람들이 실제로 Lisp를 사용하는 방식을 전혀 반영하지 못합니다. 그렇게 축소하면 그리 매력적이지도, 그다지 유용하지도 않습니다.
2005, I’d think long and hard before choosing LISP over Ruby. I’d probably only do it if I needed optimized code, or macros which acted as full-fledged compilers.
Ruby가 좋은 선택이라는 점은 확신합니다. 하지만 Lisp를 그저 매크로 때문에 쓰고 있다면, 아마 그 흥미로운 기능 대부분을 놓치고 있는 겁니다…
(궁금하다면 Practical Common Lisp는 매크로와 고차 함수만이 아니라 훨씬 넓은 범위를 다루는 좋은 무료 책입니다.)
null 님이 2005년 12월 03일에 작성:
여러분은 이해하지 못하고 있습니다… 여러분이 그렇게 아끼는 작은 언어들은 움직이는 표적입니다. 그런 언어들로는 20년을 버틸 코드를 절대 쓸 수 없습니다.
웹 페이지만 만드는 거라면 뭐, 괜찮을 수도 있겠지요. 하지만 데이터 통합과 AI가 웹으로 들어오는 순간까지만입니다.
그러면 Ruby, Perl, Python은 끝입니다.
Lisp에 필요한 것은 더 많은 사람입니다. Lisp 커뮤니티에 매번 새 유행마다 열광적으로 뛰어드는 사람들의 /절반/만 있었어도, 우리는 그런 라이브러리들을 이미 갖고 있었을 겁니다.
하지만 착각하지는 마세요. 우리는 이미 작은 CPAN 비슷한 것을 갖고 있고, 웹용 도구도 조금 있으며, 자유 소프트웨어도 계속 발전하고 있습니다.
Erik Enge 님이 2005년 12월 03일에 작성:
좋은 글입니다. 다만 Ruby가 왜 Lisp가 아닌지에 대해서는 zlxcgrogdss(두 번째 댓글)의 지적이 정확하다고 생각합니다. 어쨌든 “라이브러리가 충분하지 않다”는 문제는 해결하려는 시도가 진행 중이며, 궁금한 분들은 현재 상황을 http://common-lisp.net/projects.shtml 와 http://cliki.net/Library 에서 볼 수 있습니다.
이 댓글이 괜찮기를 바랍니다. 아니라면 자유롭게 지우셔도 됩니다.
Dookus Binglebib 님이 2005년 12월 03일에 작성:
But don’t kid yourself, we already have our mini CPAN
와, 벌써요?
eh 님이 2005년 12월 03일에 작성:
ruby에 prolog를 내장해 보세요.
Eric 님이 2005년 12월 03일에 작성:
Anonymous, 저도 CLOS가 좋은 것이라는 데 동의합니다.™ 특히 generic function은 전통적인 메시지 전달보다 훨씬 더 깔끔한 설계를 이끌어 냅니다. 이것은 제게 아주 중요한 주제입니다. 또 Common Lisp의 condition system을 짚어 주신 것도 맞습니다. 예외 처리를 다루는 우아한 접근이고, 미래의 언어 설계자들이 주의 깊게 연구할 만한 방식이지요.
그렇다 해도, 당신의 Common Lisp 스타일이 매크로에 크게 의존하지 않는다면, 재미의 절반을 놓치고 있는 셈입니다. 당신의 제일가는 판매 포인트를 포함해서 말이죠. :-)
Eric 님이 2005년 12월 03일에 작성:
Null, Dookus, 예의를 지켜 주시겠습니까? 뜨거운 논쟁은 괜찮지만, 서로를 존중하며 이야기해 주세요.
EH, 저는 몇 주째 Ruby 안의 Prolog 문제와 씨름하고 있습니다. 백트래킹은 쉽고, 괜찮은 패턴 매처도 돌리고 있습니다. 다만 규칙의 오른쪽 부분이 조금 까다롭군요.
Garrett Snider 님이 2005년 12월 04일에 작성:
글과 댓글 모두 감사합니다. 저는 오랫동안 Java를 써 온 사람으로서, 가끔 Lisp에 관심을 가져 왔고, Lisp 커뮤니티가 Ruby를 어떻게 생각하는지 궁금했습니다. 저는 Ruby를 Java보다 한 단계 위의 언어로 아주 좋아하고, 적어도 강력함이라는 측면에서는 Lisp에 상당히 가깝다고 생각해 왔습니다. (언어 설계 자체가 같다는 뜻은 아닙니다.) 다만 실용적인 차이가 무엇인지 더 깊이 이해하고 싶었습니다.
그리고 저는 위에서 bill a가 말한 “임의 문법”을 사랑하게 해 주는 약도 삼켰습니다. 다른 사람들을 설득하기는 어렵습니다. 그들은 PERL 대 Python의 대립 구도를 떠올리고 Python(/Java) 쪽에 서 있으니까요. 하지만 Domain Specific Language를 구현하는 방법으로는 Ruby의 열린 문법이 빛난다고 생각합니다. 제가 궁금한 점은 다음과 같습니다.
추가 문법 선택지가 단단하고 일관된 핵심 위에 층처럼 쌓여 있다면(Lisp처럼), 그것은 확실한 장점이며, 어떤 면에서는 이 점에서 venerable Lisp를 능가할 수도 있을 것 같습니다. (특히 저는 Executable DSL을 생각하고 있습니다.) 그래서 Ruby 언어의 근본적인 건전성과 일관성이 Lisp와 비교해 어떠한지에 대한 의견이 특히 흥미롭고 감사하게 느껴집니다.
bill a 님이 2005년 12월 04일에 작성:
좋습니다, 여러분…. Lisp에 대해서는 뭐든지 말씀하셔도 좋지만, 제발, 제발, 제발 더 이상 전부 대문자로 쓰지 마세요. Lisp지, LISP가 아닙니다.
jimmy 님이 2005년 12월 04일에 작성:
고마워요, Bill. 저도 그게 거슬리고 있었어요.
Smalltalking with a Lisp 님이 2005년 12월 04일에 작성:
Ruby는 주로 Smalltalk와 Lisp에서 영감을 받았습니다. 정말 놀라고 싶다면 그 둘을 살펴보세요. Ruby와 Python은 그 언어들로 가는 관문 언어입니다.
bill a 님이 2005년 12월 04일에 작성:
Ruby가 Lisp에서 얼마나 많이 왔는지 궁금하군요. 제 생각에는 Lisp에 익숙한 사람들은 돌아다니며 새 언어를 쓰는 편이 아닌 것 같습니다. :)
Unordinary Programmer 님이 2005년 12월 04일에 작성:
“어떤 사람들은 Ruby를 Lisp나 Smalltalk의 형편없는 베낀 물이라고 말할지 모르고, 나도 인정한다. 하지만 보통 사람들에게는 더 낫다.”
Anonymous Coward 님이 2005년 12월 04일에 작성:
흠. Lisp 매크로는 당신이 나열한 사소한 용례보다 훨씬 강력합니다. 여기서 예를 많이 들 수도 있겠지만, 스스로에게 이렇게 물어보세요. 왜 프로그래밍 커뮤니티 대부분은 “Design Patterns”를 그토록 좋아하는 반면, Lisp 커뮤니티는 대체로 그렇지 않을까요?
그 이유는 패턴이라는 것이 결국 코드의 고수준 명세에 불과하고, 같은 것을 계속해서 다시 쓰게 만들기 때문입니다. Lisp의 접근은 그 패턴을 실제로 구현하는 몇몇 매크로와 보조 함수를 만들어 언어의 능력을 확장하고, 바퀴를 계속 재발명하는 일을 피하는 것입니다.
이 일은 객체 지향(CLOS 기억하시나요?), aspect-oriented programming, OO<→SQL mapping 등에서 이미 일어났습니다. 모두 기본 언어 문법이나 구현을 건드리지 않고 말입니다.
Ruby의 기능이 특정한 경우에는 경쟁할 수 있을지 모르지만, 다른 언어들에도 같은 말이 적용될 것이라고 저는 생각합니다.
마지막으로 Lisp 라이브러리에 대해 말하자면, 언어 명세에 역사적 찌꺼기(예를 들어 지금은 죽은 버전 관리 파일 시스템에 대한 의무 지원)가 가득하고 표준 소켓 같은 것이 없는 것은 사실입니다. 하지만 그 빈틈을 메워 주는 자유로운 라이브러리가 꽤 있다는 것도 사실입니다. 맛보기로 http://cliki.net/ 을 둘러보세요.
Beowulf 님이 2005년 12월 04일에 작성:
여기 올라온 글들에 대한 꽤 긴 응답을 제 블로그에 적었습니다. http://zifus.blogspot.com/2005/12/power-vs-popularity.html
제 논리에 동의하지 않을 수도 있겠지만, 제가 시스템을 Lisp 대신 Rails로 만들기로 결정할 때 실제로 거친 사고 과정이 이것이었다는 점은 알아 주세요. 저는 Lisp를 사랑합니다. 하지만 실제 문제를 푸는 데 집중하는 대신 다양한 프레임워크를 억지로 붙이고 전체를 일관되게 돌리려 애쓰는 데 시간을 쓰고 싶지 않았기 때문에 쓰지 않았습니다. 제가 Java와 C#를 버린 가장 큰 이유도 바로 실제 문제에 집중하고 싶었기 때문이지, 언어와 프레임워크를 원하는 대로 움직이게 하려고 싸우고 싶어서가 아니었습니다. 이건 Lisp를 사랑하는 사람이 하는 말입니다. 언어 팬도 아니고, 존재조차 모르는 사람이면 무슨 생각을 하겠습니까.
Tayssir John Gabbour 님이 2005년 12월 04일에 작성:
Ruby의 창시자는 Lisp와 Smalltalk의 영향을 공개적으로 인정합니다. 그런데 Ruby의 성공이 Lisp 사용자들에게 나쁜 일이라고요? 특히 Smalltalk의 창시자 또한 Lisp의 영향과 존경을 말하는데 말이죠? ;)
제가 (아마 잘못) 기억하기로는, 움직이는 물체는 마찰계수가 더 낮아지는 경향이 있습니다. Ruby의 기술적 장점이 주류를 움직이게 한다면, 훌륭한 일입니다.
저는 이 글이 Java나 Python 세계에서 기억나는 것들보다 더 신중한 글이라고 생각합니다.
하지만 정직한 오류가 두 가지 있다고 봅니다. 하나는 Common Lisp를 함수형 언어라고 부르지 않겠다는 점입니다. 만약 그 말이 다른 기법보다 재귀를 선호한다는 뜻이라면요. 저는 반복을 많이 쓰고, Lisp의 LOOP 매크로는 이 점에서 꽤 인상적이며, 더 강력한 서드파티 반복 구성도 있습니다.
아마 Rubist들은 함수에 많은 힘이 있음을 정확히 보고 거기에 집중하는 것일 겁니다. 하지만 그 경우 그들은 Ruby 색안경을 끼고 Lisp를 보고 있는 셈입니다.
둘째, 순수한 매크로는 단지 어떤 시점, 예를 들면 “컴파일 타임”에 코드를 받아 코드를 돌려주는 함수입니다. sexp를 받아 sexp를 반환합니다.
그 목적은 무엇일까요? 대개는 표현력입니다. 가독성이지요.
그리고 매크로는 dynamic scope 같은 언어의 다른 측면을 활용할 수 있으므로, 언어의 표현력을 배가시킵니다. 하지만 힘에는 정당화가 필요하듯, 맛있게 써야 합니다.
저는 애초에 lambda를 그다지 많이 쓰지 않기 때문에, 제 매크로 사용이 lambda 타이핑을 줄이기 위한 것일 리가 없습니다. ;) 완전히 직교하는 개념입니다.
Anonymous (again) 님이 2005년 12월 04일에 작성:
Eric이 한 말에서:
That said, if your Common Lisp style doesn’t rely heavily on macros, you’re missing out on half the fun, not to mention your number one sales pitch.
좋습니다, 그 점에는 동의합니다. 제 첫 반응이 조금 성급했을 수도 있겠네요 :-)
하지만 제 요점은 여전히 같습니다. 재미의 절반보다 더 많은 부분은 자주 언급되지 않는 다른 부분들에서 나온다고 말하고 싶습니다. 매크로는 필수적인 부분이지만, 그 많은 요소 중 하나일 뿐입니다.
Beowulf에게: 말씀을 듣고 보니, 스스로도 조금은 쓰라린 전직 순교자 역할을 하고 있는 것처럼 들립니다. (의도된 공격은 아닙니다.)
여러 상용 Lisp 벤더들은 구현체가 싸지 않고, 활발히 개발되는 고품질 오픈소스 구현도 다양함에도 불구하고 잘 운영되고 있습니다. 그 사실만으로도 시장이 존재한다는 증거입니다. David Thornley의 말처럼 “Lisp가 평소보다 더 죽어 보이지는 않는다”는 것이죠. 사람들이 반대로 말한다고 해서 그게 바뀌지는 않습니다. (재미있는 건, 제가 FreeBSD를 쓰던 시절에도 “FreeBSD는 죽었다”는 말이 주기적으로 반복되는 전형적인 외침이었습니다. 이건 Lisp만의 일은 아닌가 봅니다.)
현대 언어들만큼 오픈소스 소프트웨어에서 널리 쓰이지는 않는다는 데는 동의합니다. 하지만 조금만 더 들여다보면 놀랄 만큼 자주 쓰이고 있습니다. 특히 당신이 푸는 문제가 어려울수록 더 그렇습니다.
대부분의 자유 Lisp 구현은 오래 실행되는 프로그램에 유리한 경향이 있습니다. (작은 초기 메모리 풋프린트나 작은 바이너리 생성에 초점을 두지 않습니다.) 그리고 언어의 장점은 문제가 어려울수록 더 큰 도움이 됩니다. 추상화는 프로그램 복잡성을 다룰 때 규모에 맞게 확장되지만, 문법적 지름길은 그렇지 않습니다. 아마 이것이 대부분의 Lisper들이 (lambda (…) …)를 더 짧게 쓰는 문법에 별로 관심을 두지 않는 이유이기도 할 겁니다. 그건 비사소한 프로그램에서는 큰 이득을 주지 않기 때문입니다. 다른 요인들이 훨씬, 훨씬 더 중요해집니다. (마이크로 벤치마크가 보통 별 의미가 없는 것도 같은 이유입니다. 하루 종일 fibonacci나 factorial만 쓰지 않는 이상 말이죠.)
You must occupy a niche if you will […]
아니요. (제 소견으로는) Common Lisp의 힘은 바로 _틈새에 집중하지 않는다는 점_에서 나옵니다. 그것은 힘에 집중합니다. 틈새를 점유하고 싶다면, Common Lisp를 쉽게 확장해 그렇게 만들 수 있습니다. 요즘 말로는 “자신만의 DSL을 쓴다”고 하죠. :-) Prolog-in-Lisp, Lisa, AllegroCache, AspectL은 그 몇 가지 예일 뿐입니다. (모두 일반 라이브러리로 구현된 것으로 압니다.) Common Lisp가 이렇게 오랫동안 살아남을 수 있었던 이유도 바로 그 확장성 때문이라고 확신합니다. Perl, Python, Java 등 다른 대부분의 새 언어들은 해마다 자기 자신을 다시 만들고 있습니다. 그렇지 않았다면 이미 다른 “경쟁” 언어에 밀려났을 겁니다. 대부분의 틈새는 움직이는 표적이고, 경쟁은 잠들지 않으니까요. 반면 Common Lisp _표준_은 오랫동안 바뀌지 않았고, 아마 앞으로도 한동안 바뀔 필요가 없을 것입니다.
물론 그렇다고 단순한 웹페이지를 PHP로 쓰는 것이 더 쉬울 수 없다는 뜻은 아닙니다. 빠른 한 줄짜리 작업에는 Perl이 훨씬 편할 것이고, Erlang은 원래 만들어진 영역에서는 Common Lisp를 쉽게 이길 겁니다. Common Lisp가 빛나는 순간은 그러나 당신의 문제가 _전형적인 틈새_로 덮이지 않을 때입니다. 문제가 어렵고 요구사항이 단순하지 않아 고급 기능을 실제로 활용할 수 있을 때입니다. 다른 언어에 당신이 원하는 일을 정확히 해 주는 라이브러리나 프레임워크가 있고 Common Lisp에는 없다면, 애초에 왜 Common Lisp를 쓰겠습니까?
저는 모든 것에 하나의 언어만 쓰는 것은 보통 좋은 징후가 아니라 나쁜 징후라고 생각합니다. (개발자의 유연성이 부족하다는 뜻이죠.) 어떤 언어도 모든 것에 최고는 아니며, 저는 그렇게 생각하는 사람들을 한 번도 이해하지 못했습니다. 그리고 만약 정말 그렇다면 삶은 몹시 지루할 겁니다.
Common Lisp에서 프레임워크를 사람들이 안 썼거나 안 쓴다는 것은 사실이 아닙니다. 다만 Common Lisp에서 여러분이 프레임워크라고 부르는 것이 대개 그냥 라이브러리라고 불릴 뿐이라고 생각합니다. 상업 세계에서 죽었다고 생각한다면, franz.com(상용 Lisp 벤더 중 하나) 하나만 보아도 그것이 틀렸다는 성공 사례와 인상적인 제품들이 충분히 있습니다. (현대적 예로 반지의 제왕 영화, Jak and Dexter, Orbitz 등을 들 수 있겠죠.) cliki.net / lispwire.com에는 오픈소스 라이브러리와 소프트웨어 링크가 잔뜩 있습니다.
Kent Pitman의 말을 인용하며 마치겠습니다.
“…Animation and Graphics, AI, Bioinformatics, B2B and E-Commerce, Data Mining, EDA/Semiconductor applications, Expert Systems, Finance, Intelligent Agents, Knowledge Management, Mechanical CAD, Modeling and Simulation, Natural Language, Optimization, Research, Risk Analysis, Scheduling, Telecom, and Web Authoring에만 Lisp가 유용하다고 생각하지 마세요. 그저 그들이 우연히 그것들만 나열했을 뿐입니다.”
Eric 님이 2005년 12월 04일에 작성:
Bill, Common Lisp에 대해서는 소문자 “Lisp”가 분명히 올바른 표기입니다. (덕분에 본문도 고쳤습니다. 감사합니다!) 하지만 대문자 표기도 오래되고 존중받는 역사가 있습니다. 새 표기가 눈에는 훨씬 편하지만, 제 LISP 선생님들은 다소 올드스쿨이었고, 저는 아직 그 향수를 완전히 극복하지 못했습니다. :-)
그리고 Lisp 해커들이 얼마나 자주 새 언어를 쓰느냐는 질문에 대해서는: 이상하게도 그것은 꽤심각한집착입니다요. 네, 알아요. 말이 안 되죠. 하지만 _H. sapiens_는 늘 완벽한 것을 가지고도 손대고 싶어 했으니까요.
Tayssir, Ruby가 프로그래머들로 하여금 Lisp(그리고 SmallTalk)를 제대로 들여다보게 만든다면 저는 정말 기쁠 겁니다. 그리고 Ruby는 일반적으로 메타프로그래밍에 분명히 좋은 소식입니다. 아마 이제는 매크로가 “너무 강력하다”는 말을 좀 덜 듣게 되겠지요. 개인적으로 아주 싫어하는 말입니다.
Ben 님이 2005년 12월 04일에 작성:
좋은 글입니다. 저는 Ruby 사용자는 아니지만, Common Lisp에는 없고 Ruby에는 있는 언어 기능 하나를 말하자면 first class continuations입니다.
또 Python이 CLR로 포팅하는 데 성공한 것을 보면, .Net용 Lisp 비슷한 언어를 진지하게 만들어 볼 생각을 한 사람은 없을까요? 뭔가 있긴 하지만 지원이 좋지 않거나 꽤 ML 스타일인 것들뿐인 것 같더군요. 나쁘진 않지만 Lisp와 정확히 같지는 않죠.)
Joku 님이 2005년 12월 04일에 작성:
이 글은 Python과 비교해 Ruby가 더 강력할지도 모른다는 전제에서 시작했으면서도, 실제로 Python에서 어떻게 할 수 있는지는 비교하지 않았습니다. 놀라운 일은 아니죠. 그렇게 했으면 Python 코드도 Ruby 코드와 거의 같은 복잡도로 쓸 수 있다는 결론이 나왔을 테니까요. (어떤 때는 조금 더 복잡하고, 어떤 때는 조금 덜 복잡하겠지요.) 그리고 Ruby도 Python도 Lisp의 매크로가 제공하는 힘에는 전혀 가깝지 않습니다.
저는 이 글에서 Ruby를 Python보다 고려해야 할 이유를 찾지 못했습니다.
bill a 님이 2005년 12월 04일에 작성:
Lisp와 Ruby의 큰 차이 하나는, Ruby는 동적 성질과 메타프로그래밍 능력을 얻기 위해 런타임 코드 평가를 많이 한다는 점입니다. Lisp에 익숙하지 않은 사람들에게는, 이것이 유일한 방법처럼 보일 수도 있습니다.
그래서 예를 들어 Lisp에서는 도메인 특화 언어의 일부를 정의하는 매크로를 쓸 수 있습니다. 그 매크로는 컴파일 타임에 다른 코드로 확장되고, 그걸로 끝입니다. 반면 Ruby는 훨씬 더 런타임 중심의 접근을 택합니다. 코드를 변환하는 대신, 어떤 블록이 평가될 때 그 블록이 호출하는 메서드에 접근 가능한 컨텍스트에 있도록 보장해 주면 됩니다. 그래서 Ruby는 실제로 그 코드가 실행되기 전까지는 읽은 코드에 대해 많은 가정을 할 수 없습니다.
두 접근 모두 장단점이 있지만, 저는 (당연히 ;-) Lisp의 접근이 더 깔끔하다고 생각합니다. 컴파일러가 정의되지 않은 함수를 호출하는 것에 대해 경고할 수 있고, 개발 환경도 코드 정의를 쉽게 찾아갈 수 있게 해 주기 때문입니다. Ruby 방식에서는 일반적으로 코드 조각의 의미론을 런타임에 가서야 알 수 있습니다. 물론 Lisp에도 eval 함수가 있지만, Lisp 자체 외부에서는 거의 쓰이지 않습니다. 이건 좋은 일입니다. Ruby를 쓰던 시절 저는 가끔, 코드에 대한 모든 결정이 마지막 가능한 순간에야 내려진다는 상황에 답답함을 느꼈습니다. 물론 그 덕분에 method_missing 같은 것도 할 수 있지만, 저는 Lisp로 옮긴 뒤에는 그것이 그립지 않았습니다. (말장난은 아닙니다.) 제 뜻은 Lisp의 모든 것이 C나 Java처럼 정적으로 준비되어 있어야 한다는 것이 아니라, 동적 성질에 대해 Lisp는 일반적으로 다른 접근을 취한다는 것입니다.
기억해 둘 또 하나의 차이일 뿐입니다.
Eric 님이 2005년 12월 04일에 작성:
멋지군요! Lython은 분명히 아주 근사한 해킹이고, 샘플 코드를 보니 저자도 Python의 바이트코드가 공식 문법보다 사실상 더 Lisp 같다는 점을 눈치챈 것 같습니다. Python의 if 문은 실제로 표현식입니다. 그저 파서가 중첩을 거부할 뿐이죠. 일반적으로 Python Language Services는 매크로 시스템을 구현하기에 꽤 유망한 기반으로 보입니다. 누군가 소매를 걷어붙이고 실제로 만들기만 하면 됩니다.
Bill, LISP와 컴파일 타임 대 런타임 성능에 대한 당신의 지적은 완전히 맞습니다. Exhibit A: Ruby on Rails는 새 서버 프로세스를 시작하는 데 몇 초가 걸립니다. 좋은 컴파일러가 있다면 LISP는 운영체제가 메모리에 페이지를 읽어들이는 속도만큼 빠르게 같은 일을 해낼 수 있을 겁니다.
장기적인 추세는 아마 LISP(혹은 Scheme)식 접근을 더 선호하게 될 것입니다. 주된 이유는 IDE 지원과 더 나은 컴파일 타임 검사 때문입니다. Ruby 접근은 팀이 단위 테스트를 유지하지 않으면 실제로 위험할 수 있습니다.
Beowulf 님이 2005년 12월 04일에 작성:
First Rest, 익명 댓글을 허용하도록 설정했습니다. 그걸 켜야 하는 줄 몰랐네요. :(
낮게 매달린 과일이 더 따기 쉽다는 당신 말에 동의합니다. 그래서 정확히 그 이유로 제 시스템에 Rails를 골랐습니다. 그렇다고 제가 떠났다는 뜻은 아닙니다. 이 시스템, 이 시점에서는 Lisp가 올바른 선택이 아니라고 느꼈다는 뜻입니다.
다른 분이 말했듯이 Ruby의 인기가 높아지는 것은 Lisp에도 좋은 일입니다. Java/C/C++ 패러다임보다 강력한 언어를 보게 만드는 모든 것은 Lisp에 좋은 일입니다.
Eric, 제 요점은 Lisp가 틈새 언어가 되어야 한다는 것이 아니었습니다. 대부분의 사람들 머릿속에서 Lisp는 이미 틈새 언어입니다. 그들에게는 AI 언어지요. 저는 Lisp가 더 인기를 얻으려면 사람들의 머릿속에서 차지할 새로운 자리를 찾아야 한다고 말하고 싶었습니다. 제 생각에 가장 좋은 방법은 Rails 같은 반짝이는 새 물건을 만들어 사람들이 가지고 놀게 하고, 엄청나게 홍보하는 것입니다.
쓴웃음 나는 전직 순교자라는 받아치기는 좋았어요. 적어도 웃게는 만들더군요. :-p.
저는 “Lisp가 x보다 낫다”는 논의를 수없이 봐 왔고, 직접 참여도 했습니다. 제가 본 것들은 전부 결국 너무 익숙한 문법 싸움으로 흘러갑니다. 저는 사람들이 자식 합산 문제의 해법이 Lisp에서 더 낫다는 이유로 언어를 바꾸지는 않을 거라고 생각합니다. 그건 핵심을 놓치는 겁니다. 사람들은 현실적이고 눈에 보이는 이점을 원합니다. 코드 3줄, 5줄짜리 작은 예제는 Lisp의 실제 힘과 표현력을 보여 주지 못합니다. 대부분의 사람들은 3줄짜리 예제를 실제 시스템으로 외삽하지 못합니다.
Rails가 Ruby 채택을 이끌고 있다는 점은 정말 좋은 소식이라고 생각합니다. 사람들이 새 언어를, 심지어 비교적 문법이 특이한 생소한 언어라도 실제로 받아들일 수 있다는 점을 보여 주니까요.
bill a 님이 2005년 12월 04일에 작성:
Beowulf, 저는 자식을 합산하는 해법이 Lisp로 옮겨야 할 이유라고 말한 적은 없습니다. Eric이 Ruby가 Lisp보다 더 “함수형”이고 “함수형 밀도가 높다”고 주장한 데 반박하고 있었을 뿐입니다. 당신이 그렇게 해석한다면 물론 “핵심을 놓치는 것”이 되겠지요.
현실적이고 눈에 보이는 이점이라고요? 좋습니다, 여기 있습니다.
매크로는 코드의 가독성, 간결성, 정확성을 높일 힘을 줍니다. LOOP와 ITERATE 매크로를 보세요. 흔한 반복 패턴들을 놀라울 정도로 단순하게 표현할 수 있는데, 이는 LOOP와 ITERATE가 코드 변환을 수행할 수 있기 때문입니다. 예를 들어 어떤 시퀀스의 원소들을 돌면서 절대값이 가장 큰 것을 반환하고 싶다고 합시다.
(iter (for x in seq) (finding x maximizing (abs x))) 이건 아주 멋지다고 생각합니다. 그리고 ITER 형식이 컴파일 타임에 엄청나게 빠른 코드로 확장되고(그리고 몇몇 LISP 구현에서는 기계어로 컴파일까지 됩니다), 그러면서도 강력한 추상화와 향상된 가독성을 공짜로 얻는다는 점을 생각해 보면 더 그렇습니다. 이 구성에는 런타임 효율이나 공간 낭비가 전혀 없습니다. ITERATE는 흔한 반복 패턴을 매우 빠른 코드로 바꿔 주는 효율적인 관용구를 갖고 있기 때문입니다. 더 빠른 코드를 직접 쓰면 가독성과 유지보수성이 엄청나게 떨어집니다. Lisp 매크로 덕분에 저는 그럴 걱정을 할 필요가 없습니다. 궁금한 분들을 위해, 이 ITER 형식의 macroexpansion은 이 댓글 맨 아래에 붙여 두었습니다.
더 많은 매크로 예제를 원한다면 Peter Seibel의 책 Practical Common Lisp를 보세요. (http://www.gigamonkeys.com/book 에서 무료로 볼 수 있습니다.) 마지막 장들 중 하나에서는 바이너리 파일 파싱 시스템을 만드는데, 정말 놀랍고, 그것도 매크로로 합니다. 앞부분에서는 테스트 프레임워크를 대략 26줄 만에 쓰는데, 역시 매크로를 사용합니다. 다음은 MP3의 ID3 태그를 파싱할 때 쓰는 코드의 예입니다.
(define-tagged-binary-class id3-tag () ((identifier (iso-8859-1-string :length 3)) (major-version u1) (revision u1) (flags u1) (size id3-tag-size)) (:dispatch (ecase major-version (2 'id3v2.2-tag) (3 'id3v2.3-tag)))) 이 코드는 파일에서 읽어야 할 데이터의 타입과 순서를 지정하고, major-version 값에 따라 다른 binary-class에 제어를 넘겨 줍니다. 심지어 major-version이 잘못되었을 때는 자동으로 에러도 던집니다.
상호작용적 개발은 또한 실행 중인 시스템에 접근할 수 있다는 뜻이기도 합니다. 어떤 함수가 어떤 파라미터를 받는지 잘 모르겠나요? “(func ”까지 입력하면 Lisp 환경이 뒤에 어떤 인자가 합법적인지 알려 줍니다. 가지고 노는 객체에 대한 정보가 필요하다고요? “(inspect x)”를 입력하면 상호작용적이고 재귀적인 inspector가 내부를 훑어보게 해 주고, 심지어 진행하면서 바꾸는 것도 가능합니다.
프로그램이 동작하기 시작하면, 약간 리팩터링하고, 벤치마크를 돌리고, Lisp의 완전히 선택적인 타입 시스템을 이용해 병목을 가속한 뒤, 편히 앉아 작업 결과를 감상하면 됩니다.
condition/restart system. 대부분의 언어에서는 예외가 발생한 뒤 할 수 있는 일이 많지 않습니다. 로그를 남기고, 사용자에게 보내고, 프로그램을 끝내는 정도죠. Lisp에서는 에러를 _다시 시작_할 수 있습니다. 예를 들어 UncommonWeb 프레임워크에서는 URL로 접속했을 때 에러가 signal되면 환경에서 백트레이스를 보게 됩니다. 이제 코드를 수정하고, 다시 컴파일한 다음, RESTART-REQUEST restart를 호출할 수 있습니다. 그러면 UCW가 처음부터 요청 처리를 다시 시작하고, 브라우저는 수정된 최신 코드의 실행 결과를 받습니다.
규칙적인 문법. Lisp에서는 모든 형식을 (operator [args])로 분해할 수 있습니다. 다른 것은 없습니다. 이것은 제가 매크로를 쓸 수 있게 해 줄 뿐 아니라, 편집기들도 Lisp가 규칙적이기 때문에 코드로 정말 멋진 일을 할 수 있게 해 줍니다. Emacs 키 조합으로 리스트의 위, 아래, 앞, 뒤, 안, 밖으로 이동할 수 있습니다. 직접 해 보기 전까지는 이 가치를 느끼기 어려울 수도 있지만, 정말 추천합니다. 제어 구조를 하나의 개체처럼 다루어 잘라내고 붙여넣을 수 있을 때, 코드 리팩터링은 아주 쉬워집니다.
거의 50년에 걸친 사용과 발전. Lisp는 FORTRAN을 제외한 모든 언어보다 오래 살아남았습니다. C, C++, Ada, COBOL을 이겨 냈고, Java와 Ruby도 견뎌 낼 것입니다. Paul Graham의 표현을 빌리면, “백 년짜리 언어”입니다.
그 외 더 많습니다!
이 모든 말을 하고 나니, 저도 사실 모든 직업적 프로젝트에 Ruby와 Rails를 쓴다는 점은 인정해야겠습니다. 그것이 Lisp가 준비되지 않았다고 생각해서가 아니라, 이 프로젝트들을 시작할 때는 제가 아직 Lisp를 배우지 못했기 때문일 뿐입니다. 지금은 그 점을 후회하고 있고, 다음 웹 애플리케이션은 Lisp로 만들 것입니다.
약속한 대로, 위 ITERATE 형식의 미친 듯하지만 효율적인 확장은 아래와 같습니다.
(LET* ((#:LIST6 NIL) (X NIL) (#:TEMP7 NIL) (#:RESULT5 NIL) (#:MAX8 NIL) (#:FIRST-TIME9 T)) (BLOCK NIL (TAGBODY (SETQ #:LIST6 SEQ) LOOP-TOP-NIL (IF (ATOM #:LIST6) (GO LOOP-END-NIL)) (SETQ X (CAR #:LIST6)) (SETQ #:LIST6 (CDR #:LIST6)) (SETQ #:TEMP7 (SQRT X)) (COND (#:FIRST-TIME9 (SETQ #:FIRST-TIME9 NIL) (SETQ #:MAX8 #:TEMP7) (SETQ #:RESULT5 X)) (T (COND ([snipped to prevent overflow -ed.]) (T #:RESULT5)))) (GO LOOP-TOP-NIL) LOOP-END-NIL) #:RESULT5))
Johnny 님이 2005년 12월 04일에 작성:
Ruby는 단순한 동적 언어나 스크립트 언어 그 이상입니다. 하지만 비웃고 싶다면 그렇게 하세요. 더 많은 “Rails 같은” 프로젝트들이 곧 나올 겁니다.
First Rest 님이 2005년 12월 04일에 작성:
Lisp는 당신이 Gosling이 되게 해 주고, Matz가 되게 해 주고, Guido가 되게 해 줍니다. 언어를 확장하고 싶다면, 실제로 가능합니다.
“DOLIST는 Perl의 foreach나 Python의 for와 비슷합니다. Java는 Java 1.5에서 JSR-201의 일부로 “enhanced” for 루프를 추가하여 비슷한 반복 구성을 넣었습니다. 매크로가 얼마나 큰 차이를 만드는지 보세요. 자신의 코드에서 흔한 패턴을 발견한 Lisp 프로그래머는 매크로를 써서 그 패턴에 대한 소스 수준 추상화를 스스로 만들 수 있습니다. 반면 Java 프로그래머가 같은 패턴을 발견하면, 그 추상화가 언어에 추가될 가치가 있다고 Sun을 설득해야 합니다. 그러면 Sun은 JSR를 발표하고 업계 전체의 ‘전문가 그룹’을 소집해 모든 것을 조율해야 하지요. Sun에 따르면 그 과정은 평균 18개월이 걸립니다. 그 후에는 컴파일러 작성자들이 모두 그 기능을 지원하도록 컴파일러를 업그레이드해야 합니다. 그리고 Java 프로그래머가 좋아하는 컴파일러가 새 버전을 지원한다 해도, 이전 Java 버전과의 소스 호환성을 깨도 되는 시점이 오기 전까지는 그 기능을 사용할 수 없는 경우가 많습니다. Common Lisp 프로그래머가 5분 안에 스스로 해결할 수 있는 불편함이 Java 프로그래머들에게는 수년간 이어지는 것입니다.”
—Peter Siebel, Practial Common Lisp에서
Garrett 님이 2005년 12월 04일에 작성:
고마워요 Bill. 정말 도움이 되는 내용이네요.
null 님이 2005년 12월 04일에 작성:
언급되지 않은 것들 몇 가지: meta-object protocol; sexpr (코드는 데이터이고, 데이터는 코드다); 상용 lisp의 경우 놀라운 IDE들.
Scheme에도 장점이 있습니다. 더 작은 언어라는 점, 즉 나머지 코드를 라이브러리로 옮긴다는 철학, 무료에 가깝게 C 수준으로 빠른 컴파일러(Bigloo), (자유 Scheme가 Lisp보다 IDE가 더 좋습니다), call/cc로 알고리즘을 프로토타이핑한 뒤 최종적으로 C 같은 명령형 언어의 효율적인 반복 알고리즘으로 옮길 가능성 등입니다.
마지막 예를 들자면, 오픈소스로 공개된 Common Lisp 기반의 오픈소스 컴퓨터 대수 시스템 두 개를 보세요. 1) 원래 IBM Thomas Watson Research Center에서 나온 Axiom, 2) 원래 미국 에너지부에서 나온 Maxima.
이 소프트웨어는 70년대에 작성된 것입니다. 움직이는 표적인 언어로는 이렇게 오래 살아남는 소프트웨어를 쓸 수 없습니다. 이런 종류의 생존력과 복잡한 도메인이 바로 Lisp의 힘을 보여 줍니다.
이 모든 스크립트 언어들은 사실 사람들이 언어 문법을 실험하는 데 관한 것입니다. 이것도 좋습니다. 하지만 그것을 있는 그대로 보세요. 게다가 그런 언어들 거의 대부분에는 이론적 기반도 없습니다. Lisp에는 있습니다. (lambda calculus) Smalltalk에도 있습니다. (Actors) ML에도 있습니다. (type theory) 이런 것이 좋고 오래가는 설계를 보장합니다. 극단적인 설계 실험의 예로 Perl6를 보세요. 어디로 가는지는 아무도 모르지만, 그래도 구현에는 Haskell을 쓰고 있습니다. (전 항상 Perl 사람들이 똑똑하다고 생각했습니다.)
Lisp는 70년대와 80년대에는 하드웨어를 많이 먹고 비쌌습니다. 지금은 그렇지 않습니다. 훌륭하고 반짝이는 자유 Lisp IDE는 아직 없지만, 적어도 한 벤더가 상대적으로 좋은 가격에 좋은 것을 팔고 있습니다. 그런데도 사람들이 Lisp를 쓰지 않는 것은 이상한 일입니다. 사람들은 Perl처럼 꽤 “거친” 것도 잘만 써 왔으니까요. 참고로 Higher Order Perl이라는 훌륭한 Perl 책이 있는데, 사실상 Lisp에서 많은 기법을 배워 Perl에 적용하는 내용입니다. 그 중 많은 것을 Ruby에도 적용할 수 있을 겁니다.
이 모든 스크립트 언어들이 하는 일은 문제를 만들고, 그 해결책을 다시 파는 것입니다. 하지만 공정하게 말하면, 스크립트 언어들은 Lisp가 하지 못했던 방식으로 프로그래머들에게 힘을 주었습니다. (“batteries included” 같은 것들이요.)
저는 Common Lisp와 Smalltalk의 문제가 배워야 할 것들의 크기라고 생각합니다. 문법적인 면에서요. 90년대에 스크립트 언어가 필요했던 어떤 일들은 빨리 무언가를 배워야 했습니다. (요즘은 모든 것이 더 복잡해졌지만 적어도 프레임워크는 있죠.) 또 Perl, Python 등은 이식성이 좋았습니다. 문법 측면만 보면 Scheme와 Eiffel이 택한 접근이 더 합리적이라고 생각합니다. (물론 Common Lisp와 Smalltalk가 큰 이유도 있긴 하지만, 저는 그것이 장애물이 된다고 봅니다.)
누가 알겠습니까, 어쩌면 사람들은 Ruby 때문에 Lisp를 시도할 수도 있겠지요. 하지만 되돌아와 기여하기보다는 좌절하는 사람이 더 많을 것 같습니다. Scheme도 해 보세요. 철학이 다르고 Common Lisp보다도 덜 주목받지만, 충분히 가치 있습니다.
bill a 님이 2005년 12월 04일에 작성:
아, 여기 댓글 수정 기능이 있었으면 좋겠네요. 오타가 더 있습니다.
“the bottom of this comment”인데 “the bottom of this common”이라고 썼네요.
“no extra space or time is wasted”에 “completely”가 하나 더 들어갔고요.
“making changes as you go” 대신 “even allowing you to make changes”가 더 낫겠군요.
그리고 iterate 예제를 더 명확하게 쓰면 다음과 같습니다.
(iter (for x in '(-1 2 -10 4)) (finding x maximizing (abs x))) ;; => -10 죄송합니다, 여러분. 큰 차이는 없겠지만, 이 댓글이 혼란을 조금 줄여 주면 좋겠네요.
bill a 님이 2005년 12월 04일에 작성:
null, 그건 좋은 조언이지만 Scheme에 익숙하지 않은 분들은 Common Lisp와 Scheme가 매우 다른 언어라는 점을 기억해야 합니다. 겉으로 비슷한 문법 때문에 그 차이가 잘 드러나지 않아, 어떤 사람들은 둘을 거의 바꿔 써도 될 정도로 생각합니다. 저는 개인적으로 Scheme를 좋아하지 않지만, Common Lisp는 단연 제가 가장 좋아하는 언어입니다. 그러니 Scheme를 접해 보고 마음에 들지 않았다고 Common Lisp까지 덩달아 치워 버리지는 마세요. (반대로도 마찬가지입니다.)
Lisp를 “이해”하려면 많은 노력과 사고의 전환이 필요하다는 데 동의하고, 그것이 Lisp를 가로막는 데 한몫했다는 데도 동의합니다. Peter Seibel의 Practical Common Lisp 책은 그 문제를 해결하는 데 큰 도움이 된다고 생각합니다. (정말 강력 추천합니다!)
Johnny 님이 2005년 12월 04일에 작성:
일본 자동차 회사들이 GM에 무슨 일을 했는지 기억하죠? :-) 크고, 강하고, 번쩍이는 것만으로는 단순함과 성실함을 이길 수 없습니다. Lisp 사람들은 Ruby 기능의 90%를 무시합니다.
bill a 님이 2005년 12월 04일에 작성:
한숨. Johnny, 이 토론을 읽기는 한 건가요? 무슨 이야기를 하는지 알고 있나요? 아니면 좋아하는 언어의 우월성이 의심받자 감정적으로 반사 반응을 보이고 있는 건가요? Lisp 사람들이 Ruby의 기능을 모른다고요? 그중 많은 기능이 Lisp에서 왔기 때문에 오히려 잘 압니다. 여기서 Ruby를 공격하는 사람은 없습니다. (적어도 저는 아닙니다.) 이 토론의 요점은 단지 Lisp가 대단하다는 것입니다. Ruby의 지리적 출신은 언어의 성질과 아무 관련이 없고, 믿기 어렵겠지만 Lisp의 많은 개념은 사실 Ruby보다 훨씬 단순합니다. Lisp 커뮤니티에 대해 그렇게 말하는 것은 별로 현명하지 않습니다. 당신은 분명히 Lisp를 모르니까요. 또 정확히 무엇이 “동적 언어”라는 점에서 문제입니까? 마치 욕처럼 그 표현을 쓰고 있는데요. Ruby는 동적 언어가 아니면 무엇입니까?
Beowulf 님이 2005년 12월 04일에 작성:
Bill, 정말 잘 쓴 댓글이었습니다. 제게 확실히 아픈 지점 중 하나는 매크로의 부재였습니다. 댓글과 점수 시스템의 여러 부분 사이에는 유사성이 충분히 많아서, 더 나은 추상화 방법을 정말 원했습니다. Rails에는 Partials와 Components라는 개념이 있어 설계를 추상화할 수 있지만, 상당한 성능 오버헤드도 함께 가져옵니다. 그 오버헤드 때문에 어떤 페이지들에서는 좋은 추상화를 포기할 수밖에 없었습니다. 너무 느렸으니까요. 반면 좋은 매크로 몇 개가 있었다면 같은 종류의 성능 손실 없이 우아한 수준의 추상화를 얻을 수 있었을 겁니다.
Practical Common Lisp(PCL)은 Lisp에 정말 중요한 책입니다. 제가 Lisp를 배울 좋은 책을 찾는 데 아주 오래 걸렸고(PCL이 나오기 전 일이었죠), 결국 절판된 80년대 책인 Winston and Horn의 Lisp로 돌아가야 했습니다. PCL 덕분에 드디어 인터넷 시대의 문제를 푸는 데도 적절한, 잘 쓰인 Lisp 입문서가 생겼습니다.
저는 진심으로 Lisp가 다시 인기를 얻었으면 좋겠습니다. 세상이 점점 더 강력함과 표현력의 방향으로 움직이고 있는 것 같거든요. 다만 그 속도가 꽤 빙하처럼 느릴 뿐입니다.
Lisp가 오래 버틸 힘이 있다는 점은 분명합니다.
하지만 Eric이 원문에서 말했듯이, Ruby는 정말 괜찮은 차선책입니다. Paul Graham은 Lisp를 쓸 수 없다면 Python을 쓰라고 한 적이 있습니다. 저는 Python을 아주 오래 써 왔고, 지금 시점에서는 분명히 Python보다 Ruby를 더 선호합니다. 언어 자체로 볼 때 Python보다 Lisp에 훨씬 가깝고, OO 문법도 훨씬 더 깔끔합니다. 그렇다고 해도 세 가지(Lisp, Ruby, Python)를 모두 아는 것은 나쁜 일이 아닙니다.
Eric 님이 2005년 12월 04일에 작성:
Johnny, Bill, 제발 예의를 지켜 주세요. 지금 좋은 토론을 하고 있으니, 누구의 모음도 빼앗고 싶지 않습니다. :-)
Bill의 지적은 아주 훌륭하고, 조금 더 풀어 쓸 가치가 있습니다. 그의 코드를 Ruby로 쉽게 번역할 수 있습니다.
class Array
def find_maximizing
result, f_result = nil, nil
each do |x|
f_x = yield x
if f_result.nil? || f_x > f_result
result, f_result = x, f_x
end
end
result
end
end
{|x| x.abs }
하지만 이 코드는 Bill의 것보다 훨씬 느릴 것입니다. iter 매크로가 컴파일러 안에 살고 있기 때문입니다. IDv3 파서도 마찬가지입니다. Ruby로도 보기 좋은 코드를 쓸 수 있겠지만, 성능에서는 완전히 밀릴 것입니다.
일 년쯤 지나면, 이건 Common Lisp의 꽤 괜찮은 판매 문구가 될 수도 있겠습니다. “Ruby 같은데, 하드웨어 속도로 실행됩니다!”
Johnny 님이 2005년 12월 04일에 작성:
Bill, Ruby와 Lisp 둘 다에 능숙하다고 주장할 수 있는 사람은 몇 안 됩니다. 문제는 당신이나 다른 사람들이 Lisp에서 쓸 기능이 Ruby가 보통 쓰이는 기능들의 부분집합일 뿐이라는 데 있습니다. 예를 들어 Ruby는 Bash, Sed, Awk, Perl 등을 대신하는 셸 프로그래밍에 아주 적합합니다. 저는 최근에야 Rake를 쓰기 시작했는데, 셸 프로그래밍용으로 정말 멋진 도구더군요. RubyC, mkmf.rb, setup.rb 등은 외부 라이브러리에 바인딩되는 Ruby 확장을 만들기 아주 쉽게 해 줍니다. Lisp에서 바인딩 만드는 일을 Ruby만큼 즐기는 사람이 많다고는 생각하지 않습니다.
그러니 당신이 Lisp로 하고 싶은 것이 WebApp 만들기뿐이라면, 좋습니다. 하지만 그 용도는 Ruby가 보통 쓰이는 범위의 일부일 뿐입니다. Lisp도 아주 강력하고, 정말 다양한 것들에 쓰인다는 점은 저도 압니다. 하지만 요즘 대부분의 사람들은 WebApp을 만들고 있지요. 그런데 Ruby는 웹사이트 처리만이 아니라 더 많은 일에 쓸 수 있습니다.
그래서 제가 사람들이 Ruby 기능 대부분을 모른다고 말하는 이유는, 그것들이 Lisp처럼 여러 사용 사례와 라이브러리에 흩어져 있기 때문입니다.
Code RESTful in Ruby!
Eric 님이 2005년 12월 04일에 작성:
Null이 이렇게 썼습니다: I think the problem with Common Lisp and Smalltalk is the size of the stuff you have to learn, syntax-wise.
저는 수년 동안 많은 비프로그래머들에게 Scheme를 가르쳐 왔습니다. 문법이 문제이긴 하지만, 좋은 편집기를 찾고 나면 그 문제는 줄어듭니다. (안타깝게도 Emacs는 많은 비프로그래머들에게 잘 맞지 않습니다.) 또 다른 큰 장애물은 보통 LET인데, 어떤 사람들에게는 중첩이 너무 깊게 느껴집니다.
더 큰 장애물은 _Lisp로 생각하는 법_을 배우는 일입니다. (혹은 SmallTalk, Haskell, 다른 강력한 언어들 모두 마찬가지입니다.) Bill이 훌륭한 매크로를 열두 개쯤 올려도, 대부분의 사람들은 “어?”라고 반응할 뿐입니다. 그것을 어떻게 쓸지 알기 전까지는, 그냥 이상한 기능일 뿐이니까요.
작은 문제를 해결함으로써 언어를 파는 것이, 심오한 새 기능을 제안하는 것보다 오히려 더 쉽습니다. Rails는 (1) 멋지고, (2) 문법이 너무 이상해 보이지 않기 때문에 사람들을 끌어들입니다. 그리고 사람들이 ActiveRecord를 한동안 써 보면, 메타프로그래밍이 왜 멋진지 이해하기 시작합니다.
Eric 님이 2005년 12월 04일에 작성:
Johnny, 비록 Lisp로 Makefile이나 셸 스크립트를 쓰는 사람이 많지는 않지만, 분명히 가능합니다. Lisp는 기본적으로 Rake 같은 프로그램을 만드는 도구 상자입니다.
그렇다면 왜 훌륭한 Lisp 프레임워크가 더 많이 보이지 않을까요? 주된 이유는 (아주 최근까지) Lisp 커뮤니티가 작고, 파편화되어 있었으며, CPAN이나 RubyGems 같은 것을 갖고 있지 못했기 때문입니다. 좋은 트릭들™ 대부분은 한두 번만 기록되었고, 책들도 절판되었습니다.
다행히도 지난 5년 동안 빠진 조각들 중 상당수가 나타났고, 나머지 대부분도 곧 따라올 것입니다.
timsuth 님이 2005년 12월 04일에 작성:
Eric: ‘def find_maximizing’
Ruby 1.9에는 ‘max_by’가 있습니다.
[1, 2, -3].max_by { |x| x.abs }
bill a 님이 2005년 12월 04일에 작성:
좋습니다, 매크로의 달콤함을 조금 더 잘 보여 주는 ITERATE 예제를 하나 더 보겠습니다.
(iter (for x in '(1 2 3 -4 -10 -43 49 49 8934)) (until (= (sqrt 7) x)) (collect x into collection) (finding x maximizing (abs x) into y) (finally (return (list collection y)))) ;; ((1 2 3 -4 -10 -43) -43) 이건 앞의 예제보다 훨씬 복잡하고, 여전히 그렇게 유용하지는 않지만, ITERATE가 생각할 수 있는 거의 모든 반복을 표현하기 위한 완전한 하위 언어를 제공한다는 점을 보여 주기를 바랍니다.
또 ITERATE는 사용자 정의 매크로로 확장할 수 있다는 점도 말하고 싶습니다. 예를 들어 저는 ITERATE와 함께 동작하는 AVERAGING이라는 매크로를 쓴 적이 있습니다.
(defmacro averaging (var) "ITERATE에 VAR의 평균을 구하는 절을 추가한다." (with-unique-names (count total) `(progn (summing ,var :into ,total) (counting ,var :into ,count) (finally (return (/ ,total ,count)))))) (iter (for x in '(1 2 3 4 5)) (averaging x)) ;; => 3 여기에는 더 많은 Lisp스러움이 들어가 있고, 그중 일부는 다소 난해해 보일 수도 있습니다. (덧붙이자면 이 예제는 Lisp의 docstring도 보여 줍니다. defmacro 줄 뒤의 그 문자열은 AVERAGING의 문서로 연결되므로, 런타임에 (describe ‘averaging)을 실행하면 그 문자열을 볼 수 있습니다.) 하지만 주목할 점은, 제가 반복에서 가장 흔한 개념을 표현하기 위한 확장 가능한 하위 언어를 다루고 있다는 것입니다. Eric과 timsuth가 든 예제는 first-class function이 있는 어떤 언어에서도 정의할 수 있겠지만, 이런 맞춤형 미니 구성을 정의할 수 있는 언어는 Lisp뿐입니다. 또 ITERATE가 새로운 코딩 구성을 제공하면서도, 그 안에서는 여전히 완전한 Lisp를 쓸 수 있다는 점도 보세요.
(iter (for x initially 0 then (1+ x)) (if (evenp x) (print x)) (when (= x 10) (return x))) 이 (인위적인) 예제에서는 첫 줄을 제외한 모든 것이 순수 Lisp입니다.
이런 종류의 일은 Ruby에서는 불가능합니다. 또 다른 예를 보죠.
(iter (for line in-file "foobaz" using #'read-line) (collect line)) 이 코드는 파일 “foobaz”의 각 줄을 리스트로 수집합니다.
(iter (for line in-file "foobaz" using #'read-line) (collect line at beginning)) 이 코드는 각 줄을 결과의 앞에 추가하여 역순으로 수집합니다.
(iter (for line in-file "foobaz" using #'read-line) (collect line at beginning result-type 'vector)) 이 코드는 줄들을 리스트가 아니라 벡터로 수집합니다.
이런 식으로 끝없이 계속됩니다. (혹은, 제 댓글들이 점점 반복되니 nauseam이라고 해야 할지도 모르겠네요.) 요점은 ITERATE의 작성자들이 만든 미니 언어가 다음과 같은 성질을 가진다는 것입니다.
절대값 기준 최대값 찾기, 어떤 조건에 따라 항목 수집하기 같은 비교적 복잡한 것까지 포함해 가장 흔한 반복 구성을 포착할 수 있다.
사용자가 확장 가능하다.
Lisp로 쓰였으며 Common Lisp 언어 자체의 일부가 아니다. 즉, 당신이나 제가 앉아서 ITERATE를 써도 아무도 그것이 언어의 일부가 아니라고는 모를 것이다.
빠르면서도 여전히 고수준이다.
매크로에는 ITERATE 말고도 훨씬 더 많은 것이 있지만, 규칙적인 문법과 매크로가 얼마나 강력한 힘을 줄 수 있는지를 보여 주는 예로 ITERATE만큼 좋은 것은 드물다고 생각합니다. 그리고 이 모든 것이 결국 빠르고 촘촘한 기계어 코드가 된다는 점을 기억하세요. 그래도 충분히 빠르지 않다면 타입 선언과 최적화 설정을 추가하면 됩니다.
(iter (for x in '(1 2 3 4 5)) (declare (type fixnum x)) (declare (optimize (speed 3) (safety 0) (debug 0) (compilation-speed 0))) (averaging x)) 이 댓글들이 Lisp에 호기심을 가진 사람들에게 도움이 되기를 바랍니다. 저도 이런 것들을 몇 년 더 일찍 알았더라면 좋았을 텐데요.
모든 코드는 테스트되지 않았습니다.
P.S. Eric이 여기서 예의를 지켜 달라고 한 요청은 감사하지만, Johnny, Lisp가 Ruby 기능의 부분집합만 가진다고 말한 것은 너무 심합니다. 사실은 완전히, 완전히 반대입니다.
bill a 님이 2005년 12월 04일에 작성:
좋습니다. 오늘 하루 대부분을 여기 댓글 다는 데 썼으니 이제 다른 일도 좀 해야겠지만, 마지막 예제의 최적화가 그 블록 안에서만 지역적으로 적용된다는 점에도 주목해 주세요. 저는 그게 정말 대단하다고 생각합니다.
그리고 매크로의 마지막 하나만 더 예로 들자면, AllegroCache는 Lisp용 상용 데이터베이스인데 Prolog로 질의할 수 있게 해 줍니다. 면책: 저는 Prolog를 잘 모르고, 본 예제도 기억이 희미하니 완전히 정확하지 않을 수 있지만 대략적인 느낌은 전달될 겁니다.
(<-- (parent ?x ?y) (db person ?x children ?c) (member ?y ?c)) (<-- (grandparent ?x ?y) (parent ?x ?z) (parent ?z ?y)) 여기서 <— 매크로는 뒤의 형식들이 Prolog로 해석된다는 뜻입니다. 즉 parent와 grandparent 관계를 Prolog를 사용해 정의한 것입니다. 둘의 관계를 대략적으로 선언하면, 그 위에 데이터베이스 질의를 할 수 있게 됩니다.
예를 들어 x라는 변수에 제가 들어 있다고 합시다. 그러면 ?- 라는 Prolog 질의 매크로를 이렇게 실행할 수 있습니다.
(?- (parent me ?x)) 그러면 AllegroCache는 children 목록에 제가 들어 있는 사람들의 이름을 데이터베이스에서 반환합니다. grandparent에 대해서도 같은 일을 할 수 있지요.
여러분, 저는 이게 지금까지 본 것 중 가장 멋진 것들 중 하나라고 생각합니다. 다시 말하지만, 이것들은 매크로이므로 모든 prolog는 컴파일 타임에 Lisp 코드로 축약됩니다. 하지만 정말 이런 멋진 것을 본 적이 있나요? 이것을 SQL로 하려면 얼마나 끔찍할지 상상할 수 있나요?
Lisp가 우위에 있습니다.
좋습니다, 진짜로 이제 갑니다. RandomHacks 블로그 말고도 인생에는 더 많은 것이 있으니, 그 일들을 하러 가야겠습니다. 제가 뭔가 도움이 되었기를 바랍니다.
Johnny 님이 2005년 12월 04일에 작성:
좋아요, Bill. “자동차” 비유는 사과합니다. Lisp 사용자가 Ruby에서 보통 가능한 것의 부분집합만 쓴다고 한 것도 사과합니다. 다만 세상이 가끔은 너무 점잖다는 생각이 들어서요. “생소한” 언어를 쓰고 아무도 말하지 않으면 전파하지 않은 죄가 있고, “생소한” 언어를 쓰는데 사람들이 말하기 시작하면 “과장했다”는 죄가 생깁니다. Rails 같은 혁명적 도구를 내놨을 때만 “과장”해도 된다는 것 같군요. 좋습니다. 미래에는 그런 것이 더 나오겠지요. 그때까지는 이만.
bill a 님이 2005년 12월 04일에 작성:
아니요, 댓글 하나만 더 있겠습니다.
우선, Prolog 절들이 실제로 심각한 코드로 직접 확장되지는 않습니다. AllegroProlog 함수 호출과 약간의 bookkeeping으로 확장될 뿐입니다. 제 실수입니다. 제가 그것들이 Prolog와 동등한 코드로 직접 변한다고 인상 준 것 같네요.
JAP 님이 2005년 12월 04일에 작성:
이 댓글은 LOL로 시작할 수도 있겠군요. 제안된 명제가 우리 중 많은 이들이 Lisp를 얼마나 깊이 오해하고 있는지를 너무도 완벽하게 보여 주기 때문입니다.
그러니 제 관점을 짧게 설명하겠습니다.
제가 Lisp에 오게 되었고, 또 절대로 떠나지 않게 만든 진짜 이유는 단순히 이것입니다.
7 (s-e-v-e-n)개의 원시 연산자로 그것을 만들 수 있기 때문입니다!
그리고 그 결과, 어떤 다른 언어도 자기 자신을 그렇게 짧게 표현할 수 없습니다.
(아시다시피 거의 모든 언어는 자기 자신으로 표현될 수 있습니다.)
그러니 다시 말하겠습니다. 어떤 다른 언어도 자기 자신을 그렇게 짧게 표현할 수 없고, 따라서 자연스러운 결과로 그보다 더 적은 원시 연산자로 구축될 수도 없습니다.
Lisp의 다른 모든 특징은 위 사실의 자연스러운 결과일 뿐입니다.
McCarthy는 다른 언어 설계자들처럼 Lisp를 “창조”한 것이 아니라, 그것을 “발견”한 것입니다!! (이 문장을 처음 읽는다면 두 번 더 읽어 보세요.)
진짜 요점은 이겁니다. 어떤 다른 언어도 동시에 Lisp보다 더 단순하고 더 유연할 수는 없습니다. BASTA!
JAP (그저 또 하나의 Paul [Graham은 아니고])
P.S. 우리 철학자들을 위해 덧붙이자면, 우리는 모두 함께 너무 복잡하고, 남은 생애 동안 더 단순해지는 데 정말 애를 먹고 있습니다. ;-)
Johnny 님이 2005년 12월 04일에 작성:
마지막 댓글 하나만 더: 어쩌면 “Lisp는 가장 알고리즘적인 언어”일지도 모르겠네요.
Haskell Junkie 님이 2005년 12월 04일에 작성:
매크로도 좋고 다 좋은데, Lisp 강경파들에게 잘못 이끌리지는 마세요. Haskell 언어를 배우면 DSL과 다른 온갖 기법을 그저 고차 함수만으로도 어떻게 하는지 보게 됩니다. Haskell을 배우는 데 3개월 정도만 투자해 보면, 대부분의 매크로 사용은 불필요한 해킹처럼 보이게 될지도 모릅니다. 누군가는 이미 Ruby에서 monad를 구현했을 거라고 저는 장담합니다. 동기 부여가 될 만한 흥미로운 예제로는 parser combinators와 backtracking이 있습니다.
Eric 님이 2005년 12월 04일에 작성:
Bill, Lisp 계열과 Prolog를 결합하는 데 관심이 있다면, The Reasoned Schemer가 Scheme에서 논리 프로그래밍을 소개하는 훌륭한 입문서라고 들었습니다.
Haskell Junkie, parser와 backtracking 논문 링크 감사합니다. Haskell은 이런 토론에서 자주 빠지곤 하는데, 아쉽습니다. 강력하고 독특한 언어니까요.
그리고 맞습니다. 누군가 Ruby에서 monad를 작업하고 있습니다.
다시 한 번, 이 토론을 친근하게 유지해 주신 모든 분들께 감사드립니다. 좋은 예의는 중요한 주제에 대해 뜨겁고 열정적인 논쟁을 더 쉽게 만들어 줍니다. :-)
bill a 님이 2005년 12월 04일에 작성:
Eric, 제가 결국 Lisp에서 Prolog를 배울 때를 대비해 PAIP를 가지고 있습니다. 저는 그저 데이터베이스에 복잡한 질의를 하는 데 그것을 쓸 수 있다는 점이 인상적이었을 뿐입니다.
Haskell Junkie, 예제를 좀 더 들려주실 수 있나요?
SB 님이 2005년 12월 04일에 작성:
“그러니 다시 말하겠습니다. 어떤 다른 언어도 자기 자신을 그렇게 짧게 표현할 수 없고, 따라서 자연스러운 결과로 그보다 더 적은 원시 연산자로 구축될 수도 없습니다.”
Forth는요. 혹은 Factor. 혹은 Joy. 여기서 무지해지지는 맙시다 :)
Ralph 님이 2005년 12월 05일에 작성:
저는 현재 PHP와 MySQL을 쓰는 평범한 사용자지만, FORTRAN과 Assembler, 그리고 IBM 1620 Machine Language 시절부터 이 업계를 지켜본 사람으로서, 여기 논의를 정말 즐기고 있다고 말하고 싶습니다. 어떤 면에서는 체스 선수나 예술가들의 대화처럼 들리기도 하네요.
언어의 “멋짐”, 우아함, 표현력은 모두 언어 설계자들에게 중요합니다. 저도 몇몇 작은 언어를 만들어 본 적이 있고, 그중 하나는 소규모로나마 실제 운영에도 들어갔습니다. 재귀 하강 컴파일러, 바이트코드 인터프리터, 애플리케이션 수준 멀티스레딩 등도 해 봤습니다. 기본은 이해하고 있습니다.
하지만 우리 대부분은, 우아함을 사랑하는 사람들조차도, 결국 총체적 노력으로 봤을 때 일을 가장 쉽게 끝내게 해 줄 것 같은 언어 환경 쪽으로 끌립니다. 다시 말해 흔한 작업이 읽기 쉬운 관용구로 표현되어야 한다는 뜻입니다.
잠시 자연어의 구어체를 생각해 보면, 그것은 본질적으로 관용구로 이루어져 있다는 점을 보게 될 것입니다.
글에서는 저는 이렇게 표현할 수 있습니다. “The ship did not contain any containers containing containers, but it did contain several containers containing parts used to construct containers” — 여기서 저는 영어의 약간의 규칙성을 바탕으로 조금 추상화합니다.
이 스레드를 읽어 온 사람이라면 제가 방금 쓴 내용을 곧바로 이해하는 데 거의 문제가 없을 것입니다.
하지만 구어에서는 위 내용을 명확하고도 간결하게 표현하기가 꽤 어렵습니다. 이 포럼에 자주 오는 사람들조차, 어떤 문맥이 미리 자리 잡지 않았다면, 그 생각을 구어로는 빠르게 표현하거나 이해하지 못할 수도 있습니다. 제 생각에 그 이유는, 그렇게 아주 조금만 복잡한 생각을 위한 정착된 관용구가 아마 없기 때문입니다. 그런 생각을 자주 표현해야 한다면, 어떤 형태로든 관용구가 즉시, 그리고 “자발적으로” 생겨났을 겁니다. 그러면 위 문장은 표현하기도 이해하기도 사소한 일이 되었겠지요.
문법적 설탕은 자주 수행하는 동작을 위한 관용구를 제공합니다. 그렇다면 당연히 일반성은 줄어들 수밖에 없습니다. 관용구는 원래 일반성을 줄이기 위한 것이니까요.
그러므로 Perl이나 심지어 (헉!) PHP를 쓴다고 “인정하는” 것은, 평범한 구어 영어를 쓴다고 인정하는 것과 비슷합니다. 하지만 우리 모두 실제로 그렇게 합니다.
그래서 저는 Lisp on Rails와 다른 온갖 멋진 것들, 그리고 그런 멋진 것들로 웹 페이지를 쓰게 해 주는 호스팅 회사를 간절히 기다리고 있습니다.
제발요. 그 “어려운 문제 해결” 능력을 실제로 이런 것들을 이루는 데 써 주세요. 그러면 저는 기꺼이 첫 번째 고객이 되겠습니다. 제 현재 호스팅 업체는 사용한 디스크 공간과 네트워크 대역폭에 따라 한 달에 대략 20달러 정도를 가져갑니다.
비꼬거나 무례하려는 뜻은 전혀 아닙니다. 마지막을 꽤 익숙하고, 심지어 날카로운 어조로 끝내 버린 것은 사실이고, 혹시 누군가의 신경을 건드렸다면 사과드립니다. 하지만 정말로 진지한 지점을 제기하고 싶었습니다.
Jules 님이 2005년 12월 05일에 작성:
이건 정말 좋은 토론이네요! 저는 Ruby 프로그래머지만 Lisp를 한번 들여다보고 싶습니다. 몇 가지 원리만으로 이루어진 단순함이 마음에 듭니다. 제가 이해하기로는 큰 Lisp가 두 개, Common Lisp와 Scheme가 맞나요? 어느 쪽을 배워야 할까요? 핵심 차이는 무엇인가요?
bill a 님이 2005년 12월 05일에 작성:
Jules, 저는 Common Lisp를 배우라고 하겠습니다. 다만 제 편향은 감안해야겠지요 :-). 앞에서 Practical Common Lisp 링크를 걸어 두었습니다. 온라인 장들을 조금 읽어 보고 마음에 드는지 보세요. 시작하기에 꽤 좋은 방법입니다.
또 MIT의 무료 책이자 컴퓨터 과학의 고전인 Structure and Interpretation of Computer Programs도 있습니다. 그 책으로 Scheme를 익힐 수 있고, Lisp류 언어가 어떻게 동작하는지도 더 깊이 이해하게 될 겁니다.
실제로 널리 쓰이는 세 번째 Lisp도 있습니다. Emacs Lisp요. 실제로 사용 중인 Emacs Lisp 코드 줄 수가 Scheme와 Common Lisp를 합친 것보다 많다는 말도 있습니다. 그래도 시작 언어로 Emacs Lisp를 권하고 싶지는 않습니다. 다소 특이하고, 또 다소 제약이 있으니까요.
어쨌든, 즐거운 lisping 하세요!
Eric 님이 2005년 12월 05일에 작성:
좋습니다, Common Lisp와 Scheme의 차이를 설명해 보겠습니다. 다른 분들이 제 지나친 일반화를 고쳐 주고 보완해 주시면 되겠지요. :-)
Scheme는 Lisp의 아주 깔끔한 방언으로, 함수형 프로그래밍에 중점을 둡니다. (위의 1부를 보세요.) 가장 인기 있는 구현 중 하나는 PLT Scheme이며, 종종 How to Design Programs와 함께 사용됩니다.
반면 Common Lisp는 더 나은 컴파일러와 상용 지원을 갖는 경향이 있습니다. Common Lisp 문화는 다양한 프로그래밍 스타일을 포용합니다. 함수형, 객체 지향, 명령형 스타일이 모두 흔하며, 매크로도 마찬가지입니다. (위의 2부를 보세요.) Common Lisp는 또한 Scheme보다 역사적 찌꺼기가 더 많습니다. 위쪽 댓글들을 보면 Practical Common Lisp를 좋아하는 것 같네요. 같은 사이트에서 Common Lisp 설치 파일도 내려받을 수 있습니다.
문화와 구현 차이를 제쳐 두면, Common Lisp와 Scheme는 거의 서로 바꿔 써도 될 정도입니다. 언제든 Scheme에 Lisp 기능을 더하는 매크로 패키지를 찾을 수 있고, 반대도 마찬가지입니다. 어느 쪽이든 훌륭한 선택이며, 둘 다 Ruby와 관련이 있습니다.
nemesis 님이 2005년 12월 05일에 작성:
Smalltalking with a Lisp:
“Ruby was inspired mainly by Smalltalk and Lisp”
Perl도 잊지 마세요! Ruby는 꽤 Perlish합니다…
Tayssir John Gabbour 님이 2005년 12월 05일에 작성:
Ralph, tech.coop은 Lisp 웹 호스팅을 제공하고, 그곳 사람 중 한 명은 “Lisp on Lines”(LoL)라고 농담처럼 부르는 무언가를 작업하고 있습니다. 다만 아직 일반적으로 쓸 준비가 되었다는 말은 듣지 못했습니다.
하지만 제가 Lisp를 “전도”하는 것은 아니라는 점은 기억해 주세요. 지금 이 순간에도 진입 장벽은 존재합니다. 그리고 저는 Common Lisp가 매우 강력한 도구라고 생각하지만, Lisp 옹호자들의 말을 그대로 믿어서는 안 됩니다. 그들은 Lisp의 장점을 설명하는 데는 능하지만, 현재의 단점들에 대해서는 충분히 이야기하지 않는 경우가 많습니다. (이 블로그 글에서 언급된 것들 같은요.) 그래서 오히려 오해를 줄 수도 있습니다.
물론 대부분의 옹호자는 사람을 오해하게 만듭니다. 하지만 저는 CL 사람들에게는 좀 더 높은 기준을 기대하고 싶습니다. ;)
SB 님이 2005년 12월 05일에 작성:
nemesis:
Ruby는 한때 Perlish했지만, 이제는 노골적인 Perl스러움($_ 같은 것)을 아무도 쓰지 않습니다…
Jamie 님이 2005년 12월 05일에 작성:
"But my code is going to run much slower than Bill’s, because the iter macro lives in the compiler. The same goes for the IDv3 parser: It would look fine in Ruby, but get absolutely clobbered on performance.
A year from now, this could actually be a pretty good sales pitch for Common Lisp. “It’s like Ruby, but it runs at full hardware speed!”
이건 사실 어느 언어에 대한 찬반 논거가 아닙니다. 그저 Ruby에는 현재 괜찮은 컴파일러가 없다고 말하는 것뿐이지요. 한때는 Lisp도 그랬고, 언젠가는 Ruby도 그렇지 않게 될 겁니다. 충분히 인기를 유지해 좋은 상용 컴파일러가 만들어질 동기가 생긴다면, 언젠가는 Ruby가 Lisp보다 더 나은 컴파일러를 갖게 될 수도 있습니다.
여기서 Lisp를 옹호하는 대부분의 논거는 매크로 기능에 관한 것 같습니다. 하지만 충분히 규칙적인 어떤 언어든 비슷한 기능을 가질 수 있습니다. 특히 모든 문법적 설탕이 환원되는 정규형이 정의되어 있다면 더 그렇지요. 그리고 그 설탕이 매크로 패턴에서도, 일반 코드에서도 쓰일 수 있다면 더 좋겠지요.
Lisp가 7개의 원시 요소로 이루어졌다는 주장은 사실 정확한 비교는 아닙니다. Ruby도 문법적 설탕을 처리하고 나면 몇 개의 원시 요소로만 이루어져 있습니다. 그건 Lisp의 reader macro와 다를 바 없고, 양쪽 모두 추가적인 입출력 원시 연산과 매크로 확장으로 구성된 수많은 부가 기능이 있다고 볼 수 있습니다.
이 토론에서 Lisp 예제들과 Ruby 예제들을 모두 보고 있는데, 실행 시간을 무시한다면(그건 단지 컴파일러 성숙도의 문제이니), 기능 면에서는 대략 동등해 보입니다. 다만 Ruby 쪽이 괄호가 적고 설탕이 더 많은 식으로 제시될 뿐입니다.
— Jamie (Lisp는 거의 쓰지 않고 Ruby는 한 번도 써 보지 않았지만, 컴파일러가 무엇을 할 수 있는지는 아는 사람)
Eric 님이 2005년 12월 05일에 작성:
Bill, 저는 Haskell 초보에 불과하지만 당신 질문에 답해 보겠습니다. A Gentle Introduction to Haskell에서 많은 예제를 찾을 수 있고, 저는 Glasgow Haskell Compiler를 써서 꽤 좋은 경험을 했습니다.
Lisp 관점에서 보면 두 가지가 눈에 띌 것입니다. (1) Haskell 변수는 한 번 설정되면 절대 바뀌지 않고, (2) Haskell은 lazy evaluation을 사용하므로 무한 데이터 구조를 쉽게 표현할 수 있다는 점입니다.
Lazy evaluation은 정말 온갖 멋진 일을 가능하게 합니다. 반복과 데이터 순회를 아주 아름다운 방식으로 표현할 수 있지요. (사실상 모든 것이 co-routine입니다.) 원한다면 모든 소수의 무한 리스트를 정의할 수도 있습니다.
하지만 이런 특징에는 큰 대가가 있습니다. Haskell 변수에는 할당할 수 없기 때문에, _모든 것_을 순수 함수형 스타일로 써야 합니다. 입출력조차도 바깥세계를 바꾸기 때문에 까다롭습니다. 그렇다면 실제 프로그램은 어떻게 쓸까요?
알고 보니 Haskell에는 두 번째 강력한 비밀 무기가 있습니다. 바로 monad입니다. Monad는 꽤 난해하지만, Lisp 식으로 말하자면 _제어 흐름_을 메타프로그래밍하는 방식처럼 생각할 수 있습니다. 가장 흔한 Haskell monad는 IO로, Haskell 문장들을 순차적으로 실행되게 하여 외부 세계와 상호작용할 수 있게 해 줍니다. 다른 monad는 할당, list comprehension, continuation, 그리고 도메인 특화 언어(위의 parser combinator 같은 것들)를 구현합니다.
Haskell은 어려운 언어일 수 있습니다. 흔한 프로그래밍 관용구 중 일부는 monad로만 표현되는데, 이것은 적어도 매크로만큼 어렵습니다. 하지만 Prolog와 마찬가지로 Haskell은 어떤 종류의 프로그램을 정말 아름답게 보이게 만듭니다. 그리고 물론 원한다면 Haskell의 많은 아이디어를 Lisp에 적용할 수도 있습니다.
매크로를 익혔고, 멋진 프로그래밍 스타일을 찾고 있다면, Haskell은 나쁘지 않은 출발점입니다.
bill a 님이 2005년 12월 05일에 작성:
Jamie, Lisp의 이런 특징은 Lisp 컴파일러의 성숙도와는 아무 관련이 없습니다. 대신 전적으로 Lisp의 규칙적인 문법과 관련이 있습니다. Ruby에 바이트코드 컴파일러조차 없다는 건 분명 안타까운 일이지만, 여기서 핵심은 그게 아닙니다.
Ruby의 코드는 단순한 “문법적 설탕”보다 훨씬 더 많은 것입니다. 그것은 완전한 형식 언어이며, 사용되기 전에 AST(abstract sy로 오타를 냈군요)로 번역되어야 합니다. 예를 들어 다음 Ruby 코드:
if foo.bar? and bar.baz? puts foo.to_s end 이것은 단지 다른 코드로 확장되는 것이 아닙니다. 무언가 유용한 것으로 바뀌기 위해서는 완전한 Ruby 파서가 필요합니다. 그래도 믿기 어렵다면, if 테스트 안에 임의의 표현식이 들어갈 수 있고, Ruby는 Lisp처럼 if를 표현식으로 다루며 아무 곳에서나 쓸 수 있다는 점을 생각해 보세요. 예를 들어 이렇게 쓸 수 있습니다.
if foo.bar? and bar.baz? and (89 + 44 + 54.0444).abs == 4 and (if x == 4; 56; nil) do_this end 이것을 AST로 쉽고 단순하게 번역해 주는 “문법적 설탕” 같은 것은 없습니다.
Lisp의 문법적 설탕은, 아마 당신이 직접 익숙하지 않은 것 같은데, 완전히 다릅니다. 예를 들어 #’foo reader macro는 단순히 (function ’foo)로 확장됩니다. 또는 ’foo 매크로는 (quote foo)로 확장되죠. Lisp reader macro는 다른 Lisp 형식에 대한 지름길일 뿐입니다. 가장 엄밀한 의미의 문법적 설탕이며, 아주 단순한 코드 변환밖에 하지 않습니다. 제 예제들이 보여 주었기를 바라듯, 이것은 일반적인 문법을 가진 언어와는 꽤 다릅니다.
“7개의 원시 요소로 만들어졌다”라는 말은 오해를 부를 수 있다고 생각합니다. 더 나은 표현은 7개의 원시 요소로 _기술될 수 있다_입니다. paulgraham.com에는 그 7개 연산자만으로 완전한 Lisp evaluator를 구현하는 코드가 있습니다. 인쇄된 페이지 한 장 정도 분량입니다. 그러니 말할 수 있는 것은, Lisp의 의미론을 7개의 원시 요소로 완전히 설명할 수 있다는 것입니다. 그것도 여전히 멋진 사실이지만, 위에서 주장된 것과는 다릅니다. 언어를 실제로 만들려면 문자열을 받아 리스트로 바꾸는 함수와, I/O 같은 유용한 것들도 필요합니다. (참고로 Paul Graham의 코드는 여기 있습니다: http://lib.store.yahoo.net/lib/paulgraham/jmc.lisp).
Ruby에 Paul Graham의 코드와 동등한 것을 보여 줄 수 있다면, 저는 논쟁을 접겠습니다. 하지만 그럴 수 없고, 실제로도 불가능합니다. Ruby는 그렇게 동작하지 않기 때문입니다. Ruby에는 문법이 있고, 그 문법을 AST로 바꾸기 위해 완전한 파서가 필요합니다. Lisp 코드는 원래부터 AST이기 때문에, 상황이 완전히 다릅니다.
또 Ruby를 더 규칙적인 언어로 변환할 수 있다면, 거기서 매크로를 쓸 수 있다는 점도 맞습니다. 실제로 Ryan Davis의 ParseTree 패키지로는 아마 가능할 겁니다. 하지만 이제 매크로는 순수 Ruby 코드 대신 어떤 중간 형식을 입력과 출력으로 다뤄야 합니다. 그 중간 형식은 AST의 일종일 것이고, 실제 Ruby와 그 중간 형식 사이의 대응은 관리하기가 거의 틀림없이 골치 아플 겁니다. 그렇다면 애초에 문법을 버리고, 모든 코드가 자연스럽고 자동으로 AST인 Lisp 모델을 쓰는 편이 낫지 않을까요? 거기서는 매크로가 그냥 따라오니까요.
Haskell Junkie 님이 2005년 12월 05일에 작성:
bill,
여기에 Parsec 라이브러리를 사용한 간단한 산술 파서와 평가기의 예제가 있습니다. 그리고 아래는 비결정적 프로그램의 작은 예입니다. 예를 들어 8633이 되도록 곱해지는 숫자 쌍을 찾고 싶다고 해 봅시다. 물론 이게 가장 효율적인 방법은 아니지만, 보기 좋고 선언적으로 보이기는 합니다…
-- 2와 100 사이의 인수 조합을 시도하고 틀린 것을 걸러낸다 import Control.Monad main = print ( do a <- [2..100] b <- [2..100] if a*b == 8633 then return (a,b) else mzero )
…새로운 건 아닙니다. “amb” 연산자가 있는 Lisp라면 매크로 없이도 할 수 있는 종류의 것이죠. 하지만 이게 동작하게 해 주는 list monad가 7줄 코드로 구현된다는 점은 멋집니다.
bdw 님이 2005년 12월 05일에 작성:
사실 저도 같은 생각을 해 본 적이 있습니다. Ruby가 ‘lisp’인 이유는 더 밀도 높은 함수형 언어이기 때문이 아닙니다. Ruby는 사실 함수형 언어가 아닙니다. Lisp는 그렇지만요.
Ruby가 ‘새로운 Lisp’가 될 수 있었던 것은(‘yellow is the new black’ 같은 식으로 말입니다) 만약 그것이 조금만 더 일관적이었더라면 그랬을 겁니다. 예를 들어
loop { obj.meth }
대신 이렇게 했어야 했습니다.
loop.obj.meth
그러면 Consistently Object Oriented™가 완성되었겠지요.
보세요, Lisp의 진짜 훌륭한 점은 매크로가 아닙니다. 물론 매크로도 좋습니다.
대신 Lisp가 제공하는 진짜 두 가지는 다른 언어들에는 없는 것입니다. 요약하면:
1): Lisp에서는 모든 것이 함수입니다. ‘If’도 함수이고, ‘cond’도 함수이고, ‘defun’도 함수입니다. ‘and’와 ‘or’도 논리 연산자가 아니라 함수입니다. 그것들도 값을 반환합니다. 이런 관점이 있으면 아주 강력하고 밀도 높은 코드를 쓰는 것이 가능해집니다.
2): Lisp에는 문법 규칙이 하나뿐입니다. (reader macro는 제외하고)
(function arguments). 어떤 인자도 함수일 수 있습니다. 이것은 단순하고 일관적이며, 항상 이럴 것이라고 기대할 수 있습니다. 머리를 뒤틀리게 하고 사람을 미치게 만들 수 없다는 뜻은 아닙니다. 하지만 이것이 유일한 문법입니다. Ruby가 조금만 더 일관적이었다면 진정한 ‘새로운 Lisp’가 되었을지도 모르지만, 지금은 최신의 새로운 OO 언어일 뿐입니다.
오해는 마세요. Ruby는 아주 좋은 언어이고, 문법도 좋고(완벽하진 않지만), 커뮤니티와 라이브러리도 훌륭하며, 빠른 웹앱을 만들기에는 분명 훌륭합니다.
하지만 그것은 전혀 새로운 Lisp가 아닙니다.
Eric 님이 2005년 12월 05일에 작성:
BDW, 당신이 언급한 연산자들(if, defun 등)은 기술적으로 함수가 아니라 “special form”입니다. 모든 인자를 반드시 평가하는 것이 아니기 때문이죠. 그 점을 제외하면 요지는 좋습니다.
Bill, 여기 제 미공개 라이브러리와 ParseTree에 기반한 Ruby 코드가 있는데, 아주 말도 안 되는 수준까지는 아닙니다.
assert_body(call(lit(2), :+, lit(3))) do
2 + 3
end
괄호 안의 표현식은 parse tree를 만들고, assert_body는 그것을 블록의 AST와 비교합니다. 이것은 Paul의 예제를 재현하는 작은 한 걸음입니다.
Jamie, 여러 Lisp 컴파일러에 조금씩 기여해 본 사람으로서 말하자면, Ruby가 Common Lisp만큼 빨라질 거라고는 생각하지 않습니다. 큰 장애물이 두 가지 있습니다.
(1) Ruby 메타프로그래밍은 런타임에 일어나지만, Lisp 매크로는 컴파일러 안에서 돌아갑니다. 런타임 비용이 0인 것을 이기기는 어렵습니다. :-)
(2) Ruby는 duck typing과 method_missing 같은 여러 동적 기능을 지원하는데, 이런 것들은 Lisp의 대응 기능보다 최적화하기가 훨씬 어렵습니다.
물론 최고의 SmallTalk VM들은 아주 뛰어나고, 가상의 Ruby VM도 거의 모든 애플리케이션에 충분할 정도는 될 수 있을 겁니다.
bill a 님이 2005년 12월 05일에 작성:
Eric, 그거 멋지군요. ParseTree가 결국 그런 일을 할 수 있으리라고는 예상하고 있었습니다. 그래도 중간 형식으로 매크로를 쓰는 것은 Lisp 매크로보다 편의성과 유지보수성 면에서 훨씬 떨어진다고 생각합니다. ParseTree 인터페이스가 바뀌면 어쩌죠? 새 버전의 Ruby가 나와 모든 것을 깨뜨리면 어쩌죠? 저는 여전히 Lisp 쪽에 남겠습니다. :)
PleaseDontKillMe 님이 2005년 12월 05일에 작성:
참고: 원래는 위의 Zifus 블로그에 이 글을 올렸습니다. 완결성을 위해 여기에 다시 올립니다. 저를 트롤이라고 생각하셔도 되고, 어느 정도 진실을 담은 문서로 읽으셔도 됩니다.
[과도한 욕설로 인해 생략. 예의를 지켜 주세요. -편집자
원래 댓글은 Zifus 블로그의 “I think most ‘dead’ or ‘dying’ languages…”로 시작하는 부분에서 볼 수 있습니다.]
bill a 님이 2005년 12월 05일에 작성:
좋습니다. 겨울방학이 2주 뒤에 시작합니다. 저는 Lisp on a Ladder를 쓰고, 문서화하고, 공개하겠습니다.
약속합니다.
그럼 반대하는 사람들도 만족할까요?
Eric 님이 2005년 12월 05일에 작성:
Bill, ParseTree 인터페이스가 바뀌면 제 라이브러리 내부를 다시 해킹해서 테스트 케이스가 통과할 때까지 손볼 겁니다. 위 코드는 ParseTree 자체가 아니라 제 고수준 API를 상대로 작성된 것이니까요.
진짜 위험은 Ruby 2.0에서 VM이 바뀌는 것입니다. 그러면 ParseTree는 사실상 끝장이 나겠지요. 누군가 지금 당장 엄청나게 멋진 것을 만들어 놓는다면, 미래의 VM에서도 ParseTree가 살아남는 데 도움이 될지도 모르겠습니다. 하지만 프로덕션 시스템을 위한 좋은 논거는 아니겠네요. :-/
Johnny 님이 2005년 12월 05일에 작성:
(알아요, 알아요…)
동의합니다, 제 “PleaseDontKillMe” 친구. 프로그래밍은 목적 자체가 아니라 수단입니다. 알고리즘을 손질하는 것이 프로그래밍의 목적은 아니죠. 프로그래밍은 어렵기 때문에, 무료든 상용이든 받을 수 있는 모든 도움을 받아야 합니다! 사람들을 “힘든 포스의 편”으로 초대해서는 절대 성공하지 못합니다.
Mike 님이 2005년 12월 06일에 작성:
글에서는 python을 언급하고, 그러고는 ruby로 넘어가셨네요. Ruby는 훌륭한 언어입니다.
저는 80년대 후반 Lisp로 시작했고, 90년대 초반에는 python으로 갔습니다.
Python은 예전부터 풍부한 함수형 지원을 기본으로 갖고 있었습니다.
lambda
map
apply
Python 2.2 (현재는 python 2.4)
에 추가된 것:
– iterators
Python 2.4는 generic programming을 단순화하는 decorators를 추가했습니다.
여기 python as Lisp 관련 흥미로운 자료가 있습니다.
Lisp와 Python 비교에 대한 좋은 글
http://www.norvig.com/python-lisp.html
Functional Programming in Python
http://www-128.ibm.com/developerworks/linux/library/l-prog.html
http://www-128.ibm.com/developerworks/linux/library/l-prog2.html
MultiMethods:
http://www-128.ibm.com/developerworks/linux/library/l-pydisp.html
Generic Programming:
http://www-128.ibm.com/developerworks/library/l-cppeak2/
Eric 님이 2005년 12월 06일에 작성:
decorators 링크 감사합니다. 특히 그 접근은 아주 좋네요. Ruby는 이 부분을 조금 덜 우아하게 처리합니다.
Python의 lambda와 map에 대해, 그리고 왜 그것들이 생각만큼 큰 도움이 되지 않는지에 대해서는 앞선 토론을 보세요.
Brad 님이 2005년 12월 06일에 작성:
책을 절반쯤 읽고 나서 든 생각은, Ruby는 대체로 Smalltalk와 거의 같은 언어처럼 보인다는 점입니다. (단, Ruby의 메타프로그래밍 계층은 제가 잘 모릅니다.) 다만 문법이 훨씬 많이 추가되어 있죠.
Ruby 코드를 읽으면서, 약간만 손보면 Smalltalk로 바꿀 수 있겠다는 생각이 듭니다… 그리고 제 눈에는 Smalltalk 쪽이 훨씬 더 우아해 보입니다.
<a Common Lisp and Smalltalk fan>PleaseDontKillMe 님이 2005년 12월 06일에 작성:
Eric, 실망스럽습니다. 원래 제 의도는 트롤링이 아니었으니 공격적인 단어들만 지우셨으면 됐을 텐데요.
하지만 여긴 당신 블로그이고, 그건 괜찮습니다.
그래도 제가 한 말 중에는 불편한 내용들이 있었고, 당신이 댓글 전체를 통째로 뺀 것이 그 불편한 진실들 때문이 아니었기를 바랍니다. (제가 말하는 건 “F”로 시작하는 단어들이 아닙니다.)
이것 역시 제가 말하려 했던 점 하나를 잘 보여 줍니다. 우리 마음속의 현상 유지에 도전하는 아이디어를 대할 때의 불필요할 정도로 거만한 태도 말입니다.
“과도한 욕설”에 대해서는 사과드립니다.
제가 더 “불경하다”고 느끼는 것은, supposedly “inferior” 언어들이 생존 경쟁(사실상 mindshare 경쟁)에서 우리를 휙휙 추월해 지나가는데도, 우리는 계산적 배꼽의 아름다움만 이야기하고 있다는 점입니다.
아무리 1억 년 동안 가장 날렵한 종이었다 해도, 겸손한 뒤쥐와 경쟁할 수 없다면 결국 지는 건 당신입니다. 반짝이는 비늘과 날카로운 이빨이 아무리 많아도 소용없습니다!
언어는 화자를 빚고, 화자는 언어를 빚습니다. Latin과 Sanskrit가 어쩌면 ‘최고의’ 언어일지 몰라도, 세상이 왜 더 이상 그것들을 쓰지 않는지 저는 궁금합니다.
제가 Lispers/Forthers/Smalltalkers에게 던진 질문은 이렇습니다.
당신들은 세계의 Sanskrit가 되고 싶은가요? 오직 대사제들만 쓰고, 그 대사제들이 존재하는 한에서만 간신히 살아남는 언어요?
아니면 세계의 lingua franca, 그러니까 (예를 들면) English가 되고 싶은가요? 대중이 매일매일 삶을 굴려 가기 위해 쓰는 언어요?
선택은 당신들의 몫입니다. 정말로 그 언어를 “사랑”한다면, 근시안적인 짓(클로즈드 소스, 난독화, 쓸데없는 현학)은 하지 말고, 당신이 말하는 그 “사랑”을 돕는 일(오픈소스화, 도움 주기, 대중 침투를 위한 킬러 앱 만들기)을 해야 합니다.
킬러 앱이라고 해서 저는 그 언어의 또 다른 방언(혹은 엄청나게 비싼 상용 소프트웨어)을 말하는 것이 아닙니다.
어쨌든, 생각할 거리를 던지는 글을 써 주셔서 감사합니다.
::AAEBACMD::
Jim 님이 2005년 12월 07일에 작성:
bill a,
제발 Lisp on a Ladder를 만들어 주세요.
멀티플랫폼으로 만들 수 있나요? 여러 구현체에서 돌아가게 할 수 있을까요? 노력이 중복되는 파편화가 너무 많습니다. 아름다운 언어인 Lisp가 mindshare를 얻으려면 무엇이 필요할까요?
Lisp 커뮤니티에는 더 많은 개발과 협업이 필요합니다. 더 흥미로운 일들이 벌어져야 합니다!
bill a 님이 2005년 12월 07일에 작성:
Jim: 네, 가능한 한 최대한 멀티플랫폼, 다중 구현체를 목표로 해야 할 것입니다. CLISP에서 테스트하는 것도 꼭 하겠습니다. CLISP에서 돌아가는 것은 Linux, FreeBSD, OS X, Windows 등에서도 돌아가니까요.
또 프로젝트의 일부로 튜토리얼 비디오도 공개할 계획입니다. xvidcap이 녹화 버튼을 누를 때마다 죽는 문제만 해결되면, Lisp 기초부터 Emacs + SLIME 사용법, Lisp on a Ladder 프레임워크의 구체적인 내용, 그리고 말도 안 될 정도로 짧은 시간 안에 장난감 애플리케이션(예를 들어 블로그)을 만드는 법까지 다양한 주제를 다룰 생각입니다. 이를테면 4분 같은 시간이요.
이 작업을 하면서, 많은 사람들이 던지는 질문의 답을 깨달았습니다. 왜 Lisp에는 Ruby on Rails에 해당하는 것이 없을까요? 제 생각에 답은, 대부분의 프레임워크는 사용자가 비슷한 구성을 반복적이고 지저분하게 타이핑하는 일을 줄여 주기 위해 존재한다는 것입니다. 매크로가 있으면 그런 필요성이 덜합니다. 또 SLIME 같은 환경이 매크로나 함수가 어떤 매개변수를 받는지 정보를 주기 때문에, 이 점도 덜 중요해집니다.
어쨌든, LoaL에 넣어 볼까 생각 중인 기능은 다음과 같습니다.
사용자가 선택하면 LoaL이 데이터베이스 스키마를 자동 관리합니다. (데이터베이스 전체 혹은 개별 테이블 단위로 켜고 끌 수 있습니다.) 그래서 모델 코드에 변화를 주면, LoaL은 스키마가 실제 테이블과 맞는지 확인합니다. 맞지 않고, 사용자가 production 모드가 아니라면, 데이터가 든 컬럼을 지우는 경우 등을 주의 깊게 경고하면서 모델에 맞게 스키마를 조정할지 제안합니다.
뷰 템플릿을 컴파일해서 사용자에게 보낼 뷰를 만드는 데 드는 시간을 최소화합니다.
객체 지향 모델에 억지로 끼워 넣는 대신, LoaL은 특정 작업을 위한 언어 구성을 정의할 수 있게 할 것입니다. 예를 들어 RoR에서는 controller의 메서드가 private로 만들지 않는 한 자동으로 public이 됩니다. LoaL은 (defaction …) 형식을 써서 이 문제를 없앨 것입니다. action은 action으로 만들었을 때만 action이 됩니다. action과 method는 다른 개념이니까요.
Rails보다 적은 스텁 생성. 아마 애플리케이션 디렉터리와 한두 개 파일을 만드는 정도의 스텁 생성은 있겠지만, 이상적으로는 그 정도면 충분해야 합니다. 상호작용적 개발 환경에서는 테스트 파일에 무엇이 들어가야 하는지 알아내는 일이 그렇게 어렵지 않습니다. 모든 매크로와 함수의 문서가 개발 시점에 доступ하니까요. 또 매크로가 있으면 프레임워크가 테스트 파일을 제대로 생성해 주길 기대하는 대신 (deftestsuite (deftestcase ……) ….) 같은 식으로 말할 수 있으니 스텁이 덜 필요합니다.
개발 모드에서는(적어도) LoaL이 코드에 대해 더 높은 확실성을 제공하면 좋겠습니다. 존재하지 않는 뷰를 렌더링하거나 존재하지 않는 action으로 redirect하는 일을 막고, 테이블에 없는 컬럼을 참조하지 않도록 하는 식입니다. 이건 모두 컴파일 타임/로드 타임에 이뤄지므로 비용은 0입니다. (이것이 정말 가치 있는지는 아직 완전히 결정하지는 않았습니다.)
아마 더 있을 겁니다.
LoaL은 Rails에서 영감을 받았지만, clone도 아니고 그것을 완전히 재설계하려는 시도도 아닙니다. 다른 프레임워크들이 앞으로 LoaL보다 더 잘할 것이 분명하니, 이것은 무엇보다 대외 홍보 측면의 이유가 더 큽니다.
여기 LoaL이 어떤 구성을 제공할 수도 있는지 보여 주는 아주 초기, 정말 초기, 아주아주 초기 초안(DRAFT!)이 있는 darcs 저장소가 있습니다: loal repo . driver.lisp 파일은 브라우저에서 볼 수 있고 LoaL이 어떤 형태를 가질 수도 있는지 짐작하게 해 줍니다. (이게 얼마나 임시적인지 충분히 강조했나요? 아직도 부족한가요? 좋습니다. driver.lisp는 겨우 한 시간쯤 생각한 결과라서, 앞으로 엄청나게 바뀔 것이 거의 확실합니다.) 초기 포털도 loal-web에 있습니다.
더 친근한 URL은 곧 준비됩니다! 그리고 Linux의 X 환경에서 내레이션이 있는 비디오 캡처를 하는 방법을 아시는 분이 있다면 조언 부탁드립니다. xvidcap이 저를 너무 싫어해서요.
JAP 님이 2005년 12월 07일에 작성:
(완결성을 위해 덧붙입니다):
SB: 제 무지를 바로잡아 주셔서 감사합니다. 하지만 여기서 Albert Einstein의 말을 하나 가져오고 싶습니다. "make everything as simple as possible, butnotsimpler!
이 말이 모든 것을 말해 줍니다. 너무 멀리 가면 뭔가를 잃게 되는데(어쩌면 중요한 것), 여기서 잃는 것은 바로 유연성입니다.
그래서, 겉보기에 Lisp보다 더 단순한 어떤 언어를 정말 만들 수는 있겠지만, 동시에 너무 많은 것을 잃게 됩니다!
(흥미롭지 않나요? 예를 들어 25개의 원시 연산자로 언어를 만들면 꽤 편리한 것을 얻을 수 있고, 예를 들어 3개의 원시 연산자로 언어를 만들면 누구도 이길 수 없을 만큼 간결한 것을 얻을 수는 있지만, 그러면 모든 유연성을 잃고, 이 행성의 정말 똑똑한 사람들을 절대 설득하지 못하게 됩니다!)
저는 이렇게 요약하고 싶습니다. 프로그래밍 언어를 만드는 진정한 “완벽한 공식”은 정확히 Lisp를 이루는 그 7개의 원시 연산자로 가져가는 것입니다.
그러면 “천국에 닿은” 것입니다! (너무 문자 그대로 받아들이진 마세요. 실제로는 이것을 확장해야 한다는 점은 압니다. 하지만 정신에서는 그렇지 않습니다. 어쨌든 이건 이론 자체를 건드리지는 않습니다.)
어떤 분이든 의견 있으신가요?
-JAP
Jules 님이 2006년 01월 23일에 작성:
IO라는 언어가 있습니다:
http://www.guldheden.com/~sandin/amalthea.html
이것은 아주 단순한 언어인데, 핵심은 continuations 하나뿐입니다. continuations로 pair를 만들 수 있고, 따라서 리스트, 정수, 문자열, 트리도 만들 수 있습니다.
하지만 이해하기가 정말 어렵습니다. 이것:
write 5;
write 6;
terminate
는 많은 일을 합니다. 먼저 “write”를 두 인자, 즉 5와 “write 6; terminate”로 호출합니다. 그러면 첫 번째 인자를 출력하고, 두 번째 인자(프로그램의 나머지)를 실행합니다. write 6도 같은 일을 합니다. 6을 화면에 출력하고, “terminate”를 호출합니다. Terminate는 프로그램의 나머지를 호출하지 않으므로, 프로그램은 종료됩니다.
정수도 continuations입니다:
1; → a;
write a;
terminate
1은 “함수”입니다. 그것은 프로그램의 나머지를 자기 자신을 인자로 주며 호출합니다. →는 그 인자를 a에 할당하고 나머지 프로그램을 호출합니다.
즉 모든 것을 continuations로 표현할 수 있습니다. 프로그램을 더 깔끔하게 만들기 위해 내장 정수와 문자열이 있는 것이 좋긴 하지만, 필수는 아닙니다.
그래서 이 언어에는 continuations라는 개념 하나만 있습니다. 이것으로 for-loop, goto 등도 만들 수 있습니다. IO는 아주 유연합니다.
하지만 그렇다고 이 언어가 “더 좋은가”요? 저는 이해하기가 매우 어렵다고 느끼지만, 함수 호출과 for loop도 프로그래머가 아니라면 어렵긴 마찬가지입니다. 연습을 좀 하면 first-class continuation은 매우 강력한 도구이고, 그것만 있으면 됩니다.
모든 Lisp 프로그래머가 이걸 한 번은 봐야 한다고 생각합니다. 이해하기 어렵지만 매우 강력하고, 모든 것을 continuations로 간결하게 표현할 수 있으므로 매크로도 필요 없습니다.
Rhodry 님이 2006년 02월 03일에 작성:
안녕하세요 여러분,
흥미로운 토론이네요. Ruby / Rails 애호가로서 지켜보는 것만으로도 아주 재미있었습니다. 하지만 새로운 언어를 매년 하나씩 배우려는 초보 “Pragmatic Programmer”로서, 위의 논의를 보고 나니 2006년은 어쩌면 “Common Lisp의 해”가 될지도 모르겠습니다 :)
목록의 베테랑 Lisp 해커들에게 약간 주제에서 벗어난 호기심 하나가 있습니다…
“Intentional Programming”을 본 분이 있나요? 원래는 10년 전 Microsoft Research의 프로젝트였고, Abstract Syntax Tree [AST]를 상호작용적이고 그래픽하게 생성/변환/축약하는 것이 목표였습니다. 최근에는 다른 상업적 주체가 넘겨받아 상용 솔루션을 구현하려 하고 있지만 계속 미뤄지고 있습니다. http://www.intentsoft.com/. 원래의 홍보는 멋져 보였습니다. 그래픽이 풍부한 domain specific language 통합 개발 환경 [IDE] 말이지요. 하지만 아직 아무것도 나오지 않았습니다. (Rails와는 달리요.)
그런데 Lisp의 자랑하는 규칙적 문법과 메타프로그래밍 능력, 그리고 Emacs의 기존 IDE 기능까지 생각하면, 이처럼 폭넓고 플랫폼 중립적인 도메인 특화 그래픽 시각화/메타프로그래밍 환경은 Microsoft C/C++(원래의 기술적 출발점)보다 어떤 Lisp 변종에 더 잘 맞지 않을까요?
혹시 왜
a) 어떤 Lisp 해커 그룹도 공개된 연구의 아이디어를 받아 Intentional Programming 환경을 만들어 보지 않았는지?
b) 누가 보더라도 C/C++가 Lisp보다 계산/컴파일 과정을 추론하기 더 쉬운 방법이라고 생각하게 되는지?
에 대한 생각이 있으신가요?
IDE에서 편집 효소 같은 것을 활성화하면 새로운 소스 패키지가 즉시 Ruby나 Python, 심지어 도식적 다이어그램으로 다시 표현된다면 좋지 않을까요? 당신의 생각에 더 가까운 언어, 그리고 해법에 더 잘 맞는 형태로요 :) {언어 성전도 좀 줄어들지 않을까요? :)}
생각거리를 던지고 싶었습니다…
Rhodry
Tayssir John Gabbour 님이 2006년 02월 17일에 작성:
Lisp 세계는 Intentional Programming을 알고 있을 뿐만 아니라, Interlisp가 아마 그 선구자였을 겁니다. 그리고 잘 알려진 Lisper 한 분이 1 Simonyi의 intentional programming 회사 공동창업자이기도 했습니다.
http://wiki.alu.org/AudioVideo
Lisp의 metaobject protocol 공동 저자인 Kiczales 이야기입니다. 마지막으로 들은 바로는 Intentsoft를 떠났다고 하더군요.
Rhodry 님이 2006년 03월 02일에 작성:
Lisp 프로그래밍 커뮤니티가 intentional software development에 관여하지 않았다는 식의 제 잘못된 암시를 겸허히 철회하며, 혹시 불쾌하셨다면 사과드립니다….
Rhodry
Eleanor McHugh 님이 2006년 04월 13일에 작성:
현실 세계의 애플리케이션 개발을 하는, 학문적으로도 감각 있는 Ruby 프로그래머로서 이 글을 정말 즐겁게 읽었습니다. 하지만 그에 대한 여러 반응에서 드러나는 종교적 분열은 다소 이상하게 느껴집니다.
저는 꽤 돌아가는 길을 통해 Ruby에 왔고, 제가 Ruby를 쓰는 유일한 관심사는 유지보수 가능한 애플리케이션을 효율적이고 적시에 전달하는 데 필요한 추상화에 집중할 수 있느냐입니다. 이 점에서 Ruby는 빛납니다. 문법이 있기는 하지만, 인간 언어와 대부분의 통신 프로토콜도 마찬가지입니다. 결국 소스 코드도 현실 세계의 계산적 부작용도 모두 인간 청중을 위한 것이니 놀랄 일은 아니지요.
Lisp가 더 넓은 개발 커뮤니티에서 충분한 mindshare를 얻지 못한 이유 중 일부는, 결국 그것이 수학적 추상화이고, 고전적 해커들 사이에서는 드물더라도 대부분의 사람들은 수학에 불편함을 느끼기 때문입니다. 소프트웨어를 계산 parse tree의 상징적 표현으로 개발하고 싶다면 흥미로운 표기법입니다. 하지만 그것은 프로그래밍 과정의 이론적 모델 중 하나일 뿐입니다.
반면 Ruby는 앞으로 더 널리 퍼질 유형의 언어를 보여 주는 예입니다. Lisp를 강력하게 만드는 추상화들을, 수학적 성향보다 예술적/언어적 성향을 가진 사람들에게 받아들여지기 쉬운 형태로 제시하는 언어들 말입니다.
이것은 새 일이 아닙니다. Ruby는 Icon이나 Pop-II와 많은 공통점을 갖는 것처럼 보입니다. (마치 PERL이 SNOBOL과 닮은 것처럼요.) 이 두 언어 역시 강한 이론적 기반에도 불구하고 컴퓨터 과학 바깥에서 추종자를 얻었습니다. Ruby가 다른 점은 킬러 애플리케이션, 즉 인터넷을 갖고 있다는 것입니다. Rails 웹 애플리케이션 프레임워크를 본 사람이라면 누구나, 현재 그것이 경쟁하고 있는 수많은 임시방편적 스크립트 언어들과 비교해 그 잠재력을 즉시 알아볼 수 있습니다. 그리고 그 힘의 대부분은 Ruby가 메타프로그래밍을 상대적으로 쉽게 다루기 때문에 나옵니다.
Ruby는 비기술적 사용자에게 객체 지향과 함수형 메타프로그래밍을 고통 없이 만들고, 겉으로 비슷해 보이는 다른 많은 언어에서는 지저분한 해킹이 필요할 작업들에 우아함과 아름다움을 도입합니다. 제 생각에 이것은 매우 좋은 일입니다. 우아한 소프트웨어는 대체로 유지보수가 더 쉽기 때문입니다. 이 점은 Lisp 해커 핵심층도 인정할 것이라 생각합니다.
제게 중요한 것은 절대적 의미에서 Lisp와 Ruby 중 어느 쪽이 더 강력하냐가 아니라, 가능한 한 높은 수준의 추상화를 최소한의 이론 지식으로 사용하려는 특정 개발자의 손에 어느 쪽이 더 강력한 도구가 되느냐입니다. 저는 수학에 약한 사람이기 때문에, 이전 댓글 작성자들 중 다수에게는 그렇지 않을지 몰라도 저 개인에게는 Ruby가 더 강력하다고 생각합니다.
비기술적 사용자가 Ruby를 쉽게 익숙하게 만들 수 있다는 점을 생각하면, Ruby는 1970년대와 1980년대 마이크로컴퓨터 혁명에서 BASIC이 그랬던 역할을 네트워크 프로그래밍에서 할 잠재력이 있다고 생각합니다. Ruby on Rails가 웹 개발 영역에서 빠르게 확산되는 것은 그 빙산의 일각일 뿐입니다.
요약하자면: Ruby는 강력한 언어이며, 프로그래머가 아닌 사람도 익히기 쉬운 문법을 갖고 있습니다. 보통 초보 프로그래머가 접할 수 있는 언어에는 없는 block closure, object-orientation, meta-programming 개념을 포함하고 있고, 그것들을 기억하고 쓰기 쉬운 관용구를 제공합니다. 의심할 여지 없이 Lisp에서 더 쉽게 활용할 수 있는 아키텍처 기법들이 있겠지만, 그것들이 일반적인 소프트웨어 개발에 실제로 얼마나 유용한 기술인지에 대해서는 논쟁의 여지가 있다고 생각합니다.
Eleanor
Daniel 님이 2006년 04월 24일에 작성:
Eleanor, 훌륭한 댓글입니다. 답으로 Dijkstra의 말을 하나 드리겠습니다.
Don’t blame me for the fact that competent programming, as I view it as an intellectual possibility, will be too difficult for ‘the average programmer’, you must not fall into the trap of rejecting a surgical technique because it is beyond the capabilities of the barber in his shop around the corner.
cmy 님이 2006년 05월 01일에 작성:
Eleanor, excellent comment. In response, I offer a Dijkstra quote…
제가 Dijkstra를 매우 존경하는 것은 사실이지만, 그가 GOTO 문 개수를 세고 논문도 손으로 써서 제출하던 바로 그 사람이라는 점은 기억해야 합니다.
이상을 따르는 것이 좋기는 하지만, 우리 중 일부는 현실에서 살아야 합니다.
Alexander Medvedev 님이 2006년 05월 17일에 작성:
네, 당신이 든 이 Lisp 예제와 비교하면 Ruby 예제가 더 직관적이긴 합니다.
(remove-if (lambda (n) (= (mod n 3) 1))
(mapcar (lambda (n) (* n n))
’(1 2 3)))
하지만. 당신이 말한 것보다 Lisp를 변호할 수 있는 말은 더 많습니다. 매크로는 lambda를 더 간결하게 만드는 데만 쓰이는 게 아닙니다. 만약 위 표현식이 읽기 어렵고, Ruby 예제처럼 계산 순서대로 읽고 싶다면, 이것을 보세요.
(stream-2nd-arg ’(1 2 3)
(mapcar (lambda (n) (* n n)))
(remove-if (lambda (n) (= (mod n 3) 1))))
여기서 mapcar와 remove-if에는 두 번째 인자가 없습니다. 어디서 얻을까요? 바로 이전 계산에서 가져옵니다. ‘stream-2nd-arg’ 매크로의 첫 번째 인자는 먼저 평가됩니다. 그 뒤의 인자들은 차례대로 함수 호출로 평가되고, 각각은 바로 앞 호출의 결과를 두 번째 인자로 받습니다.
단순하고, 간결하고, 읽기도 좋습니다.
stream-2nd-arg 매크로의 구현은 독자에게 남기는 연습문제로 하겠습니다 :)
John Collins 님이 2006년 05월 17일에 작성:
Ruby, Lisp, 그리고 함수형 프로그래밍 세 가지 모두에 정통한 구루를 만나게 되어 정말 기쁩니다.
Lisp와 Python에서는 apply 함수를 사용해 파라미터 리스트를, 그 파라미터를 받도록 선언된 함수에 넘기고, 호출된 함수 선언의 각 formal parameter에 직접 바인딩할 수 있습니다.
Ruby에서는 물론 하나의 *parameter에 대해 파라미터 리스트를 하나의 리스트로 넘길 수 있습니다. 하지만 제가 원하는 건 그게 아닙니다.
Ruby에서 이걸 어떻게 해내는지, 아니면 apply 함수에 가까운 것이 그냥 빠져 있는 건지 아시나요?
Eric 님이 2006년 05월 21일에 작성:
이렇게 해 보세요:
obj.meth arg1, arg2, *more_args
대략 Scheme의 “apply”에 해당합니다.
Notav A. 님이 2006년 06월 13일에 작성:
(apply (lambda (a b) (+ a b)) ’(1 2))
는 다음으로 할 수 있습니다:
lambda{|a,b| a+b}.call(*[1,2])
someone 님이 2006년 06월 24일에 작성:
Lisp 코더들은 실제로 프로그램을 쓰나요, 아니면 자기 언어 얘기만 하며 앉아 있나요?
컴퓨터 과학과 AI 연구실 깊은 곳에서는 쓰이는 것 같지만, 소프트웨어를 다루고, 시스템을 관리하고, 셀 수 없을 만큼 많은 OS와 문제 영역에서 코드를 써 온 수년 동안, 저는 Lisp 코드 한 조각도 본 적이 없고, 제 프로젝트와 비슷한 종류의 작업에서 누군가 Lisp를 쓰는 것도 본 적이 없습니다. 싸우자는 뜻은 아니지만, Lisp를 _사용하는 것_보다 Lisp에 대해 _이야기하는 것_을 대략 천 배쯤 더 많이 본 것 같습니다.
언젠가 Lisp를 배우려는 마음은 늘 있었지만, 실제 프로그램보다 usenet에서 더 많이 쓰게 될 지식을 배우게 될 것 같은 느낌이 듭니다.
최근 Ruby를 알게 되었고, 그 놀라운 장점들 가운데서도(적어도 제 워크스테이션에서 요즘 열려 있는 vim 세션 어디에서나 볼 법한 것 중 최상위에 둘 만한 것은 enumerable module입니다), 그 어떤 것도 커뮤니티의 친절함과 도움을 능가하지 못합니다. Ruby는 언어도 소프트웨어이고 프로그래머도 사용자라는 사실을 제대로 이해한 첫 언어처럼 보입니다.
프로그래밍 커뮤니티에는 집합명사가 필요합니다. Perl 해커들의 난독화, C 코더들의 완고함, 어셈블리 조련사들의 주름, Ruby 해커들의 넘쳐흐름, CSS 저자들의 궤양, Lisp 코더들의 투덜거림 같은 식으로요.
Eric 님이 2006년 06월 26일에 작성:
아마 지역에 따라 다를 겁니다. 저는 지난 10년 중 적어도 6년은 학술 프로젝트와 상업 프로젝트 양쪽에서 Lisp와 Scheme를 쓰며 보냈습니다. 하지만 저는 멋지고 비정형적인 일을 좋아하고, 보스턴 대도시권에 살고 있습니다. 이 두 요인은 함께 있을 때 Lisp와의 상관관계가 매우 높습니다.
AI 분야에서 일했는데도 Lisp를 한 번도 못 봤다면, 시작 시점이 80년대 후반 이후일 가능성이 매우 높습니다. Moore의 법칙과 심각한 경영 무능이 결합해 Lisp 회사 절반을 무너뜨린 시기니까요.
그럼에도 불구하고 Ruby를 최대한 활용하고 싶다면 Lisp를 배우는 편이 좋을 겁니다. _Practical Common Lisp_는 제가 본 모든 Ruby 문서를 합친 것보다도 10배쯤 많은 메타프로그래밍 조언을 담고 있으니까요.
William James 님이 2006년 08월 25일에 작성:
몇몇 Lisp 예제에 대한 Ruby 대응은 다음과 같습니다:
(iter (for x in '(-1 2 -10 4)) (finding x maximizing (abs x))) [-1, 2, -10, 4].inject{|a,b| a.abs > b.abs ? a : b}
[-1, 2, -10, 4].sort_by{|n| n.abs}.last (iter (for x in '(1 2 3 -4 -10 -43 49 49 8934)) (until (= (sqrt 7) x)) (collect x into collection) (finding x maximizing (abs x) into y) (finally (return (list collection y))))
(참고로, 이 Lisp 코드는 광고한 대로 동작하지 않을 것 같습니다. (= (sqrt 7) x)는 잘못된 듯합니다.)
def maxim a,b; (yield a) > (yield b) ? a : b; end y = 0 [1,2,3,-4,-10,-43,49,49,8934].inject([]){|a,n| break [a,y] if n==49 y = maxim(y,n){|x| x.abs} a << n }
William James 님이 2006년 08월 26일에 작성:
이 버전은 리스트에 49가 없더라도 제대로 동작합니다. 그리고 변수 y도 없앴습니다.
def maxi a,b; (yield a) > (yield b) ? a : b; end
[1,2,3,-4,-10,-43,49,49,8934].inject([[],0]){|a,n|
break a if n==49
a0 << n ; a }
Miquel 님이 2006년 09월 07일에 작성:
저는 매일 Lisp로 일하고 있고, 그것이 매우 강력하다고 말할 수 있습니다. 다만 웹 애플리케이션을 위한 decent한 mysql 인터페이스를 만들 동기가 없었을 뿐입니다. 그래서 Rails를 택했습니다.
아, 뭐…
하지만 db 지원이 오면 다시 돌아갈 겁니다.
Pebblestone 님이 2006년 09월 08일에 작성:
Miquel:
CLSQL을 써 보세요. 여러 DB와 여러 Lisp 구현을 지원합니다. 저도 최근 프로젝트에서 그것을 썼습니다.
KristofU 님이 2006년 09월 26일에 작성:
저는 C++ 프로그래머인데, 새 언어를 하나 배워 좀 더 우아하게 일하고 싶었습니다.
Ruby는 저를 순식간에 끌어들였습니다. 배울 것이 거의 없습니다. 그냥 거기 있습니다.
자르고, 뒤섞고, 돌리고, 무엇을 하든 결과는 언제나 동작하는 프로그램입니다.
for-statement를 좋아하나요? 좋습니다, for-statement를 쓰면 됩니다. iterator를 좋아하나요? 좋습니다, iterator를 쓰면 됩니다. object functor를 만들든 lambda를 쓰든 마음대로 하세요. Ruby는 아무것도 강요하지 않고, 그 사이에도 계속 동작하는 앱을 만들어 내면서 더 미묘한 기능들을 천천히 익힐 시간을 줍니다.
Haskell, Scheme, Erlang도 잠깐 해 보았지만, 솔직히 아무것도 해내지 못했습니다. Ruby와 비교하면 꽤 김 빠지는 경험이었습니다.
taw 님이 2006년 10월 01일에 작성:
RLisp에 관심이 있을지도 모르겠습니다. Ruby 런타임과 직접 연결되고 아주 밀접하게 통합된 Lisp입니다.
아직은 아주 알파 단계지만, 표준 Ruby 라이브러리의 webrick과 매크로를 사용하는 HTTP 서버 같은 정말 멋진 것들을 할 수 있습니다.
이 초기 단계에서도 스크립트 스타일 프로그램에 가장 실용적인 Lisp 중 하나일 수 있다고 생각합니다. 모든 Ruby 라이브러리에 접근할 수 있으니까요.
지금 가장 큰 문제는 문화적 충격인 듯합니다. RLisp에는 Ruby/Python 스타일 let이 있고(이게 가장 논쟁적인 부분인데 왜 그런지는 잘 모르겠습니다), “진짜” 객체 시스템(메시지 패싱 기반이고 모든 것이 Ruby 객체입니다), 그리고 리스트는 배열입니다(그래서 cdr/cons는 복사합니다).
즐겨 보세요 :-)
Simplicus 님이 2006년 10월 03일에 작성:
왜 Ruby를 Lisp와 비교하고 Erlang, Haskell, ML이나 다른 FP 언어들과는 비교하지 않나요?
저는 이 토론이 여러 면에서 아주 유익하다고 생각합니다. 하지만 개인적으로 가장 흥미로운 부분은 Ruby가 어느 정도까지 함수형 언어인가 하는 점입니다. Eric의 용어를 빌리면, Ruby가 실제로 얼마나 “functionally dense”한가 하는 것이지요.
저는 Lisp와 특히 Scheme가 함수형 프로그래밍(FP)에 쓰일 수 있다는 점에는 의심이 없습니다. 그렇지만 두 언어 모두 비함수형 프로그래밍에도 똑같이 잘 쓰일 수 있습니다.
이제 질문으로 돌아가겠습니다:
왜 Ruby를 Lisp와 비교하고 Erlang, Haskell, ML 같은 다른 “진짜” (Haskell) 혹은 “더 함수형인” (Erlang, ML) FP 언어들과는 비교하지 않나요?
위에서 언급한 FP 언어들 중 Erlang만 비엄격 타입이 없고, 나머지 둘(Haskell, ML)은 엄격 타입을 가진다는 점은 이해합니다. 그래도 그들의 “함수형 밀도”를 Ruby와 비교할 수는 있다고 생각합니다.
저는 DEC PDP-11 Macro Assembler로 시작해서 20년 넘게 프로그래밍을 해 왔고, 이후 작업의 대부분은 OOP, 즉 C+와 Java였으며 Java는 첫 알파 버전부터 가지고 놀았습니다. 그 시절 저는 Java를 C++보다 훨씬 좋아했습니다. 그 “옛날”에 CLOS도 접할 기회가 있었고, 그 강력한 아이디어들(리스트 = 코드 = 데이터, generic function, MOP 등), 균일성, 단순함에 매료되었습니다. 안타깝게도 그때도 지금도 Lisp나 Scheme 프로젝트로 먹고살 방법은 찾지 못했습니다. 다만 AI 관련 영역에서 조금 일했고, Prolog를 비롯해 여러 “작은” 언어와 “큰” 언어들도 배웠습니다.
약 3년 전부터 저는 Java와 다른 OOP 언어들보다 더 강력한 프로그래밍 도구(언어)를 찾기 위해 FP를 꽤 진지하게 탐구하기 시작했습니다. 오랫동안 저장고에 넣어 두었던 몇몇 아이디어를 구현하고 싶었기 때문입니다.
Scheme로 시작해서 ML, 그리고 지금은 가장 마음에 드는 Haskell을 보고 있습니다. 그 사이에 Erlang과 Ruby도 들여다보고 있습니다. 다시 “FP 밀도” 이야기로 돌아가면, Scheme와 Lisp에는 ML과 Haskell이 가진, 그리고 제가 언어의 균일성, 명료성, 가독성, 표현력에 매우 중요하다고 생각하는 강력한 구성 몇 가지가 빠져 있습니다. (오해 마세요. 저는 Haskell, Scheme, Lisp를 모두 사랑합니다 :)
그것들은 다음과 같습니다.
Guards (Haskell)
Pattern matching (Haskell, ML, Erlang)
List comprehensions (Haskell)
Guard 예제:
max :: Int -> Int -> Int max x y | x >=y = x | otherwise = y
Pattern matching과 comprehension 둘 다를 보여 주는 예 — Haskell의 quick sort는 이만큼 단순합니다:
qSort :: [Int] -> [Int] qSort [] = [] qSort (x:xs) = qSort [ y | y <-xs, y<=x] ++ [x] ++ qSort [ y | y<-xs, y > x]
이 코드는 quick sort의 비형식적 정의에 매우 가깝습니다:
“리스트를 정렬하려면 머리 ‘(x:xs)’를 떼어 내고, 나머지를 두 부분으로 나눈다. 첫 번째는 ‘x’보다 크지 않은 원소들, 두 번째는 ‘x’를 초과하는 원소들. 이 두 부분을 정렬한 뒤, ‘x’를 가운데 두 정렬 결과와 이어 붙인다.”
이게 바로 명료성과 가독성이 아닌가요?
순수 함수형 표기가 매크로가 만들어 낸 표기보다 더 낫지 않나요?
Ruby는 Guards, Pattern matching, List comprehensions에 대응하는 것으로 무엇을 주나요?
저는 아예 lazy evaluation 이야기조차 하지 않았습니다. Haskell의 매우 강력한 전략인데, 전체 결과를 계산하는 데 정말 필요할 때만 함수 인자를 평가합니다. 그래서 계산 비용이 낮아지고, 무한 데이터 구조도 기술할 수 있습니다.
거의 모든 것과 마찬가지로, lazy evaluation도 Lisp와 Scheme에서 구현할 수 있습니다. Ruby에서 이것을 어떻게 할지 아이디어가 있으신가요?
Haskell 뒤에서 보면 Ruby 문법은 저에게 너무 절충적입니다 (foo, @bar, baz :faz => xxx). ‘blocks’와 ‘yields’를 보면 함수가 first class object가 아닌 것에 대한 아쉬움이 들고, “meta programming”은 지금 눈앞의 코드 뒤에서 실제로 무슨 일이 일어나는지 숨기고 흐리게 만드는 완벽한 도구처럼 보입니다. (메서드가 다른 소스 파일에서 클래스에 추가될 수 있으니까요.) 등등…
어쩌면 제가 Ruby에 대해 완전히 틀렸을 수도 있고, 더 많은 경험을 쌓으면 의견이 확 바뀔지도 모릅니다. 두고 봐야겠지요.
이 토론과 관련해서는 여러분이 다음에 대해 어떻게 생각하는지 궁금합니다.
제가 이 메시지를 시작하며 던진 질문
Haskell(그리고 일반적인 FP)이 왜 Java, Python, Ruby처럼 널리 퍼지지 않았는가? 그리고 다음과 같은 조건이 있음에도 왜 이런 슬픈 일이 벌어지는가?
a. 비록 크진 않지만 아주 강하고 헌신적인 Haskell 개발자 커뮤니티.
지속적으로 늘어나는 오픈소스 Haskell 애플리케이션, 도구, 라이브러리 집합.
schemer 님이 2006년 10월 19일에 작성:
Simplicus, 저는 pattern matching과 guards가 엄청나다는 데 동의합니다. 타입이 없어도 말이지요.
저는 주로 Scheme로 코딩하지만, Haskell을 해 보았을 때 Monads와 lazy evaluation에는 정말 충격을 받았습니다. 대부분의 프로그래머가 Haskell의 그런 측면을 오히려 꺼린다는 사실이 믿기지 않습니다. 저는 예전에 Scheme로 프로그래밍하면서, 그것이 뭔지도 모르고 그런 기법들을 재발명하려고 하고 있었거든요.
하지만 개인적 경험으로는, Scheme에 그런 기능이 없을 이유도 없습니다. 말씀하신 것처럼 pattern matching을 위한 라이브러리도 있고, 또 there are
제가 보기에는 Scheme가 Haskell보다 덜 “functionally dense”한 이유는 다음과 같습니다.
Scheme의 mutable define, mutable cons, 그리고 non-imperative i/o의 부재가 그것을 Haskell보다 덜 함수형으로 만듭니다. 물론 제가 말한 이런 특징들이 언어의 근본적 속성은 아닙니다. 제 생각에는 둘 다 언젠가는 고쳐지길 바라는 역사적 찌꺼기입니다…
반대로 저는 sexpr Haskell을 정말 보고 싶습니다. :)
Ruby에 대해서는, 적어도 제가 알기로는 tail-call optimization이 없기 때문에 “함수형”이라고 생각하지 않습니다. Common Lisp도 tco는 없지만, 주요 컴파일러(CMUCL, SBCL, 아마 CLisp도)가 tail call을 최적화하므로, CL에 대해서는 특별한 문제의식이 없습니다. 물론 CL에서는 꽤 비전형적인 스타일을 써야 하긴 합니다.
제게 TCO는 큰 문제입니다. Elisp에서는 함수 호출 깊이를 넘지 않으려면 destructive map/filter를 써야 하는데, 정말 성가시거든요…
schemer 님이 2006년 10월 19일에 작성:
아, 오타를 좀 냈네요. 앞 문장에서 non-imperative i/o의 _부재_를 말하려던 것이었습니다.
또 몇몇 잘 알려진 Scheme 해커들이 Scheme로 monad를 구현했다는 점도 빼먹었네요. 다만 안타깝게도 표준화된 예제는 없습니다…
Scheme에서의 lazy evaluation도 말씀하신 대로 가능하긴 합니다. 다만 stream-define 등을 위한 srfi를 아직 제대로 돌리지 못했습니다. 제 구현의 모듈이 추가 매크로 없이는 그것과 호환되지 않는 것 같더군요.
또 memoized stream용 srfi가 helper function 대신 set!을 쓰는 이유도 잘 모르겠습니다. (물론 Scheme의 mutable define 문제 때문이라고 말하고 싶긴 합니다.)
William James 님이 2006년 10월 24일에 작성:
Ruby의 Quicksort:
def qsort ary
return ary if ary == []
pivot = ary0
left, right = ary[1..-1].partition{|x| x < pivot }
qsort( left ) + [pivot] + qsort( right )
end
memeplex 님이 2006년 10월 24일에 작성:
커뮤니티와 코드베이스의 부족은 극소수를 제외한 모든 언어에 큰 장애물입니다. 또 대부분의 새 언어에는 C/C++ 레거시 코드에 쉽게 접근할 수 있게 해 주는 표준 ffi가 없습니다. 가능하면 swig 같은 도구로 자동 생성된 바인딩이 좋겠지요. 이 점에서 Lisp에는 cffi가 있으니 좋은 일입니다. 하지만 결국 무거운 작업을 해 줄 사람이 없으면 멀리 가기 어렵습니다. 그 방대한 바인딩 코드는 생성되고, 패치되고, 테스트되어야 합니다. 작은 커뮤니티가 취할 길은 성숙하고 잘 자리 잡은 런타임에 기생하고, 그것의 중간 바이너리 표현으로 컴파일하는 것처럼 보입니다. 물론 저는 여기서 java와 .net/mono/pnet을 말하고 있습니다. 그것들은 자기 역할을 꽤 잘 하고 있고, 성능도 충분히 괜찮으며, 중간 언어로 보면(msil은 java 바이트코드보다 더 그렇습니다) 네이티브 코드로도 컴파일할 수 있습니다. gcj 같은 것을 보세요. 저는 scheme 프로그래밍을 사랑하지만, scala나 nemerle 같은 새로운 oo+fp 언어들도 좋아합니다. 이런 교차 언어/교차 플랫폼 런타임은 reflection 능력이 있으므로, 바인딩이 C/C++ 네이티브 대상보다 훨씬 쉽습니다. 때로는 모듈을 import하고 바로 쓰기 시작하는 것만으로도 충분합니다. 물론 범용 런타임이 특정 언어에 맞춘 표준 라이브러리와 같지는 않습니다. 하지만 그런 표준 라이브러리도 런타임이 제공하는 기능 위에 더 빨리 만들 수 있고, 아직 완전하지 않더라도 언제든 범용 API로 내려갈 수 있습니다. 막다른 길이 아니지요. 정말 필요하다면 C/C++와도 쉽게 바인딩할 수 있습니다. java와 c#용 swig 모듈은 성숙했지만, 멋지고 작은 언어들 중 상당수에는 그런 것이 없습니다. 저는 요즘 bigloo scheme로 jvm을 타깃으로 프로그래밍하고 있고, scala도 쓰고 있습니다. 둘 다 clr용 코드도 낼 수 있지만, 그 지원은 아직 그만큼 성숙하지는 않습니다. 작은 커뮤니티를 가진 언어들에게는 이 길이 맞다고 생각합니다. 그리고 어쨌든 이런 중간 플랫폼은 코드 재사용을 촉진하고, 고급 해석/컴파일 기법을 제공하고(특정 기법, 예를 들어 tail-recursion 지원이 부족할 수는 있지만 그건 감수해야 할 대가입니다), 강력한 개발 환경 등을 줍니다. 요즘 저는 대체로 이런 생각을 하고 있습니다.
Eric Kidd 님이 2006년 10월 26일에 작성:
아, 저는 Haskell의 열렬한 팬입니다. 문법은 좋고 밀도도 높습니다. (가끔은 다소 낯설긴 하지만요.) 그리고 monad는 놀랄 만큼 강력한 추상화입니다.
monad로 쉽게 표현할 수 있는 설계의 한 부류가 있습니다. (통일, 백트래킹, 트랜잭션, 라이브러리로서의 continuations, 국소 상태, 모듈형 파서 등등.) 이런 설계 가운데 많은 것들은 Lisp 매크로로는 깔끔하게 구현할 수 없습니다. (code-walker나 완전한 내장 언어를 쓰지 않는 한 말이죠.)
또 monad는 하나 이상 있을 수 있고, 함수의 반환 타입에 따라 monadic operation을 overload할 수 있을 때 가장 유용한 것처럼 보입니다. 제가 Scheme에서 monadic 코드를 쓰며 발견한 가장 큰 장벽도 바로 그 점입니다.
Haskell은 꽤 어려운 프로그래밍 언어이고, 아마 주류가 되지는 못할 것입니다. 하지만 지금 등장한 언어들 중 단연 가장 흥미로운 것 중 하나이며, 저는 거기서 많은 것을 배웁니다. 가장 큰 불만은 subtyping이 없다는 점인데, O’Haskell이 이 문제를 해결합니다.
네 개의 언어를 추천해야 한다면, Ruby(아주 재미있으니까), Lisp/Scheme(매크로 때문에), Oz(통일, concurrent logic, constraints 때문에), Haskell(monad와 깊이 수학적인 스타일 때문에)을 추천하겠습니다.
scripting geek 님이 2006년 12월 13일에 작성:
아주 흥미롭고 즐거운 토론이네요!
저는 90년대 초반 Berkeley에서 AI를 하며 몇 년 동안 LISP와 Scheme를 했습니다. 몇 년 뒤 그냥 재미로 그 코드를 다시 봤을 때, 제 예전 LISP 배설물들이 완전히 불가해하다는 사실에 충격을 받았습니다. 그 sexp의 뒤엉킴은 너무 심해서, 그걸 해독하라고 한다면 의식적 할복을 택했을 겁니다. 물론 그건 제 형편없는 LISP 코드 이야기일 뿐입니다.
또 다른 얘기로… 언어의 성질에 대한 열정적인 논쟁이 아주 흥미롭긴 하지만, 결국 생산성 측면에서는 다소 무관하다고 생각합니다.
요즘은 언어의 이론적 표현력 한계보다, 사용 가능한 오픈소스 라이브러리의 폭과 질이 훨씬 더 중요한 요소라고 생각합니다.
다소 투박한 메타프로그래밍 기능과 초강력 매크로 사이에서 적응하는 일은, 매일 필요한 절반의 기능을 해 줄 라이브러리가 없는 상황과 비교하면 무한히 쉽습니다.
듣기로는 Lisp는 그 점에서 여전히 Python/Ruby 진영에 한참 뒤처져 있는 것 같고, 그 이유만으로도 저는 지금 시점에서는 다시 보지 않을 것 같습니다. 그리고 적어도 제 경우에는, 컴퓨터에게는 좋은 program-as-AST의 힘이 인간 이해에는 그리 좋지 않다는 점도 배웠습니다. 우리는 재귀적 트리 평가기가 아니니까요. 적당한 수준을 넘는 중첩과 계층이 쌓이면, 모습이 아주 끔찍해지기 시작합니다.
반면 Python/Ruby는 매우 읽기 쉽고, 문법이나 설탕에 담긴 유용한 관용구가 풍부하며, 거의 모든 것과 쉽게 상호작용할 수 있는 API를 갖고 있습니다. 이런 도구 덕분에 저는 혼자서도 C 프로그래머 한 부서 정도를 대체할 수 있고, 그것은 매우 매력적입니다.
Chris Rathman 님이 2007년 01월 14일에 작성:
주제가 Ruby와 Lisp의 비교이고, 또 Simplicus가 Ruby와 Haskell, ML, 다른 FP 언어들을 비교하는 데 관심을 보였기 때문에, SICP 예제를 이런 언어들로 번역하는 제 개인 프로젝트를 언급하고 싶습니다.
http://www.codepoetics.com/wiki/index.php?title=Topics:SICP_in_other_languages
아직 Ruby에 대해 배울 것이 많지만, 첫 장과 반쯤을 번역하면서 Ruby에서 느낀 주요 특성은 (1) tail call optimization이 없다는 점 — Python과 JavaScript도 마찬가지이고, Lisp 커뮤니티에서는 TCO 부재를 버그로 보는 사람도 많습니다 — 그리고 (2) 이름 있는 함수와 익명 함수의 구분입니다. 제 이해가 맞다면 lambda는 curry할 수 있지만, 이름 있는 함수는 curry할 수 없지요. Erlang도 이 점은 비슷합니다.
gar37bic 님이 2007년 01월 14일에 작성:
정말 좋은 토론이고, 저에게도 아주 유익했습니다. 저는 다음으로 어떤 언어를 본격적으로 할지 고민 중인데, 주로 neural network 같은 분야를 연구하기 위해서입니다.
실제 업무에서는 정규 표현식 기반의 휴리스틱 필터를 많이 씁니다. (대체로 PCRE, 즉 ‘perl-compatible regex’를 사용하지만 Perl 자체를 쓰진 않습니다.) 예를 들어 여러 개의 레거시 데이터셋을 하나의 ‘canonical’ 세트로 파싱하고, 걸러내고, 정제하고, 결합하는 식입니다. 여기에는 다른 프로그램을 돌려 들어오는 데이터 스트림을 미리 정렬하고, 가능한 매칭에 대한 확률 점수 배열을 유지하는 일도 자주 포함됩니다. 이런 문제를 이 언어들은 어떻게 다루나요?
gar37bic 님이 2007년 01월 14일에 작성:
또 저는 허락을 받고 HTML, SQL, CVS, PDF, DOC 같은 파일들을 웹에서 ‘스크랩’해서 회사의 데이터 마이닝 작업에 입력 데이터로 쓰기도 자주 합니다. 예를 들면 기업의 연방 문서나 SEC 보고서들이지요.
그리고 지금은 OpenOffice를 스크립트해서 파일 형식 변환을 .DOC로 하고 있습니다…
Eric Kidd 님이 2007년 01월 14일에 작성:
Chris Rathman: I thought I’d mention my pet project to translate the SICP examples into these languages.
오, 정말 멋지네요. 링크 감사합니다!
David A. Wheeler 님이 2007년 01월 15일에 작성:
전통적인 Lisp 표기법의 한 가지 문제는 읽기가 꽤 어렵다는 점입니다. 프로그램이 커질수록 더 심해지고요. 특히 전통적으로 중위 연산자인 것들을 쓸 때는 더욱 끔찍합니다. 예를 들어 (+ 3 (* 2 3))는 규칙적인 문법일지는 몰라도, Graham조차 어색하다고 인정합니다.
저는 “sweet-expressions”라는 표기법을 개발했는데, 읽기가 더 쉽다고 생각합니다. sweet-expression reader는 일반적인 s-expression도 읽을 수 있지만, 다른 형식도 받아들일 수 있습니다. 그래도 macro construct 등 Lisp류 언어의 장점은 그대로 유지됩니다. 더 많은 정보는 여기 있습니다.
http://www.dwheeler.com/readable/
A Total Coward 님이 2007년 01월 17일에 작성:
bill a: LoaL 링크가 전부 404입니다. 이게 실제로 공개되긴 했나요?
Timmy Jose 님이 2007년 01월 18일에 작성:
C/C++와 JAVA에 익숙하고, 대학에서 A.I 과목의 일부로 Lisp 기초만 공부해 봤으며, 지금까지는 Ruby를 옆에서 보기만 했던 프로그래머로서 이 스레드는 정말 흥미로웠습니다. 다만 Lisp 쪽 사람들이 Ruby 쪽보다 압도적으로 많아서 조금 일방적이긴 했네요! 제 취향으로는 Lisp 문법이 Ruby보다 더 잘 맞는 것 같습니다. 아마 Lisp 쪽으로 뛰어들고 나서 Ruby가 어디에 들어맞는지 나중에 보게 될 것 같네요. 여러분, 좋은 토론이었습니다!
Charlie Lindahl 님이 2007년 12월 13일에 작성:
저는 80년대에 Texas Instruments에서 몇 년 동안 LISP 머신으로 일했습니다.
제 요점 중 하나는 이것입니다: 핵심은 환경입니다. 단지 IDE가 아니라요. LISP와 Smalltalk 환경에서는 질문/사용 중인 모든 것이
그 언어 자체로 쓰여 있습니다. (OS / 머신 아키텍처와 대화하기 위해 필요한 기계 특화 저수준 부분은 제외하고요.)
이 말이 뜻하는 바는(일반적인 비즈니스 사례에는 그다지 좋지 않을 수도 있지만, 학습과 표현력 면에서는 놀랍습니다), 제가 실제로 기본 OS나 도구를 뜯어 보고, 사용자화하고, 연구할 수 있다는 것입니다.
예를 들어 EMACS가 마음에 안 든다고 해 봅시다. 소스를 잡아 와서(LISP IDE에서는 키 한 번이면 됩니다)
IDE/디버거로 파고들어 당신 사양에 맞게 다시 쓰면 됩니다.
저는 Mac에서 Lisp를 돌리고, 소스 코드 없이도 윈도우 데이터 구조를
실시간으로 / 즉석에서 분해해 보면서 Macintosh Toolbox 구조를 배웠습니다.
Ruby가 이런 수준의 환경, 관찰, 디버깅 능력에 도달하면,
엄청나게 강력해질 것입니다.
지금도 제가 본 바로는 꽤 괜찮습니다.
제 2센트 Charlie Lindahl Houston, TX
Eric Kidd라는 개발자이자 가끔 창업가가 올리는, 소프트웨어에 관한 무작위 코드 조각, 프로젝트, 그리고 단상들입니다.