Ruby
Go
Goby

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: )。

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

それでは。