루비 메타프로그래밍의 끝을 보여주는 RubyInline, ParseTree, Ruby2Ruby
December 11th, 2008
RubyQuiz를 풀며 Code Beautifier를 간단하게 구현해보려다가, 약간 삼천포로 빠졌다. 그치만 그 과정에서 사용하면 즐거운 도구들을 만났다. 그 친구들을 소개하고, 어떻게 활용할 수 있을지 고민해보자.
RubyInline
RubyInline은 루비 코드 안에서 다른 언어로 만든 코드를 적을 수 있는 라이브러리다. 아래 예제는 루비 코드가 C 코드를 포함하고 있다.
- class Hello
inline do |builder|
builder.include "<stdio.h>"
builder.c 'void hello() { puts("hello world"); }'
end
end
위 파일을 실행하면 ~/.ruby_inline 디렉토리에서 C 코드를 컴파일하여 수행한다. 느리기로 유명한 루비에서 속도가 중요한 일을 할 때, 또는 다른 언어로된 코드와의 연동을 위해 사용할 수 있다.
ParseTree
ParseTree는 RubyInline을 이용해 루비 코드의 Parse tree를 추출하여 s-expression으로 반환한다. 예를 들면 이런 식이다.
- def conditional1(arg1)
if arg1 == 0 then
return 1
end
return 0
end
이 코드를 변환하면 아래와 같다.
- [:defn,
:conditional1,
[:scope,
[:block,
[:args, :arg1],
[:if,
[:call, [:lvar, :arg1], :==, [:array, [:lit, 0]]],
[:return, [:lit, 1]],
nil],
[:return, [:lit, 0]]]]]
<그림> 1+1을 나타내는 ParseTree
Ruby2Ruby
Ruby2Ruby는 ParseTree가 반환하는 s-expression을 이용해 다시 루비 코드를 생성해낸다. ruby2ruby를 이용하면 동적으로 언어를 다루는 일을 쉽게 할 수 있다.
좋은 예 중 하나는 내가 루비세미나에서 소개한 적이 있는 Heckle이다. Heckle은 코드의 특정 부분을 Ruby2Ruby를 이용해 변경하고, 그 때 테스트가 깨지는지 확인하는 도구다. 물론 깨져야 테스트를 촘촘하게 잘 만든 것이다.
또, Ruby2Ruby를 이용하면 Code Beautifier를 정말 간단하게 구현할 수 있다.
- sexp = ParseTree.new.parse_tree_for_string($stdin.read).first
puts Ruby2Ruby.new.process(sexp)
위 코드가 전부다.
- #!/usr/bin/ruby -rcgi
H,B=%w'HomePage w7.cgi?n=%s';c=CGI.new'html4';n,d=c['n']!=''?c['n']:H,c['d'];t=`
cat #{n}`;d!=''&&`echo #{t=CGI.escapeHTML(d)} >#{n}`;c.instance_eval{out{h1{n}+
a(B%H){H}+pre{t.gsub(/([A-Z]\w+){2}/){a(B%$&){$&}}}+form("get"){textarea('d'){t
}+hidden('n',n)+submit}}}
이런 요상한 코드가 있으면 아래처럼 읽을 수 있는 코드로 바꿔준다.
- H = , B = = ["HomePage", "w7.cgi?n=%s"]
c = CGI.new("html4")
n, d = (not (c["n"] == "")) ? (c["n"]) : (H), c["d"]
t = `\ncat #{n}`
((not (d == "")) and `echo #{t = CGI.escapeHTML(d)} >#{n}`)
c.instance_eval do
out do
(((h1 { n } + a((B % H)) { H }) + pre { t.gsub(/([A-Z]\w+){2}/) { a((B % $&)) { $& } } }) + form("get") { ((textarea("d") { t } + hidden("n", n)) + submit) })
end
end
ParseTree와 Ruby2Ruby를 이용해 또 어떤 재미있는 일을 할 수 있을까?
Forbidden Fruit: A Taste of Ruby's ParseTree
Goruko2008에 이와 관련된 재미있는 발표가 있으니 한번 확인해보자 . 아래는 루비 코드를 SQL로 바꿔주는 참신한 ORM인 Ambition을 공개한 Chris Wanstrath의 발표자료를 보자. 동영상도 공개되어 있다.
Chris는 ParseTree와 Ruby2Ruby를 이용해서 이런 일을 한다.
- mapreducerb: RingyDingy를 이용한 간단한 분산 처리
- sake: rake 코드를 만들어내기 위해 ruby2ruby를 사용한다.
- Ambition: 루비 Enumerable을 이용한 코드를 SQL, ActiveRecord 코드, LDAP 쿼리 등으로 변환한다.
다시 한번 생각해보자. 또 어떤 재미있는 일을 할 수 있을까?
프로덕션에서도 사용가능하다: merb의 예
성능을 떨어뜨리는 코드는 버그라는 철학을 가진 Merb에서도 ruby2ruby를 사용한다. merb-action-args가 그 예인데 요청의 쿼리 파라메터를 컨트롤러의 액션 메서드의 파라매터로 매핑해주는 역할을 한다. def bar(baz) 라는 액션이 있고, "/foo/bar?baz=bat"라는 요청이 들어오면 foo("bat")을 호출해주는 식이다. 조금 더 자세한 설명은 이 링크에서 확인할 수 있다.
이 사용예에서의 핵심은 특정 루비 메서드의 파라메터 목록을 런타임에 찾아내는 아래 코드다.
예를 들어, 아래같은 Example 클래스가 있다고 하자.
- class Example
def hello(one,two="two",three)
end
def goodbye
end
end
get_args 메서드를 사용하면 메서드의 파라메터와 기본값을 알 수 있다.
- Example.instance_method(:hello).get_args
#=> [[:one], [:two, "two"], [:three, "three"]]
Example.instance_method(:goodbye).get_args
#=> nil
구현은 아래와 같다.
- def get_args
klass, meth = self.to_s.split(/ /).to_a[1][0..-2].split("#")
# Remove stupidity for #<Method: Class(Object)#foo>
klass = $` if klass =~ /\(/
ParseTreeArray.translate(Object.const_get(klass), meth).get_args
end
merb-action-args는 구동시에 Merb::AbstractController를 상속받는 모든 클래스의 메서드에 대해 파라매터 정보를 구축해놓고 있다가, 디스패치 작업에 이용한다.
메타프로그래밍의 재미
메타프로그래밍은 조금 더 일을 잘하기 위한 노력이다. 약간이라도 읽기 쉬운 코드, 반복을 최대한 줄이고 효율적인 작업이 가능하려면 어떻게 해야할까?를 고민하다보면 어느새 메타프로그래밍에 푹 빠지게 된다. 루비는 메타프로그래밍을 자연스럽게 할 수 있는 언어다. 그리고 다양한 예제와 프랙티스, 그리고 도구들이 있다. 남은 것은 코드를 조금 더 잘 만들려는 개발자들의 작은 열정과 틀을 깨는 상상력이 아닐까 싶다.
참조




October 24th, 2008 at 01:28 AM 와.. 뽐뿌 장난 아니다. ㅎㅎ 역시 딥쁠
October 26th, 2008 at 10:40 PM JasonPA// ^^
October 30th, 2008 at 05:34 PM 올만에 와서 좋은글 보고갑니다. <br/>이거 장난아니신데요 ^0^ ㅋㅋ <br/> <br/>Ruby2Ruby를 이용하면 Code Beautifier를 정말 간단하게 구현할 수 있다.. 요 부분..제대로 파악하고 간다능! ㅋㅋ <br/> <br/>