Edited at

Gobyの組み込みクラスにメソッドを追加する方法

More than 1 year has passed since last update.

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でスクリプトを書いて追加できます!


lib/file.gb

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


https://github.com/goby-lang/goby/blob/v0.1.3/lib/file.gb

.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
}

https://github.com/goby-lang/goby/blob/v0.1.3/vm/file.go#L357


Goで書く場合

vm以下のクラス名に対応したファイルのBuiltinMethodObjectのマップに以下のようなエントリを追加すると組み込みメソッドを追加できます。


vm/file.go

        {

// 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
}
},
},


https://github.com/goby-lang/goby/blob/v0.1.3/vm/file.go#L144,L202


どうやって書けばいいの?

Fileのように、基本的なAPIをGoで実装しリッチなAPIをgobyで書くとよさそう!


例: methodsメソッドを追加する

methodsメソッドのようなどのオブジェクトにも備わっているようなメソッドはvm/class.goで定義されています。


TDD

実装する前にテストを書きましょう! gobyのテストはGoで書かれています。


vm/class_test.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を返します。


vm/class.go

        {

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を使いましょう!


vm/class.go

        {

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の種類がないので、そのあたりは考慮しないことにします。

テストケースを追加します


class_test.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},
{`
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プログラムと変わらないですね!


class.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にないメソッドを確認しました (圧倒的成長できそうな気配 :muscle: :muscle: :muscle: )。

足りなかったら足すだけ!

それでは。