コード生成用のツールに欲しいもの1 出力先の指定
はじめに
(個人的なコード生成のための環境を作っている。まだ完成には程遠いので実際のチュートリアルなどは作らない)
コード生成用のツールに欲しいものを思いつくままにあげていく。
出力先の指定
コード生成で欲しいのは出力先を指定できることかもしれない。これは特にデバッグ出力ができると嬉しい。
完全に動作することがわかって状態でのコード生成では全く要らないものではあるのだけれど。コード生成の出力結果が不安定であった場合にデバッグがしたい。
不安定というのは以下の2種類がある。
- 入力となる設定に不備がある
- コード生成の実装に不備がある
設定に不備がある場合には少しずつの入力の結果を変えて試したいかもしれない。コード生成の実装に不備がある場合には実装部分まで潜ってテストを書いたりするかもしれない。ただ、まずは最初の第一歩として不備の結果を再現する最小のサブセットを見つけるところから始めることが多いと思う。
最小のサブセットを見つけるときや初めの一歩のタイミングでは、依存もなるべく最小限にしたい。なるべくなら1ファイルで完結させたい(依存ライブラリのimportは仕方がないかもしれない。とは言えなるべく依存も減らしたいところではある)。
例えば、設定ファイルを受け取って、直接何らかのパスに書き込む形のコマンドはデバッグがしにくい。以下のようなもの。
$ go run main.go --pkg github.com/podhmo/x/y --name S # $GOPATH/src/github.com/podhmo/x/y/s_gen.go などに出力
これは入力対象のファイル(設定ファイル)と出力対象のファイルと変換用のツールの3つを確認する必要が出てくる。
個人的には、特にGOPATH以下を前提としなければいけないコードになるのが結構苦痛だったりする(挙動を確かめるために /tmp
以下のファイルであったり、個人的なsandbox的なディレクトリに雑にコードを書いて試すということが多いので)。
1ファイルで完結するというのは以下のようなもの。
go run main.go # 出力結果が標準出力に出力される
出力先の指定はディレクトリ単位であってほしい
goのパッケージの出力のことを考えると出力先が1つであることは少ない。例えばStringerみたいなものを考えたときに、1つのパッケージ内の複数の型を指定したいことがしばしばある。ほとんどコストが0であれば都度呼び出すということでも良いかもしれないけれど。ちょっと凝った処理だったりを書いてしまった場合には困るので(go/astやgo/typesを使わないコード生成の場合にはそんなに読み込みに時間は掛からないのだけれど。それならたぶんコード生成ツール自体をgoで書く必要はあんまりない)。
ということはコード生成結果を標準出力に吐いて終了と言うようなことはできない。デフォルトではあくまで出力先は複数であって欲しい。
一方でデバッグ時には全てコンソール上に出力されて欲しい。
そんなわけで以下に対して1ファイルで完結するような動作が求められる感じ。
- 複数の入力ファイル
- 複数の出力ファイル
現在の状況
現在のところでも一応不格好だけれど。複数の入力と複数の出力をシミュレートした状態を1ファイルで試すことができるようにしている。
入力としてmessage.goを利用し、出力としてhello.goとbye.goを生成するというようなコマンドを作ってみる。
もう少し丁寧に書くと以下の様な感じ。
入力ファイル
- testcode/message.go
出力ファイル
- (testcode/message.go)
- testcode/hello.go
- testcode/bye.go
実行結果は以下のような感じ。
$ go run main.go 2018/04/04 00:43:39 package ./testcode is not found, creating. open message.go ---------------------------------------- package testcode type Message string const ( MessageHello = Message("HELLO") MessageBye = Message("BYE") ) open bye.go ---------------------------------------- package testcode func Bye() string { return string(MessageBye) } open hello.go ---------------------------------------- package testcode func Hello() string { return string(MessageHello) }
これは以下の様なコードで作れる(まだAPIなどは安定していないので変わることはある)
main.go
package main import ( "log" "github.com/podhmo/handwriting" "golang.org/x/tools/go/loader" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { // 入力となるようなsource codeをコード内に含められる source := ` package testcode type Message string const ( MessageHello = Message("HELLO") MessageBye = Message("BYE") ) ` c := &loader.Config{} f, err := c.ParseFile("message.go", source) if err != nil { return err } // 入力ファイルを複数にすることも可能だけれど。今はひとつだけ c.CreateFromFiles("./testcode", f) p, err := handwriting.New( "./testcode", handwriting.WithConfig(c), // x/toolsのloader経由で*ast.Fileで作られたpackageを受け取れる handwriting.WithConsoleOutput(), // これをつけると出力は標準出力になる(debug print) ) if err != nil { return err } // hello.goとbye.goの複数の出力が可能 p.File("hello.go").Code(func(f *handwriting.File) error { f.Out.WithBlock("func Hello() string", func() { f.Out.Println(`return string(MessageHello)`) }) return nil }) p.File("bye.go").Code(func(f *handwriting.File) error { f.Out.WithBlock("func Bye() string", func() { f.Out.Println(`return string(MessageBye)`) }) return nil }) return p.Emit() }
handwritingでfizzbuzz
(handwritingは自作のパッケージ。まだwip。todo どこかで説明)
昔にpythonで書いた貧者のfizzbuzzを作ってみる。
貧者のfizzbuzzとは(悪い意味で)まじめな人が書いたfizzbuzzのこと。(全ての条件分岐と出力処理を手書きする)
code
package main import ( "fmt" "log" "github.com/podhmo/handwriting" ) func main() { p := handwriting.Must(handwriting.New("./main")) f := p.File("main.go") f.Import("fmt") f.Code(func(f *handwriting.File) error { o := f.Out o.WithBlock("func main()", func() { o.WithBlock("for i := 1; i<=100; i++", func() { o.WithBlock("switch i", func() { for i := 0; i <= 100; i++ { o.WithIndent(fmt.Sprintf("case %d:", i), func() { if i%3 == 0 && i%5 == 0 { o.Println(`fmt.Println("fizzbuzz")`) } else if i%3 == 0 { o.Println(`fmt.Println("fizz")`) } else if i%5 == 0 { o.Println(`fmt.Println("buzz")`) } else { o.Println(`fmt.Printf("%d\n", i)`) } }) } o.WithIndent("default:", func() { o.Println(`panic("not supported")`) }) }) }) }) return nil }) if err := p.Emit(); err != nil { log.Fatal(err) } }
実行結果
$ go run main.go 2018/04/03 23:55:57 package ./main is not found, creating. 2018/04/03 23:55:57 open "main/main.go" $ go run main/main.go 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 .. buzz 86 fizz 88 89 fizzbuzz 91 92 fizz 94 buzz
出力されたコード
package main import ( "fmt" ) func main() { for i := 1; i<=100; i++ { switch i { case 0: fmt.Println("fizzbuzz") case 1: fmt.Printf("%d\n", i) case 2: fmt.Printf("%d\n", i) case 3: fmt.Println("fizz") case 4: fmt.Printf("%d\n", i) case 5: fmt.Println("buzz") case 6: fmt.Println("fizz") case 7: fmt.Printf("%d\n", i) case 8: fmt.Printf("%d\n", i) case 9: fmt.Println("fizz") case 10: fmt.Println("buzz") case 11: fmt.Printf("%d\n", i) case 12: fmt.Println("fizz") case 13: fmt.Printf("%d\n", i) case 14: fmt.Printf("%d\n", i) case 15: fmt.Println("fizzbuzz") case 16: fmt.Printf("%d\n", i) case 17: fmt.Printf("%d\n", i) case 18: fmt.Println("fizz") case 19: fmt.Printf("%d\n", i) case 20: fmt.Println("buzz") case 21: fmt.Println("fizz") case 22: fmt.Printf("%d\n", i) case 23: fmt.Printf("%d\n", i) case 24: fmt.Println("fizz") case 25: fmt.Println("buzz") case 26: fmt.Printf("%d\n", i) case 27: fmt.Println("fizz") case 28: fmt.Printf("%d\n", i) case 29: fmt.Printf("%d\n", i) case 30: fmt.Println("fizzbuzz") case 31: fmt.Printf("%d\n", i) case 32: fmt.Printf("%d\n", i) case 33: fmt.Println("fizz") case 34: fmt.Printf("%d\n", i) case 35: fmt.Println("buzz") case 36: fmt.Println("fizz") case 37: fmt.Printf("%d\n", i) case 38: fmt.Printf("%d\n", i) case 39: fmt.Println("fizz") case 40: fmt.Println("buzz") case 41: fmt.Printf("%d\n", i) case 42: fmt.Println("fizz") case 43: fmt.Printf("%d\n", i) case 44: fmt.Printf("%d\n", i) case 45: fmt.Println("fizzbuzz") case 46: fmt.Printf("%d\n", i) case 47: fmt.Printf("%d\n", i) case 48: fmt.Println("fizz") case 49: fmt.Printf("%d\n", i) case 50: fmt.Println("buzz") case 51: fmt.Println("fizz") case 52: fmt.Printf("%d\n", i) case 53: fmt.Printf("%d\n", i) case 54: fmt.Println("fizz") case 55: fmt.Println("buzz") case 56: fmt.Printf("%d\n", i) case 57: fmt.Println("fizz") case 58: fmt.Printf("%d\n", i) case 59: fmt.Printf("%d\n", i) case 60: fmt.Println("fizzbuzz") case 61: fmt.Printf("%d\n", i) case 62: fmt.Printf("%d\n", i) case 63: fmt.Println("fizz") case 64: fmt.Printf("%d\n", i) case 65: fmt.Println("buzz") case 66: fmt.Println("fizz") case 67: fmt.Printf("%d\n", i) case 68: fmt.Printf("%d\n", i) case 69: fmt.Println("fizz") case 70: fmt.Println("buzz") case 71: fmt.Printf("%d\n", i) case 72: fmt.Println("fizz") case 73: fmt.Printf("%d\n", i) case 74: fmt.Printf("%d\n", i) case 75: fmt.Println("fizzbuzz") case 76: fmt.Printf("%d\n", i) case 77: fmt.Printf("%d\n", i) case 78: fmt.Println("fizz") case 79: fmt.Printf("%d\n", i) case 80: fmt.Println("buzz") case 81: fmt.Println("fizz") case 82: fmt.Printf("%d\n", i) case 83: fmt.Printf("%d\n", i) case 84: fmt.Println("fizz") case 85: fmt.Println("buzz") case 86: fmt.Printf("%d\n", i) case 87: fmt.Println("fizz") case 88: fmt.Printf("%d\n", i) case 89: fmt.Printf("%d\n", i) case 90: fmt.Println("fizzbuzz") case 91: fmt.Printf("%d\n", i) case 92: fmt.Printf("%d\n", i) case 93: fmt.Println("fizz") case 94: fmt.Printf("%d\n", i) case 95: fmt.Println("buzz") case 96: fmt.Println("fizz") case 97: fmt.Printf("%d\n", i) case 98: fmt.Printf("%d\n", i) case 99: fmt.Println("fizz") case 100: fmt.Println("buzz") default: panic("not supported") } } }