フューチャー夏休みの自由研究連載の2回目です。
TIG DXユニットの伊藤真彦です。
夏休みの自由研究として、GoPlusの調査を行いました。
この記事ではGoPlusの紹介、および簡単な内部構造の調査を行います。
目次
- はじめに
- GoPlusとは
- GoPlusの記述例
- GoPlusのインストール
- GoPlusをGoにインポート
- GoPlusのハックを研究
- GoPlusのこれから
はじめに
私は現在Goを書いていますが、前職ではRubyでのWeb開発がメインでした。
Rubyは型定義を書く必要がありません、端的に言うと書きやすさに重きを置いています。
Wikipediaにも
開発者のまつもとゆきひろは、「Rubyの言語仕様策定において最も重視しているのはストレスなくプログラミングを楽しむことである (enjoy programming)」と述べている。
と書いてあることからも、その設計思想が伺えます。
一方Goはコンパイル言語としての高速実行と書きやすさの絶妙なバランスを攻めている印象です。
C#を書いた経験もあるので、型があるのが自然という感覚も理解できますが、Rubyに慣れ親しんだ身からすると型定義や配列の取り扱いが少々面倒に感じます。
そもそも大抵の言語はRubyよりお堅いのではないでしょうか。
(どちらの言語が優れている、という主張は勿論ありません。)
そんなRuby愛を引きずっている私にとって衝撃的なニュースがありました。
GoPlusの存在です。
GoPlusとは
Goのスーパーセットにあたるライブラリです。
- Goと完全な互換性を持ってる
- スクリプト言語のようなスタイルで、データサイエンスにおいてGoより高い可読性を実現している
と、READMEに書いてあります。
Cに対するC++のようなノリで命名されているようですね。
GoPlusの記述例
HelloWorld
拡張子は.gop
です。
この1行だけで動きます、つまりpackageの定義やmain関数の記述が不要です。
素晴らしいですね。
配列、マップの取り扱い
maplit.gopl := [1, 2, 3] m := {"a": 1, "b": 2} println(l) println(m)
# [1 2 3]、map[a:1 b:2]が出力される
|
特にスライス(可変長の配列型)の宣言の感じがRuby時代を思い出します。
同じことをGoでやると、このような感じでしょうか
maplit.gopackage main
import "fmt"
func main() { l := []int{1, 2, 3} m := map[string]int{"a": 1, "b": 2} fmt.Println(l) fmt.Println(m) }
|
余談ですが、サンプルコードのprintln
関数もGoPlus
で新たに定義されています。
本家GoのPrint系の関数より柔軟で、型を意識せずに使えるように拡張されています。
他にも多倍長整数型・多倍長浮動小数点数型のサポート、配列操作の拡張、エラーハンドリングの簡易的な記法の実装など便利な機能が多数用意されています。
GoPlusのインストール
GoPlusを実際に使用する方法を紹介します。
go get
コマンドでGOPATH
配下にライブラリをインポートします
go get github.com/qiniu/goplus go get github.com/qiniu/goplus@v0.6.50 #バージョン指定する場合
|
GOPATH
配下にgoplus
ディレクトリが生成されるので移動します。
go install
コマンドでパッケージをインストールします。
cd $GOPATH/src/github.com/qiniu/goplus go install -v ./...
|
インストールするとgop
コマンドが利用できるようになります。
※執筆時点ではα版であるため、代わりに4種類のコマンドがインストールされます。
- qrun: Similar to gop run
- qfmt: Similar to gop fmt
- qexp: Similar to gop export
- qgo: Similar to gop go
試しにqrun
コマンドを使用します。
qrun
はgop run
、つまりGoのgo run
に相当するファイル実行コマンドです。
拡張子がgop
のファイルを実行できます。
実際にprintln("Hello, world!")
1行だけが書かれたファイルを実行できました。
GoPlusをGoにインポート
GoPlusの拡張機能をGoのライブラリとしてインポートするような使い方も可能です。
main.gopackage main
import ( "fmt"
"github.com/qiniu/goplus/tutorial/14-Using-goplus-in-Go/foo" )
func main() { rmap := foo.ReverseMap(map[string]int{"Hi": 1, "Hello": 2}) fmt.Println(rmap) }
|
GoPlusのハックを研究
仕事で使うかはともかくGoPlusは面白いライブラリだと感じました。
今回はGoPlusがどのような手法でGoを柔軟な言語に拡張しているのか調査します。
ようやく自由研究の始まりです。
qrunコマンドでファイルが実行できる仕組み
go run
に相当するqrun
コマンドを見てみましょう。
コマンドの実装はこちらです。
いたって普通のGoのスクリプトです。
つまり、go install
コマンドでインストールしたGoの実装が各種コマンドの正体です。
qrun
のmain関数内部で、拡張子gop
のファイルを解析、実行することで、gop
ファイル本体にはmain関数や諸々のおまじないが不要になっていたんですね。
GoPlusは既に5年に渡り開発が行われています(initial commitが2015年でした)。
そのため複雑な実装が組みあがっていますが、頑張って要点だけでも解析してみます。
qrun.gofunc main() { flag.Parse() if flag.NArg() < 1 { fmt.Fprintf(os.Stderr, "Usage: qrun [-asm -quiet -debug -prof] <gopSrcDir|gopSrcFile>\n") flag.PrintDefaults() return }
|
まずは標準ライブラリのflag
を用いて、コマンドライン引数を格納しています。
つまりflag.Arg(0)
で引数で渡したファイル名.gop
が取得できます。
試しに何もファイル名を指定せずqrun
コマンドのみを実行するとflag.NArg() < 1
がtrue
になり、実装の通りのエラーメッセージが出力されます。
qrun.gotarget, _ := filepath.Abs(flag.Arg(0)) isDir, err := IsDir(target) if err != nil { log.Fatalln("input arg check failed:", err) } pkgs, err := parser.ParseGopFiles(fset, target, isDir, 0) if err != nil { log.Fatalln("ParseGopFiles failed:", err) }
|
ファイル名にパスを組み合わせたtarget
がparser.ParseGopFiles()に渡されます。
ここでgopファイル
としての解析が始まるようです。
qrun.gocl.CallBuiltinOp = exec.CallBuiltinOp
b := exec.NewBuilder(nil) _, err = cl.NewPackage(b.Interface(), pkgs["main"], fset, cl.PkgActClMain) if err != nil { log.Fatalln("cl.NewPackage failed:", err) } code := b.Resolve() if *flagAsm { code.Dump(os.Stdout) return } ctx := exec.NewContext(code) ctx.Exec(0, code.Len()) if *flagProf { exec.ProfileReport() }
|
解析した結果の実行はNewBuilder
、NewPackage
などの関数内部で行われています。
全てを解析しようとすると終わりが見えません、5年分の重みを感じます。
構文解析の仕組み
GoPlusスクリプトの構文解析はcl.NewPackage()で行われるようです。
compile.gofunc NewPackage(out exec.Builder, pkg *ast.Package, fset *token.FileSet, act PkgAct) (p *Package, err error) { if pkg == nil { return nil, ErrNotFound } if CallBuiltinOp == nil { log.Panicln("NewPackage failed: variable CallBuiltinOp is uninitialized") } p = &Package{} ctxPkg := newPkgCtx(out, pkg, fset) ctx := newGblBlockCtx(ctxPkg) for _, f := range pkg.Files { loadFile(ctx, f) } switch act { case PkgActClAll: for _, sym := range ctx.syms { if f, ok := sym.(*funcDecl); ok && f.fi != nil { ctxPkg.use(f) } } if pkg.Name != "main" { break } fallthrough case PkgActClMain: if pkg.Name != "main" { return nil, ErrNotAMainPackage } entry, err := ctx.findFunc("main") if err != nil { if err == ErrNotFound { err = ErrMainFuncNotFound } return p, err } if entry.ctx.noExecCtx { ctx.file = entry.ctx.file compileBlockStmtWithout(ctx, entry.body) ctx.checkLabels() } else { out.CallFunc(entry.Get(), 0) ctxPkg.use(entry) } out.Return(-1) } ctxPkg.resolveFuncs() p.syms = ctx.syms return }
|
上記のように複雑な分岐を経由しますが、各経路での前処理を終えたらcompileStmt()で実際に構文解析が行われます。
その先頭のcompileExprStmt()内部のcompileExpr()までトークンを解析して送り込みます。
expr.gofunc compileExpr(ctx *blockCtx, expr ast.Expr) func() { switch v := expr.(type) { case *ast.Ident: return compileIdent(ctx, v.Name) case *ast.BasicLit: return compileBasicLit(ctx, v) case *ast.CallExpr: return compileCallExpr(ctx, v) case *ast.BinaryExpr: return compileBinaryExpr(ctx, v) case *ast.UnaryExpr: return compileUnaryExpr(ctx, v) case *ast.SelectorExpr: return compileSelectorExpr(ctx, v) case *ast.ErrWrapExpr: return compileErrWrapExpr(ctx, v) case *ast.IndexExpr: return compileIndexExpr(ctx, v) case *ast.SliceExpr: return compileSliceExpr(ctx, v) case *ast.CompositeLit: return compileCompositeLit(ctx, v) case *ast.SliceLit: return compileSliceLit(ctx, v) case *ast.FuncLit: return compileFuncLit(ctx, v) case *ast.ParenExpr: return compileExpr(ctx, v.X) case *ast.ListComprehensionExpr: return compileListComprehensionExpr(ctx, v) case *ast.MapComprehensionExpr: return compileMapComprehensionExpr(ctx, v) case *ast.ArrayType: return compileArrayType(ctx, v) case *ast.Ellipsis: return compileEllipsis(ctx, v) case *ast.KeyValueExpr: panic("compileExpr: ast.KeyValueExpr unexpected") default: log.Panicln("compileExpr failed: unknown -", reflect.TypeOf(v)) return nil } }
|
こちらのswitch文まで辿り着いたら、解析結果に応じたコンパイルが走ります。
興味があればqrunから構文解析までの処理の流れを詳しく探索してみてください、私はそろそろ限界です。
試しに解析結果が*ast.SliceExpr
だった場合の動き、compileSliceExpr()を見てみます。
expr.gofunc compileSliceExpr(ctx *blockCtx, v *ast.SliceExpr) func() { var kind reflect.Kind exprX := compileExpr(ctx, v.X) x := ctx.infer.Get(-1) typ := x.(iValue).Type() if kind = typ.Kind(); kind == reflect.Ptr { typ = typ.Elem() if kind = typ.Kind(); kind != reflect.Array { logPanic(ctx, v, `cannot slice a (type *%v)`, typ) } typ = reflect.SliceOf(typ.Elem()) ctx.infer.Ret(1, &goValue{typ}) } return func() { exprX() i, j, k := exec.SliceDefaultIndex, exec.SliceDefaultIndex, exec.SliceDefaultIndex if v.Low != nil { i = compileIdx(ctx, v.Low, exec.SliceConstIndexLast, kind) } if v.High != nil { j = compileIdx(ctx, v.High, exec.SliceConstIndexLast, kind) } if v.Max != nil { k = compileIdx(ctx, v.Max, exec.SliceConstIndexLast, kind) } if v.Slice3 { ctx.out.Slice3(i, j, k) } else { ctx.out.Slice(i, j) } } }
|
独自の実装が山盛りですね…再帰的にcompileExpr()が呼び出されるところも迷宮ポイントを高めています。
キリがないので今回はこの辺りで調査を終了したいと思います。
GoPlusのこれから
GoPlusは執筆時点では月間90件に近いプルリクエストがマージされ、絶賛開発中の状態です。
α版を乗り越えgop
コマンドが動き出す日は来るのか、乞うご期待です。
追記
記事の公開前日に確認したところ、バージョン0.7.1よりgopコマンドが採用されていました!
おめでとうGoPlus。
私は偉いので記事公開の1か月前に概ね書き終わっていたのですが、その間にもGoPlusは大きく進化していました。
追記執筆時点での最新の実装(v0.7.4)ではREPL(対話型インタプリタ)の実行機能が追加されたようです。
さらにはvscode用プラグインの開発もスタートしました。
今後の成長がますます楽しみになりました。