[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 真常
0 retries:
Post a Comment
Note: Only a member of this blog may post a comment.