コードのジェネレート #
Generating code by Rob Pike
普遍的な計算の性質、チューリング完全、とは、コンピュータプログラムがコンピュータプログラムを書けるということです。
これは、実際に評価されるべきほどには評価されていない、強力な考え方です。たとえば、これはコンパイラの定義の
大きな部分を占めています。また go test
コマンドの動作にも関わってきます。 go test
はテスト対象の
パッケージをスキャンして、そのパッケージ用のテストハーネスを含んだGoプログラムを書き出して、
それをコンパイルして実行します。現代のコンパイラは非常に速いので、このコストが高そうな一連の処理も
1秒以内に完了できます。
プログラムがプログラムを書く例は他にもたくさんあります。たとえば Yacc は、 文法の記述を読み込んで、文法をパースするプログラムを書き出します。Protocol Bufferの「コンパイラ」は インターフェースの記述を読み込んで、構造の定義とメソッドとその他必要なコードを生成します。 あらゆる種類の設定ツールも同様の動作をします。メタデータや環境を確認して、ローカルの状態に合わせた 足場を生成してくれます。
それゆえプログラムを書くプログラムはソフトウェアエンジニアリングにおいて重要な要素ですが、 ソースコードを生成するYaccのようなプログラムは、その出力がコンパイルされるように ビルドプロセスに組み込まれる必要があります。Makeのような外部のビルドツールが使われる場合、 これは非常にたやすいことです。しかしGoでは、go toolがソースコードから必要なビルド情報を すべて取得するので、問題が有ります。go toolだけでYaccを走らせる簡単な機構がないのです。
いままでは、ありませんでした。
最新のGoのリリースである、1.4ではそのようなツールを実行しやすくする
新しいコマンドが追加されました。 go generate
というコマンド名で、Goのソースコード内の特別なコメントを
スキャンすることで動作します。 go generate
は go build
の一部ではないことを理解するのが大切です。
go generate
は依存性の解析はまったく行わず、また明示的に go build
の前に実行されなければなりません。
go generate
はGoパッケージの利用者ではなく、その作者が利用することを意図したものです。
go generate
コマンドは簡単に使えます。準備運動として、Yaccの文法を生成する方法を紹介しましょう。
たとえば、ここに gopher.y
というYaccの入力用ファイルがあったとします。このファイルでは
あなたの新しい言語の文法が定義してあるとします。この文法を実装するGoのソースファイルを生成するには
通常であれば標準でついてくるGo版のYaccを次のように起動するでしょう。
go tool yacc -o gopher.go -p parser gopher.y
-o
オプションは出力ファイル名を指定し、 -p
オプションはパッケージ名を指定します。
go generate
にこの処理を行わせるには、同じディレクトリ内の通常の(自動生成でない) .go
ファイルのいずれかに、
ファイル内のどこかに次のようなコメントを追加します。
//go:generate go tool yacc -o gopher.go -p parser gopher.y
このテキストは先のコマンドに、 go generate
が認識するための特別なコメントを先頭に付けただけのものです。
コメントは行頭から開始しなくてはならず、また //
と go:generate
の間にスペースを入れてはいけません。
そのマーカー以降は go generate
が実行するコマンドを指定します。
では実行してみましょう。ソースのディレクトリに移動し、 go generate
を実行して、 go build
等を実行します。
$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test
これだけです。エラーが無かった場合、 go generate
は yacc
を起動して gopher.go
を生成します。
そして生成されると、そのディレクトリには必要なGoのソースコードが揃うので、ビルドしてテストして
いつもどおりの処理を行うことができます。 gopher.y
が変更されるたびに、 go generate
を実行して
パーサーを再度生成しましょう。
go generate
の動作やオプション、環境変数の内容などを知りたい場合は、
デザインドキュメントを参照してください。
go generateはMakeや他のビルド機構で出来なかったことはなにもできませんが、 go
ツールに付随するもの、
つまり余計なインストールが要らず、Goのエコシステムに上手く適用出来ます。 go generate
は、
それが生成するコードが対象のマシンで得られない場合にのみ、パッケージ作者が使うためのものであり、
パッケージの利用者のためのものではないことを心に留めておいてください。また、もし生成されたコードが
go get
でインポートされることを意図しているのであれば、生成された(そしてテストされた!)ファイルは
あとにソースコードレポジトリに追加されるべきです。
go generate
について少しわかったところで、なにか新しいことをしてみましょう。
go generate
が役に立つまったく別の例としては、 golang.org/x/tools/
内の stringer
と呼ばれる、
新しいプログラムがあります。これは整数の定数に対して自動的に String
メソッドを書いてくれます。
このツールはリリースでは配布されていませんが、簡単にインストールすることが出来ます。
$ go get golang.org/x/tools/cmd/stringer
stringer
のドキュメントからの例を持ってきました。
あるコードに異なる錠剤の種類を定義するための整数の定数があったとします。
package painkiller
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
デバッグ用途にこれらの定数を綺麗に表示したい、つまりシグネチャ付きのメソッドが欲しくなります。
func (p Pill) String() string
手でそのコードを書くのは簡単です。おそらく次のようになるでしょう。
func (p Pill) String() string {
switch p {
case Placebo:
return "Placebo"
case Aspirin:
return "Aspirin"
case Ibuprofen:
return "Ibuprofen"
case Paracetamol: // == Acetaminophen
return "Paracetamol"
}
return fmt.Sprintf("Pill(%d)", p)
}
もちろん他にもこの関数を書く方法があります。stringもスライスをPillを使ってインデックスを つけることもできますし、mapを使うこともできますし、他の技もあります。どのような方法でも、 錠剤のセットが変わったらそれに合わせてコードを変更する必要がありますし、それが必ず正しいように する必要があります。(パラセタノールは2つの名前があるので他の錠剤より用心しなければなりません。) 加えて、型や値、たとえば符号有りか符号無しか、密か疎か、ゼロ基準か否かなど、に応じてどの手法を 採用するかを考えなければなりません。
stringer
のプログラムはこれらの疑問に対してすべて面倒を見てくれます。
stringer
は単体で動くプログラムではありますが、 go generate
から呼び出すことを
意図されています。使うためにはソース、おそらく型定義の近くに、生成用のコメントを加えてください。
//go:generate stringer -type=Pill
このルールでは go generate
は stringer
ツールを実行して Pill
型に String
メソッドを
生成するように指定しています。出力結果は自動的に pill_string.go
に書き込まれます。
( -output
フラグを使って出力先を変更することも出来ます。)
実行してみましょう。
$ go generate
$ cat pill_string.go
// generated by stringer -type Pill pill.go; DO NOT EDIT
package pill
import "fmt"
const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"
var _Pill_index = [...]uint8{0, 7, 14, 23, 34}
func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}
$
Pill
の定義を変更するたびに、 String
メソッドを更新するために、
次のコマンドを実行する必要があります。
$ go generate
もちろん、同じパッケージには同様の型が複数あるので、それらの String
メソッドすべてを
1回のコマンドで更新できるのです。
生成されるコードが醜いのは疑問の余地がありません。でもいいんです。人間がそのコードを
編集する必要はないですから。機械が生成したコードはしばしば醜いものです。醜いコードも
最適化の結果なのです。すべての名前が1つの文字列に押し込められています。こうすることで
メモリを節約しています。(すべての名前に対して、1つの文字列のヘッダだけですみます。
たとえ名前が何億兆あったとしてもです。)そして、配列の _Pill_index
を作り、単純で
効率的な手法で値から名前への対応を作ります。 _Pill_index
は uint8
の配列であり、
スライスではないことに気をつけてください。 uint8
の配列なのは、値の空間をつなぐのに
必要十分な最小の整数だからです。もっと多くの値がある、あるいは負の値がある場合には、
生成される _Pill_index
の型は uint16
や int8
になるでしょう。ようするに、対応ができる
ベストな型です。
stringer
が生成するメソッドで使われている手法は定数のセットの特性によって変化します。
たとえば、定数が疎の場合は、マップが使われます。2の累乗を表す定数のセットを使った例です。
const _Power_name = "p0p1p2p3p4p5..."
var _Power_map = map[Power]string{
1: _Power_name[0:2],
2: _Power_name[2:4],
4: _Power_name[4:6],
8: _Power_name[6:8],
16: _Power_name[8:10],
32: _Power_name[10:12],
...,
}
func (i Power) String() string {
if str, ok := _Power_map[i]; ok {
return str
}
return fmt.Sprintf("Power(%d)", i)
}
手短に言えば、自動でメソッドを生成させたほうが、人間が書くよりもより効率的なものが生成できるということです。
Goのコードベースにはすでに多くの go generate
の事例があります。 unicode
パッケージでのUnicode表の生成や、
encoding/gob
で配列を効率的にエンコードあるいはデコードするメソッドの作成、time
パッケージでのタイムゾーンデータの
生成などがあります。
ぜひ go generate
を創造的に使ってください。いろいろ試せるように公開しているのです。
go generate
を自分で使うのではない場合にも、ぜひ String
メソッドを書く際には新しい stringer
ツールを使ってください。
機械に仕事をさせましょう。
あわせて読みたい #
- Introducing the Go Race Detector
- Go maps in action
- go fmt your code
- Organizing Go code
- Debugging Go programs with the GNU Debugger
- The Go image/draw package
- The Go image package
- The Laws of Reflection
- Error handling and Go
- “First Class Functions in Go”
- Profiling Go Programs
- A GIF decoder: an exercise in Go interfaces
- Introducing Gofix
- Godoc: documenting Go code
- Gobs of data
- C? Go? Cgo!
- JSON and Go
- Go Slices: usage and internals
- Go Concurrency Patterns: Timing out, moving on
- Defer, Panic, and Recover
- Share Memory By Communicating
- JSON-RPC: a tale of interfaces