Monkey言語:組み込み関数playの実装

テストを書く

evaluator/evaluator_test.goに次のようなテストを書く.

func TestPlayFunction(t *testing.T){
	tests := []struct {
		input string
		expected interface{}
	}{
		{`play(60)`, 60},
		{`play("60.0")`, "argument to `play` not supported, got FLOAT"},
		{`play("60")`, "argument to `play` not supported, got STRING"},
		{`play(60, 0.5)`, "wrong number of arguments. got=2, want=1"},
	}
	for _, tt := range tests{
		evaluated := testEval(tt.input)

		switch expected := tt.expected.(type){
		case int:
			testIntegerObject(t, evaluated, int64(expected))
		case string:
			errObj, ok := evaluated.(*object.Error)
			if !ok {
				t.Errorf("object is not Error. got=%T (%+v)",
					evaluated, evaluated)
				continue
			}
			if errObj.Message != expected {
				t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message)
			}
		}
	}


引数としては現状MIDIノートナンバーを示すInt一つだけに限定している.後程,第二引数として音価などを取り込むかもしれないがまずは単純化のために周波数用の引数だけで.
引数が0のときもエラーを出すようにも後程する.

期待通りの引数が来たら音を鳴らしてMIDIノートナンバーをそのまま返させ,違ったらメッセージが出るのを想定している.

組み込み関数を認識させる

playの定義をして,認識できるようにする.

まず,組み込み関数たのための環境を保持できるようにする.
そのために,evaluator/builtins.goに次のコードを付け加える.

    "play": &object.Builtin{
		Fn: func(args ...object.Object) object.Object{
			if len(args) != 1{
				return newError("wrong number of arguments. got=%d, want=1", len(args))
			}
			switch arg := args[0].(type){
			case *object.Integer:
				return &object.Integer{Value: arg.Value}
			default:
				return newError("argument to `play` not supported, got %s", args[0].Type())
			}
		},
	},

引数が1でない時はエラーを出すようにしている,これで引数がない時もエラーがでる.
引数の型がIntegerのときは引数をそのまま返し,そうでない場合は型が違うとエラーを出すようにしている.

これを利用するためのevaluator/evaluator.goのevalIdentifierは既に記述してあるはずであるが,一応載せておく.

func evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object{
	if val, ok := env.Get(node.Value); ok{
		return val
	}
	if builtin, ok := builtins[node.Value]; ok{
		return builtin
	}
	return newError("identifier not found: " + node.Value)
}


これによって,与えられた識別子が現在の環境で値に束縛されていない場合に,フォールバックして組み込み関数を探すようにしている.

テスト

この状態で次のようにテストをすると,きちんと通過するはず.

MacBook-Air halo % go test ./evaluator
ok      monkey/evaluator        0.267s


現状の動き確認

現時点でplay()を使うとどのようになるのかを確認する.

MacBook-Air halo % go run main.go
Hello sonoyamayuto. This is The Monkey Programming language.
Feel free to type in commands
>> play(60);
60
>> play(34, 45);
ERROR: wrong number of arguments. got=2, want=1
>> play();
ERROR: wrong number of arguments. got=0, want=1
>> play("2");
ERROR: argument to `play` not supported, got STRING
>> play(2.00);
ERROR: argument to `play` not supported, got FLOAT


ちゃんと期待通り動いてるみたい.


引数を追加する

ここで紹介している,ブラウザ上のテキストボックスに書いたものを評価するコードでは,play()関数の引数は,int型のMIDIノートナンバーとfloat64型の音長の二つなので,これに統一する.

テストケースの変更

func TestPlayFunction(t *testing.T){
	tests := []struct {
		input string
		expected interface{}
	}{
		{`play(60)`, "wrong number of arguments. got=1, want=2"},
		{`play(60, 1)`, "2nd argument to `play` must be float64, got INTEGER"},
		{`play(60.0, 0.3)`, "1st argument to `play` must be int, got FLOAT"},
		{`play(60, "0.3")`, "2nd argument to `play` must be float64, got STRING"},
		{`play("60")`, "wrong number of arguments. got=1, want=2"},
		{`play(60, 0.5)`, 60},
		{`play()`, "wrong number of arguments. got=0, want=2"},
	}
	for _, tt := range tests{
		evaluated := testEval(tt.input)

		switch expected := tt.expected.(type){
		case int:
			testIntegerObject(t, evaluated, int64(expected))
		case string:
			errObj, ok := evaluated.(*object.Error)
			if !ok {
				t.Errorf("object is not Error. got=%T (%+v)",
					evaluated, evaluated)
				continue
			}
			if errObj.Message != expected {
				t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message)
			}
		}
	}
}

変えたのはtestsの内容の一部だけ.

builtins.goの修正

playの部分を修正して引数二つのものにする.
構造は単純で,引数の型検査を縦に繋げて2つにすればよい.

"play": &object.Builtin{
		Fn: func(args ...object.Object) object.Object{
			if len(args) != 2{
				return newError("wrong number of arguments. got=%d, want=2", len(args))
			}
			switch args[1].(type){
			case *object.Float:
				switch arg1 := args[0].(type){
				case *object.Integer:
					//ここにWebAudioAPIの処理をいれる.
					return &object.Integer{Value: arg1.Value}
				default:
					return newError("1st argument to `play` must be int, got %s", args[0].Type())
				}
			default:
				return newError("2nd argument to `play` must be float64, got %s", args[1].Type())
			}
		},
	},


修正後の実行

テストケースが通るか試してみる.

MacBook-Air halo % go test ./evaluator
ok      monkey/evaluator        0.261s

ちゃんと通った.
main.goを実行してインタプリタを起動させて,実際の動きを見てみる.

MacBook-Air halo %  go run main.go
Hello sonoyamayuto. This is The Monkey Programming language.
Feel free to type in commands
>> play(60);
ERROR: wrong number of arguments. got=1, want=2
>> play(60, 0.3);
60
>> 

ちゃんと期待通り動いた.

この記事が気に入ったらサポートをしてみませんか?