4
0

More than 1 year has passed since last update.

インタープリター版Go言語、gomacroを触ってみた

Posted at

はじめに

Go言語のインタープリターを探していたところ、gomacroという処理系を見つけました。

独自機能も多く、処理系を超えて「Go言語の方言」になっていると感じたので、本記事ではgomacroの機能について紹介したいと思います1

以下は記事執筆時点でのmasterブランチを使用しています。

REPL

PythonやRuby等のような、対話的にコードを実行できるモードがあります。1行おきに式がそのまま評価されます。

// Welcome to gomacro. Type :help for help, :copy for copyright and license.
// This is free software with ABSOLUTELY NO WARRANTY.
gomacro> 6 * 7
{int 42}        // untyped.Lit

もちろんprintも可能です。ちゃんと戻り値も全て表示されます。

gomacro> import "fmt"
gomacro> fmt.Println("Hello, world!")
Hello, world!
14      // int
<nil>   // error

関数や制御構文を書く場合は、ブロックを閉じるまで評価を待ってくれます。安心して改行しましょう。

gomacro> func message(name string) (string, error) {
. . . .    if name == "" {
. . . .      return "", fmt.Errorf("name must not be empty")
. . . .      }
. . . .    return fmt.Sprintf("Hello, %s", name), nil
. . . .    }

gomacro> message("")
        // string
name must not be empty  // error

gomacro> message("Taro")
Hello, Taro     // string
<nil>   // error

終了する場合は :quit で抜けられます。そのたのコマンドも :help で確認できます。

gomacro> :quit

numberとメソッド

gomacroでは、Goには無い独自の型が定義されています。
Goでは数値の定数はuntyped int/floatに推論されますが、gomacroでは untyped.Lit 型として扱われます。

gomacro> 3 * 2.4
{float64 36/5}  // untyped.Lit
gomacro> 60 * 4
{int 240}       // untyped.Lit
gomacro> 3 / 4
{int 0} // untyped.Lit
gomacro> 3 / 4.0
{float64 3/4}   // untyped.Lit

untyped int/float との違いとして、untyped.Lit*big.Int, *big.Rat, *big.Float のメソッドが使用可能です。

gomacro> (2).Cmp(3)
-1      // int
# これはsyntax error
gomacro> 2.Cmp(3)
repl.go:1:3: expected ';', found 'IDENT' Cmp
# *math.Intの仕様通りだけど少し使いにくいかも(レシーバは無視される)
gomacro> (2).Neg(3)
-3      // int

コンストラクタ

T 型のゼロ値を T() で生成可能です2

gomacro> x := int()
gomacro> x
0       // int
gomacro> string()
        // string

もともと {} で初期化可能なものでもコンストラクタは使用可能です。使いどころは...

gomacro> []int{}
[]      // []int
gomacro> []int()
[]      // []int

ジェネリクス(試験導入)

gomacroはGo本家よりも前からジェネリクスが使用可能でした。そのため、2019年のProposal (Contract) に基づいた独自構文となっています。

とはいえ、見た目が異なるだけで使い方は同様です。

gomacro> func Print#[T](x T) {
. . . .    fmt.Println(x)
. . . .    }
gomacro> import "fmt"
gomacro> Print(1)
1
gomacro> Print("abc")
abc
gomacro> Print()

構造体も型パラメータが取れますが、メソッドに型パラメータを追加することはできませんでした。これも本家と同じです。

gomacro> type MyStr#[T] struct {
. . . .    Value T
. . . .    }
gomacro> func (m *MyStr) Print#[S](other S) {
. . . .    fmt.Printf("%+v %+v\n", m.Value, other)
. . . .    }
repl.go:1:1: generic method declaration not yet implemented: func (m *MyStr) Print#[S](other S) { fmt.Printf("%+v %+v\n", m.Value, other) }

異なる点としては、gomacroのcontract (Goのconstraintに相当)ではtype setの指定はできません。

// golang.org/x/exp/constraints
type Ordered interface {
	// これ
	Integer | Float | ~string
}

type listはメソッドを取らない型の演算子使用可否を判定するために使われるので、リテラルでもメソッドを持っているgomacroでは不要となったのかもしれません。

(Goにtype setが導入された背景は以下の記事を参考にさせていただきました)

とはいえ、gomacroのジェネリクスはbeta版なのでcontractはまだ使えませんでした。残念。

// Readmeより引用
type Comparable#[T] interface {
	// returns -1 if a is less than b
	// returns  0 if a is equal to b
	// returns  1 if a is greater than b
	func (a T) Cmp(b T) int
}

