もっと楽して式の評価器を作る #golang
January 8, 2017
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
Go Advent Calendar 2016にて、「簡単な式の評価機を作ってみる」という記事を書きました。 そこでは、抽象構文木(AST)を解析し、go/constantパッケージの機能を使って式の評価器を作るという話を書きました。
この記事では、go/typesパッケージの機能を使うことで、もっと楽して式の評価を行うプログラムを作ってみたいと思います。
なお、この記事を書いた時点におけるGoの最新バージョンは1.7.4です。
定数の評価
「Goのスコープについて考えてみよう」という記事でも触れましたが、go/types
パッケージには、以下のような機能が提供されています。
- 識別子の解決(identifier resolution)
- 型推論(type deduction)
- 定数の評価(constant evaluation)
「go/types: The Go Type Checker」にも書いてありますが、これらは、go/types
パッケージが型チェックを行う上で、切ってもきれない機能です。
このうち、定数の評価については、types.Eval
関数で単体で行うことができます。
types.Eval
関数は以下のようなシグニチャを持ちます。
func Eval(fset *token.FileSet, pkg *Package, pos token.Pos, expr string) (TypeAndValue, error)
第1引数は、お馴染みのtoken.FileSet
構造体のポインタです。
token.NewFileSet
関数によって取得することができ、go/parser
パッケージのParse
系の関数によってファイル情報を追加していきます。
なお、パースについては、「ASTを取得する方法を調べる」という記事で解説しています。
第2引数は、types.Pacakge
構造体のポインタです。
*types.Config
型のCheckメソッドを使うと、指定したファイルのASTを解析し、それらを1つのパッケージとし、スコープの情報やインポートしたパッケージの情報などを設定します。
第3引数は、ソースコード上の位置を表すtoken.Pos
型の値です。
同じスコープ内でも位置によって、定義されている識別子が違う可能性があるため、位置を指定する必要があります。
第4引数はいよいよ、式を表す文字列です。 これは、定数自体がコンパイル時に評価されるため、コンパイル時に解決できるものでしか構成できません。 つまり、他の定数やリテラル、組み込み関数などしか使えません。
戻り値ですが、第1戻り値は、types.TypeAndValue
型という、その名の通り型と値の情報を持つ構造体で、第2戻り値はエラーです。
さて、types.Eval
を使って、式の評価を行っていきます。
引数のfset
については、特にパースする必要もないため、token.NewFileSet
関数で新しく作ったものをそのまま渡せば良さそうです。
pkg
についても、今回はユニバーススコープだけで十分なのでtypes.NewPacakge
で返ってくる値をそのまま渡せば良さそうです。
また、第3引数のpos
についても、1つの式を評価したいだけなので、ゼロ値であるtoken.NoPos
でも渡しておけば良いでしょう。
そして、第4引数はお好きな式を渡せば良いでしょう。
つまり、以下のようかコードで式の評価が行えるということです。
package main
import (
"fmt"
"go/token"
"go/types"
"log"
)
func eval(expr string) (types.TypeAndValue, error) {
return types.Eval(token.NewFileSet(), types.NewPackage("main", "main"), token.NoPos, expr)
}
func main() {
tv, err := eval(`(100 + 1) * len("hoge")`)
if err != nil {
log.Fatal(err)
}
fmt.Println(tv.Value)
}
あれ。。eval
関数が1行になりました。
というか、types.Eval
関数を呼び出しているだけになりましたね。
ちゃんと組み込み関数のlen
も使えていることが分かると思います。
上記のコードは、The Go Playgroundでも動かせるので、ぜひ動かしてみて下さい。
他のパッケージの定数を参照する
これだけでは面白くないので、math
パッケージの定数を参照できるように改造してみましょう。
ここではtypes.Eval
関数に渡す、*types.Pacakge
型の値に対して、いくつか操作をすることでmath
パッケージをインポートしたことにしましょう。
まず、なにもインポートしておらず、ユニバーススコープの識別子しかアクセスできないパッケージを作ります。
pkg := types.NewPackage("main", "main")
次に、math
パッケージをtypes.Importer
型のImport
メソッドを使ってインポートします。
ここでは、go/importer
パッケージのimport.Default
関数が返すデフォルトのtypes.Importer
を使います。
mathPkg, err := importer.Default().Import("math")
つづいて、インポートしたmath
パッケージをpkg
のインポートリストに追加します。
ここでは、他にインポートしてないので、新しくリスト作ってを設定しています。
pkg.SetImports([]*types.Package{
mathPkg,
})
最後にpkg
のスコープに対して、math
パッケージとmath
という識別子を関連付けるために、type.PkgName
構造体のポインタ型の値を差し込みます。
pkg.Scope().Insert(types.NewPkgName(token.NoPos, pkg, "math", mathPkg))
なお、自前の定数を定義したい場合は、以下のように*types.Const
型の値をスコープに差し込みます。
pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, "hoge", types.Typ[types.Float64], constant.MakeFloat64(100)))
ここまでの処理をまとめると、以下のようなコードになります。
なお、このコードで使っているimporter.Default
関数の返すインポーターがThe Go Playground上では動作しないので、注意してください。
package main
import (
"fmt"
"go/constant"
"go/importer"
"go/token"
"go/types"
"log"
)
func eval(expr string) (types.TypeAndValue, error) {
pkg := types.NewPackage("main", "main")
mathPkg, err := importer.Default().Import("math")
if err != nil {
return types.TypeAndValue{}, err
}
pkg.SetImports([]*types.Package{
mathPkg,
})
pkg.Scope().Insert(types.NewPkgName(token.NoPos, pkg, "math", mathPkg))
pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, "hoge", types.Typ[types.Float64], constant.MakeFloat64(100)))
return types.Eval(token.NewFileSet(), pkg, token.NoPos, expr)
}
func main() {
tv, err := eval(`(hoge + 1) * math.Pi`)
if err != nil {
log.Fatal(err)
}
fmt.Println(tv.Value)
}
実行結果は以下のようになります。
317.301
うまく外部の定数や自前の定数も評価できることが分かります。
REPLを作る
今回も、REPLも作ってみましょう。 せっかくなので、前の評価結果を次の式で使えるようにしてみます。
*type.Scope
型のInsert
メソッドを使って、評価結果をスコープに定数として追加してやり、次の式の評価で同じパッケージ情報を使うことで実現することができます。
コードは以下のようになります。
package main
import (
"bufio"
"errors"
"fmt"
"go/token"
"go/types"
"os"
)
type Evaluator struct {
pkg *types.Package
fset *token.FileSet
outputCount int
}
func NewEvaluator() *Evaluator {
return &Evaluator{
pkg: types.NewPackage("main", "main"),
fset: token.NewFileSet(),
}
}
func (e *Evaluator) Count() int {
return e.outputCount
}
func (e *Evaluator) Eval(expr string) (string, error) {
pos := e.pkg.Scope().End()
tv, err := types.Eval(e.fset, e.pkg, pos, expr)
if err != nil {
return "", err
}
if tv.Type == nil || tv.Value == nil {
return "", errors.New("cannot eval expr")
}
e.outputCount++
outputName := fmt.Sprintf("o%d", e.outputCount)
e.pkg.Scope().Insert(types.NewConst(pos, e.pkg, outputName, tv.Type, tv.Value))
return tv.Value.ExactString(), nil
}
func main() {
e := NewEvaluator()
s := bufio.NewScanner(os.Stdin)
for {
fmt.Print(">")
if !s.Scan() {
break
}
expr := s.Text()
if expr == "bye" {
break
}
o, err := e.Eval(expr)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("[o%d]=%s", e.Count(), o)
fmt.Println()
}
}
if err := s.Err(); err != nil {
fmt.Println("Error:", err)
}
}
パッケージ情報などを保持するために、構造体を定義し、Eval
メソッドとして式の評価を定義してやります。
評価結果は、o1
やo2
のような定数として、*types.Pacakge
型のScope
メソッドから取得できるスコープの末尾に挿入されていきます。
実行すると以下のようになります。
constant.Value
型のExactString
メソッドから返ってくる文字列は定数を文字列で表したものですが、実数などは近似値で表されず分数で表されます。
>1 + 1
[o1]=2
>o1 / 3
[o2]=0
>o1 / 3.0
[o3]=2/3
>o3 * (3 + 2i)
[o4]=(2 + 4/3i)
>bye
おわりに
この記事では、types.Eval
関数を使うことで簡単に式の評価器を作る方法について解説しました。
以前の「簡単な式の評価機を作ってみる」を参考にして、式の評価器を組み込んだ方は申し訳ありません。
しかし、変数や関数の機能を入れる際はASTを自前で解析する方法を取らざる得ないのでご容赦下さい!