はじめに
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を見ているようで興味深かったです。
ジェネリクスについては本家の仕様に合わせる構想があるようなので、独自構文で遊ぶなら今のうちです。