[Ruby] recursive lambda
==本文連同引文同步載於
ptt Ruby 板、
LightyRoR、
飽和脂肪星(
星之一角備份區)==
很抱歉最近狀況真的是相當糟糕,導致很多事情都沒做或是沒做好。雖然以後大概也不會比較好。這樣講講就沒關係嗎?當然不是,只是替自己找一點比較能安心的藉口吧。另外本文有任何錯誤歡迎指出。
==本文開始==
我一直覺得 Ruby 缺少一個類似 self 的東西,用來表達現在這個 function/method. 這個東西有什麼用呢?其實我也不知道有什麼用,就只是單純覺得好像少了這種東西。最直覺的例子,恐怕就是具有遞迴能力的 lambda function. 我曾在 ptt Ruby 板發過一篇文,講
quine(self-reproducing programs),後來我用了
Ruby2Ruby, 寫了像這樣的結果:(
飽和脂肪星有該文的備份(通常連不上))
#!/usr/bin/ruby
require 'rubygems'
require 'ruby2ruby'
(a = proc {
puts("#!/usr/bin/ruby")
puts
puts("require 'rubygems'")
puts("require 'ruby2ruby'")
puts
print("(a = ")
print(a.to_ruby)
print(").call")
}).call
最蠢的地方是明明都用 lambda(proc) 了,我卻還得把 lambda 的結果記起來留待以後使用。這樣實在是有點無趣。我希望我可以寫:
lambda{ print(this.to_ruby); print(".call") }.call
這樣不是帥氣多了嗎?於是我開始試著思索實作這東西的可能。接著我忽然想到,所謂 this 不正是指在 call stack 最上端的 function/method 嗎?因為當我們執行到這個 function/method 時,this 一定是指同一 function, 不可能忽然去指涉其他 function, 而另外一個 function 進 call stack 時,不把他解掉也不可能會執行到 this. 於是可以把 this 寫成一個 function, 不吃任何引數,回傳一個 Proc/Method 代表正在 call stack 頂端的那個 function.
而我記得 Ruby 是有方法可以去存取 call stack... 雖然好像是用很蠢的方法,也確實是有點蠢,但總之可以用模擬的。隨意
google 了一下,找到一個很簡單的方式,就是用 set_trace_func, 丟一個 callback 進去,於是 Ruby 在各個 function 間做動作的時候,都會呼叫這個 callback. 感覺就是效率會變狂差,不過呢,至少暫時是可以用的。
接著可以利用 Thread.current[:symbol] 來儲存 current thread call stack info, 任何 function call 時,push 資料進這個假的 call stack, function return 時,pop 資料出來。這樣就有一個很簡單的 call stack info 可以用了。
以下程式歡迎任意使用,licensed under Apache License 2.0, 複製到檔案用的話希望可以把以下這段 copy 到檔案最前面… XD
# Copyright (c) 2007, Lin Jen-Shin(a.k.a. godfat 真常)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Kernel
# 由於 google 到的參考程式把抓 call stack 叫 invoker,
# 所以這裡沿用他的名字。有個常數比較容易看懂程式在做什麼。
INVOKER_EVENT = 0
INVOKER_FILE = 1
INVOKER_LINE = 2
INVOKER_MSG = 3
INVOKER_BINDING = 4
INVOKER_CLASS = 5
# -1 就是 top 的意思囉
def invoker levels = -1
st = Thread.current[:callstack]
# st 有可能是 nil, 如果 invoker 先被 call 到的話。
# 雖然我不知道什麼時候會發生這種事…。levels - 2 的原因是:
# 0(stack bottom) => function that you called(this is what we want)
# 1 => Kernel#invoker
# 2 => Array #[]
# 所以去掉額外不要的額外兩個資訊。
st && st[levels - 2]
end
def this
# 因為多 call 了 this, 所以要再多去掉一個額外資訊。
info = invoker(-2)
# 這邊我本來寫成 lambda 的形式,可以正確執行,但有一個狀況
# 卻是失敗的。就是 lambda{yield}.call{} 這樣是會 error 的 :(
# 試了多次還是找不到 Proc.call 吃 block({}) 的方法,只好改寫
# 成用 Method 的形式。不知為何,Method 就可以正確使用 block...
eval("self", info[INVOKER_BINDING]).method(info[INVOKER_MSG])
end
end
set_trace_func lambda{ |*args|
case args[INVOKER_EVENT]
# 可能的有 call 和 c-call, 都是 function
when /call$/
# 這邊我搞不清楚到底是誰會先被 call, 是 call 還是 return?
# google 來的是把初始化寫在 call 裡,可是我測試都顯示是在
# return 上,所以反而是 return 的地方需要初始化。或是乾脆
# 全部拉出來在最上面初始化也可以 :)
(Thread.current[:callstack] ||= []).push args
# 同上可能有 return 和 c-return
when /return$/
(Thread.current[:callstack] ||= []).pop
end
}
以上不管有沒有問題,都可以來看一下我額外寫的幾個 unit test, 參考一下幾個我目前想到的用法。
require 'test/unit'
class TestThis < Test::Unit::TestCase
def test_fact
assert_equal(120, fact(5))
assert_equal(3628800, fact(10))
# 試用 recursive lambda
assert_equal(5040, lambda{|n| return n*this[n-1] if n>0; 1}[7])
end
def fact n
# 恐怕是最常見的用法
return n*this[n-1] if n > 0
1
end
##
def test_pass_around
# 這邊流程可能很怪,因為只是我隨便寫的,單純測試正確性罷了。
assert_equal(method(:pass_around_forward), pass_around.call(lambda{|v| v}))
end
def pass_around mode = 'pass'
case mode
when 'pass'
pass_around_forward this
else
'value'
end
end
def pass_around_forward func
assert_equal('value', func['value'])
this
end
##
def test_with_block
# 同上,流程亂寫的,單純測試正確性。
with_block{|b| assert_equal('value', b['value'])}
end
def with_block mode = 'pass', &block
case mode
when 'pass'
block[this]
else
'value'
end
end
##
def test_more_args
# Proc 就是死在這個測試,block 展開怎麼做都失敗 :(
# 改成 Method 後這邊就可以通過測試了。
more_args('get_this'){}.call('call', 1, 2, 3, 4, 5, &lambda{6})
more_args('get_this'){}.call('call', 1, 2, 3, 4, 5){6}
end
def more_args mode, a1=nil, a2=nil, a3=nil, *as, &block
case mode
when 'get_this'
this
else
assert_equal(1, a1)
assert_equal(2, a2)
assert_equal(3, a3)
assert_equal(4, as[0])
assert_equal(5, as[1])
assert_equal(nil, as[2])
assert_equal(6, yield)
assert_equal(6, block.call)
end
end
end
最後,我可以把 quine 改寫成:
#!/usr/bin/ruby
require 'rubygems'
require 'ruby2ruby'
require 'invoker'
def f
puts("#!/usr/bin/ruby")
puts
puts("require 'rubygems'")
puts("require 'ruby2ruby'")
puts("require 'invoker'")
puts
print(this.to_ruby)
print("\nf")
end
f
為什麼要用 def f; end 呢??這樣不就失去 this 的意義了??因為不曉得為什麼,Ruby2Ruby 跑 lambda + this 都會有奇怪的 runtime error, 而這個錯誤是來自 class RubyToRuby 的 rewrite_defn(exp), 最後的:「raise "Unknown :defn format: #{name.inspect} #{args.inspect} #{body.inspect}"」這我一時不知道要怎麼解決,不知道會是誰的錯,所以只好暫時用 def f; end 這種蠢方式了。
另外,還有一些無聊的花枝可以玩:
def just_print_it n
print n
this
end
然後就可以:
just_print_it(1)[2][3][4][5]
輸出:12345
def add_attr attr
($attr ||= []).push attr
this
end
add_attr('hello')['world']['and then?']['good-bye']
或是很簡單的流程控制:
def fight step = :ready
case step
when :ready
ready this
when :go
go this
when :finish
finish this
end
end
def ready callback
play_ready_animate
if blah
callback[:go]
else
callback[:finish]
end
cleanup_ready
end
def go callback
gogogo
if blah
callback[:finish]
else
callback[:ready]
end
cleanup_go
end
類推,我也不知道有什麼用,搞不好有什麼 cleanup 是要等所有的事都做完才能做?總而言之,就是這樣啦,沒什麼營養,但我卻覺得很重要的功能。在很多時候可以少打很多字。搞不好哪一天也會想到什麼真的很有價值的應用也說不定。總覺得真的有太多事是無心插柳柳橙汁。雖然大部份都來不及喝就乾掉了。
2007.04.16 godfat 真常