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

[RubyKaigi2017で発表のあった](http://rubykaigi.org/2017/presentations/_st0012.html)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でスクリプトを書いて追加できます!

```rb: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`に追加します。

```rb
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`のマップに以下のようなエントリを追加すると組み込みメソッドを追加できます。

```go: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](https://github.com/goby-lang/goby/blob/v0.1.3/vm/class.go)で定義されています。

### TDD

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

```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を返します。

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

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

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

```go: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プログラムと変わらないですね!

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

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

それでは。