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

# はじめに

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

https://github.com/cosmos72/gomacro

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

以下は記事執筆時点での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` のメソッドが使用可能です。

https://github.com/cosmos72/gomacro#extensions

```
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()` で生成可能です[^constructor]。

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

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

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

# ジェネリクス（試験導入）

gomacroはGo本家よりも前からジェネリクスが使用可能でした。そのため、2019年のProposal ([Contract](https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md)) に基づいた独自構文となっています。

https://github.com/cosmos72/gomacro#generics

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

```
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の指定はできません。

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

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

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

https://zenn.dev/nobishii/articles/99a2b55e2d3e50#type-list%E3%81%AE%E5%BF%85%E8%A6%81%E6%80%A7

とはいえ、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
}
```

# マクロ

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

https://github.com/cosmos72/gomacro/blob/master/doc/code_generation.pdf

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フィールドに過ぎないので差し替えできないようです。

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

https://pkg.go.dev/go/ast#BinaryExpr

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

https://github.com/cosmos72/gomacro/issues/24#issuecomment-1206641801

[^intro]: 紹介といっても、Readmeに書いてあることをなぞった程度ですが...
[^constructor]: 一見本家にもありそうですが、Goにあるのは型変換（`x` を `T(x)` で `T` 型へ変換）とstructの初期化 `T{}` だけです。