func Min#[T: Comparable] (a, b T) T {
	if a.Cmp(b) < 0 { // also <= would work
		return a
	}
	return b
}
repl.go:1:1: invalid generic function or method declaration: generic parameter 0 should be *ast.Ident or *ast.CompositeLit, found *ast.KeyValueExpr: func Min#[T: Comparable](a, b T) T {
        if a.Cmp(b) < 0 {
                return a
        }
        return b
}

マクロ

最後に、もっとも独自性が強く、かつ gomacro の名前の由来伴った機能、マクロを紹介します。

go templateやC++のような文字列テンプレートではなく、LISPのようにASTを変換するタイプのマクロです。

~' でクオートすることでASTオブジェクトを生成します。Goの ast パッケージのオブジェクトが利用されます。

gomacro> add := ~'{a+b}
gomacro> add
a + b   // *go/ast.BinaryExpr
gomacro> :inspect add
add     = a + b // *ast.BinaryExpr
    0. X        = {NamePos:435 Name:a Obj:&{bad  <nil> <nil> <nil>}}    // ast.Expr
    1. OpPos    = 436   // token.Pos
    2. Op       = +     // token.Token
    3. Y        = {NamePos:437 Name:b Obj:&{bad  <nil> <nil> <nil>}}    // ast.Expr
// type ? for inspector help

関数のように macro 文を定義することで使用可能です。呼び出し時の構文に癖があります。

gomacro> macro myAdd(a, b ast.Node) ast.Node {
. . . .    return ~"{~,a + ~,b}
. . . .    }

gomacro> myAdd; 1; 2
{int 3} // untyped.Lit

gomacro> myAdd; 1; 2 + 3
{int 6} // untyped.Lit

gomacro> myAdd; "x"; "y"
{string "xy"}   // untyped.Lit
macro makefib(typ ast.Node) ast.Node {
    return ~"{
        ~func fib(n ~,typ) ~,typ {
            if n <= 2 {
                return 1
            }
        return fib(n-1) + fib(n-2)
        }
    }
}

とはいえ、astモジュールをもとに作られているため定義できる構文には制約があります。

注意点1: 演算子はマクロ化できない

演算子を挿入できるマクロを作ることはできませんでした。

gomacro> macro infix(a, op, b ast.Node) ast.Node {
. . . .    return ~"{~,a ~,op ~,b}
. . . .    }
repl.go:2:15: expected ';', found '~unquote' (and 1 more errors)

マクロの仕組み上astのオブジェクトで表せるものしか引数に取れませんが、2項演算子は ast.BinaryExpr の1フィールドに過ぎないので差し替えできないようです。

type BinaryExpr struct {
	X     Expr        // left operand
	OpPos token.Pos   // position of Op
	Op    token.Token // operator
	Y     Expr        // right operand
}

注意点2: ジェネリクスの代わりには使えない

C++のテンプレートのように、マクロをジェネリクスの代わりに使用することはできません。

gomacro> import "go/ast"
gomacro> macro makefib(typ ast.Node) ast.Node {
. . . .        return ~"{
. . . .              ~func fib(n ~,typ) ~,typ {
. . . .                    if n <= 2 {
. . . .                          return 1
. . . .                      }
. . . .                return fib(n-1) + fib(n-2)
. . . .                }
. . . .          }
. . . .    }
gomacro> makefib; int
gomacro> makefib; float64
// warning: redefined identifier: fib

これは単純に、(具象型の)関数の宣言がオーバーロードできないためです。素直にジェネリクスを使いましょう

gomacro> func hello(name string) {
. . . .    fmt.Printf("Hello, %s", name)
. . . .    }
gomacro> func hello(times int) {
. . . .    for i := 0; i < times; i++ {
. . . .      fmt.Println("Hello!")
. . . .      }
. . . .    }
// warning: redefined identifier: hello
gomacro> hello(3)
Hello!
Hello!
Hello!
gomacro> hello("Taro")
cannot convert untyped constant {string "Taro"} to <int>

このあたりは文法上の制約もありやむなしです。やはりASTを完全に掌握するにはLISPになるしかないのか...

おわりに

以上、gomacroの紹介でした。設計選択の違いから、Go言語の歴史のifを見ているようで興味深かったです。
ジェネリクスについては本家の仕様に合わせる構想があるようなので、独自構文で遊ぶなら今のうちです。

  1. 紹介といっても、Readmeに書いてあることをなぞった程度ですが...

  2. 一見本家にもありそうですが、Goにあるのは型変換(xT(x)T 型へ変換)とstructの初期化 T{} だけです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0