RubyKaigi2017で発表のあったGoで書かれたRuby風のプログラミング言語、Goby!
作者の @_st0012 さんにGobyにメソッドを追加する方法を教えていただいたので紹介します!
Goby
Goby - Inherits from Ruby, extended with Golang
https://sample.goby-lang.org/
gobyからはGoで書かれたプログラムをGoのPlugin機構経由で呼び出して使ったりできるようです。
rubyにある便利メソッドがまだまだ足りてないみたいで、絶賛コントリビューター募集中のようです!
https://github.com/goby-lang/goby/blob/master/CONTRIBUTING.md
どこに書けばいいの?
gobyで書く場合
lib/
ディレクトリ以下にふつうのRubyのような雰囲気でgobyでスクリプトを書いて追加できます!
class File
def self.open(filename, mode = "r", perm = 0755)
file = new(filename, mode, perm)
if block_given?
yield(file)
end
file.close
end
end
.gb
ファイルのファイル名をクラスの初期化処理の部分でvm.libFiles
に追加します。
func (vm *VM) initFileClass() *RClass {
fc := vm.initializeClass(classes.FileClass, false)
fc.setBuiltinMethods(builtinFileClassMethods(), true)
fc.setBuiltinMethods(builtinFileInstanceMethods(), false)
vm.libFiles = append(vm.libFiles, "file.gb")
return fc
}
Goで書く場合
vm
以下のクラス名に対応したファイルのBuiltinMethodObject
のマップに以下のようなエントリを追加すると組み込みメソッドを追加できます。
{
// Finds the file with given filename and initializes a file object with it.
//
// ```ruby
// File.new("./samples/server.gb")
// ```
// @param filename [String]
// @return [File]
Name: "new",
Fn: func(receiver Object) builtinMethodBody {
return func(t *thread, args []Object, blockFrame *callFrame) Object {
var fn string
var mode int
var perm os.FileMode
if len(args) < 1 {
return t.vm.initErrorObject(errors.InternalError, "Expect at least a filename to open file")
}
if len(args) >= 1 {
fn = args[0].(*StringObject).value
mode = syscall.O_RDONLY
perm = os.FileMode(0755)
if len(args) >= 2 {
m := args[1].(*StringObject).value
md, ok := fileModeTable[m]
if !ok {
return t.vm.initErrorObject(errors.InternalError, "Unknown file mode: %s", m)
}
if md == syscall.O_RDWR || md == syscall.O_WRONLY {
os.Create(fn)
}
mode = md
perm = os.FileMode(0755)
if len(args) == 3 {
p := args[2].(*IntegerObject).value
perm = os.FileMode(p)
}
}
}
f, err := os.OpenFile(fn, mode, perm)
if err != nil {
return t.vm.initErrorObject(errors.InternalError, err.Error())
}
// TODO: Refactor this class retrieval mess
fileObj := &FileObject{File: f, baseObj: &baseObj{class: t.vm.topLevelClass(classes.FileClass)}}
return fileObj
}
},
},
どうやって書けばいいの?
File
のように、基本的なAPIをGoで実装しリッチなAPIをgobyで書くとよさそう!
例: methodsメソッドを追加する
methodsメソッドのようなどのオブジェクトにも備わっているようなメソッドはvm/class.goで定義されています。
TDD
実装する前にテストを書きましょう! gobyのテストはGoで書かれています。
func TestMethods(t *testing.T) {
tests := []struct{
input string
expected bool
}{
{`
class C
def hola
end
end
C.new.methods.join(", ").include?("hola")
`, true},
}
for i, tt := range tests {
v := initTestVM()
evaluated := v.testEval(t, tt.input, getFilename())
checkExpected(t, i, evaluated, tt.expected)
v.checkCFP(t, i, 0)
v.checkSP(t, i, 1)
}
}
tests
に複数のテストケースをまとめてfor
で回してチェックします。
initTestVM()
でテスト用のgobyのVMを初期化し、testEval
メソッドでgobyスクリプトを実行、返った結果をcheckExpected
で期待値と合っているか確認しています。
checkCFP
/checkSP
はVMが正しく終了したかどうか確認するために使っているようです。
難しいですがfor
の中はコピペで大丈夫です!
実装する
メソッド名はmethods
なのでName
には"methods"
をわたします。
Fnにはメソッドの定義をわたします。メソッド定義ではレシーバーを引数でうけとり、thread, 引数に渡したオブジェクトの配列, メソッド呼び出しのcallFrame、3つの引数を取るfuncを返します。
{
Name: "methods",
Fn: func(receiver Object) builtinMethodBody {
return func(t *thread, args []Object, blockFrame *callFrame) Object {
// ...
}
},
},
ここではテストを通すため単純に"hola"
と書かれた配列を返しましょう。
GobyのVMにはGoの構造体をGobyのオブジェクトに変換するAPIがいくつか生えています。
initObjectFromGoType
を使うと配列などを再帰的にGobyのオブジェクトに変換します。
他にもinit
ではじまるGoのオブジェクトをGobyオブジェクトに変換するメソッドがいくつかあります。
変換する元のGoのオブジェクトの型がわかっている場合、それらを使う方が良いです。
が、今回はお手軽なのでinitObjectFromGoType
を使いましょう!
{
Name: "methods",
Fn: func(receiver Object) builtinMethodBody {
return func(t *thread, args []Object, blockFrame *callFrame) Object {
methods := []interface{}{"hola"}
return t.vm.initObjectFromGoType(methods)
}
},
},
Goの構造に詰めてinitObjectFromGoType
に渡すだけなのでお手軽ですね。
ガチ実装する
Rubyのmethods
メソッドの動作を見ると、シングルトンクラスのメソッドや親クラスのメソッドも取れる必要がありそうです。gobyのメソッドにはpublic/protected/privateの種類がないので、そのあたりは考慮しないことにします。
テストケースを追加します
func TestMethods(t *testing.T) {
tests := []struct{
input string
expected bool
}{
{`
class C
def hola
end
end
C.new.methods.join(", ").include?("hola, ")
`, true},
{`
class C
end
c = C.new
def c.hola
end
c.methods.join(", ").include?("hola, ")
`, true},
{`
class C
end
C.new.methods.join(", ").include?(", to_s, ")
`, true},
}
for i, tt := range tests {
v := initTestVM()
evaluated := v.testEval(t, tt.input, getFilename())
checkExpected(t, i, evaluated, tt.expected)
v.checkCFP(t, i, 0)
v.checkSP(t, i, 1)
}
}
詳細は省いて実装します!! シングルトンクラスがあればシングルトンクラスから先に辿って、メソッド一覧の重複を省いて返すだけなので、パット見ふつうのGoプログラムと変わらないですね!
{
Name: "methods",
Fn: func(receiver Object) builtinMethodBody {
return func(t *thread, args []Object, blockFrame *callFrame) Object {
methods := []interface{}{}
set := map[string]struct{}{}
targets := []*RClass{}
if receiver.SingletonClass() != nil {
targets = append(targets, receiver.SingletonClass())
}
targets = append(targets, receiver.Class())
for _, klass := range targets {
for klass != nil {
env := klass.Methods
for env != nil {
for key := range env.store {
set[key] = struct{}{}
}
env = env.outer
}
if klass.superClass == klass {
break
}
klass = klass.superClass
}
}
for k := range set {
methods = append(methods, k)
}
return t.vm.initObjectFromGoType(methods)
}
},
},
実行結果を確認する
無事メソッド一覧が取れました!
% ~/bin/goby -i
Goby 0.1.3 👿 🙄 💀
» [].methods
#» ["select", "+", "clear", "nil?", "rotate", "unshift", "push", "any?", "require", "to_s", "is_a?", "class", "thread", "delete_at", "[]", "count", "shift", "reverse_each", "pop", "reduce", "new", "reverse", "require_relative", "block_given?", "last", "!=", "sleep", "map", "concat", "first", "==", "instance_variable_set", "[]=", "join", "empty?", "instance_variable_get", "values_at", "flatten", "length", "each", "each_index", "singleton_class", "at", "puts", "!", "send", "methods"]
ちなみにRubyの場合はこんな感じなので、gobyは伸びしろが有りますね!
% irb
irb(main):001:0> [].methods
=> [:each_index, :join, :rotate, :rotate!, :sort!, :sort_by!, :collect!, :map!, :select!, :keep_if, :values_at, :delete_at, :to_h, :delete_if, :reject!, :transpose, :include?, :rassoc, :uniq!, :assoc, :compact, :compact!, :flatten!, :shuffle!, :shuffle, :fill, :permutation, :combination, :sample, :repeated_permutation, :repeated_combination, :flatten, :bsearch, :product, :bsearch_index, :&, :*, :+, :-, :sort, :count, :find_index, :select, :reject, :collect, :map, :first, :any?, :pack, :reverse_each, :zip, :take, :take_while, :drop, :drop_while, :cycle, :sum, :uniq, :|, :insert, :index, :rindex, :<=>, :<<, :clear, :replace, :==, :[], :[]=, :empty?, :eql?, :reverse, :reverse!, :concat, :max, :min, :inspect, :length, :size, :each, :delete, :to_ary, :slice, :slice!, :to_a, :to_s, :dig, :hash, :frozen?, :at, :fetch, :last, :push, :pop, :shift, :unshift, :find, :entries, :sort_by, :grep, :grep_v, :detect, :find_all, :flat_map, :collect_concat, :inject, :reduce, :partition, :group_by, :all?, :one?, :none?, :minmax, :min_by, :max_by, :minmax_by, :member?, :each_with_index, :each_entry, :each_slice, :each_cons, :each_with_object, :chunk, :slice_before, :slice_after, :slice_when, :chunk_while, :lazy, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, :remove_instance_variable, :public_method, :singleton_method, :instance_variable_set, :define_singleton_method, :method, :extend, :to_enum, :enum_for, :===, :=~, :!~, :respond_to?, :freeze, :object_id, :send, :display, :nil?, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variable_get, :instance_variables, :instance_variable_defined?, :!, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
まとめ
Gobyの組み込みクラスにメソッドを追加する方法を2つ紹介しました。
- Gobyで書く
- Goで書く: GoのオブジェクトをGobyのオブジェクトに変換する方法を紹介しました
実装したmethodsメソッドで今rubyにあってgobyにないメソッドを確認しました (圧倒的成長できそうな気配 )。
足りなかったら足すだけ!
それでは。