Kobako:Handle 機制對許多支援腳本語言的專案並不算太特殊的機制,但是 Block 概念算是 Ruby 語言特有的能力,如果使用沙盒(Sandbox)時無法善用,那就太可惜。
然而,像這樣的寫法真的做得到嗎?
1sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
2sandbox.eval("Seq::Map.call([1, 2, 3]) { |x| x * 2 }").value
3
4# => [2, 4, 6]我們竟然可以在沙盒裡面用一個 Block 接收外部的呼叫。
Enumerator::Yielder
為了讓這件事情實現,我們需要有一種「幫忙把 Block 交接」這樣的設計,剛好在 CRuby 裡面有一個物件可以表達這樣的行為。
1y = Enumerator::Yielder.new do |x|
2 puts x
3end
4
5[1, 2, 3].each(&y)上面看起來並不特殊,使用 Lambda 似乎也能做到相同的效果,但這裡最抽象的地方是 Enumerator::Yielder.new 收到的 Block 是使用他的人。
我們可以來看 Enumerator 的例子
1e = Enumerator.new do |y|
2 pp y # => Enumerator::Yielder
3 y << 1
4 y.yield 2
5end
6
7e.each { |x| puts x }在這個情境 y 會是一個 Enumerator::Yielder 物件,他收到的 Block 是來自 e.each 的那一個,利用這個特性,就可以把沙盒中的那一個 Block 變成外部可以使用的版本。
Kobako::Transport::Yielder
當我們在 mruby 端呼叫一個 Block 是屬於外部物件時,會先發起向外的呼叫,並且把這個內部的 Proc 指標記錄下來,等待外部呼叫。
1sandbox.eval("Sequence.map { |x| x * 2 }")此時對於 CRuby 來說,看到的是一個 mruby 的記憶體位置,表示 mruby 中的 Proc 也就是 { |x| x * 2 } 這一個 Block 但是無法呼叫。
Kobako 會把這個無法呼叫的記憶體位置,用 Kobako::Transport::Yielder 包裝起來,變成跟 Enumerator::Yielder 相同的概念。
1sandbox.bind("Sequence", Enumerator.new { |y| y << 1 })
2sandbox.eval("Sequence.map { |x| x * 2 }").value # [2]在這個情境中 Kobako 巧妙的把宿主(Host)和客體(Guest)的呼叫隱藏起來,只留下 CRuby 原生的 Yielder 概念,讓 Block 看起來在內外都是可用的。
範例的寫法在新版的 Kobako 會失敗,為了加強安全性,只有使用者明確定義的方法才會預設被放行,如果是
Enumerator這類原生的#each和#map會被拒絕,這是因為有多個逃逸漏洞,都是利用這類邊界達成,因此在新版禁止使用。
語言特性
在 AI 能夠快速、大量實作功能的當下,我認為這種善用語言特性的機制反而更加難得,因為他不是單純的堆疊特性,而是仔細思考如果要讓使用者獲得直覺、原生的體驗可以怎樣設計。
這個特性在很多地方都非常有用,像是搭配 Rails 時,我們可以輕鬆的做出這樣的設計。
1class MetricService
2 def initialize(metric)
3 @metrics = Metric.where(metric: metric)
4 end
5
6 def each(&block) = @metrics.each(&block)
7 def map(&block) = @metrics.map(&block)
8
9 def by_day
10 @metrics = @metrics.group(:date)
11 self
12 end
13
14 def count
15 @metrics = @metrics.count
16 self
17 end
18
19 def sum_of(type)
20 @metrics = @metrics.sum(type)
21 self
22 end
23end
24
25sandbox.bind("Metric", MetricService.new(:page_view))
26sandbox.eval("Metric.by_day.sum_of(:value).map { ... }").value只需要用 CRuby 包裝特定的物件,就可以讓 mruby 像是原生的方式處理資料,這對於 Agent 來說會比讀出資料在自己處理更容易,因為可以直接寫一段程式來處理資料,就不需要把中間過程留在 Context(上下文)消耗額外的 Token 來處理。
這也是 Kobako 最初的想法,要讓 Ruby 生態系有一個可以使用的 Code Mode,不依賴容器技術等機制,而是簡單、輕量就可以使用的選擇。
喜歡這篇文章?請我喝杯奶茶 🧋