[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 真常