The Go Blog
コード生成
汎用計算の特性(チューリング完全性)として、コンピュータプログラムがコンピュータプログラムを記述できるというものがあります。これは強力なアイデアであり、頻繁に起こることですが、その価値が十分に認識されていないことがよくあります。例えば、コンパイラの定義の大部分を占めています。また、go test コマンドの動作もそうです。テスト対象のパッケージをスキャンし、そのパッケージ用にカスタマイズされたテストハーネスを含むGoプログラムを書き出し、それをコンパイルして実行します。現代のコンピュータは非常に高速なので、この高価に聞こえるシーケンスも、ほんのわずかな時間で完了します。
プログラムを記述するプログラムの例は他にもたくさんあります。Yaccは、文法の記述を読み込んで、その文法を解析するプログラムを書き出します。プロトコルバッファの「コンパイラ」は、インターフェース記述を読み込んで、構造体定義、メソッド、その他のサポートコードを出力します。あらゆる種類の構成ツールも同様に動作し、メタデータや環境を調べて、ローカルの状態に合わせてカスタマイズされた足場コードを出力します。
したがって、プログラムを記述するプログラムはソフトウェアエンジニアリングにおいて重要な要素ですが、Yaccのようにソースコードを生成するプログラムは、その出力をコンパイルできるようにビルドプロセスに統合する必要があります。Makeのような外部ビルドツールを使用している場合、これは通常簡単に行えます。しかし、Goでは、goツールがGoソースから必要なビルド情報をすべて取得するため、問題があります。goツール単独でYaccを実行するメカニズムが単純にないのです。
つまり、今までそうでした。
最新のGoリリースである1.4には、このようなツールの実行を容易にする新しいコマンドが含まれています。これはgo generateと呼ばれ、実行する一般的なコマンドを識別する特別なコメントをGoソースコード内でスキャンすることによって機能します。go generateがgo buildの一部ではないことを理解することが重要です。依存関係解析を含まず、go buildを実行する前に明示的に実行する必要があります。これは、Goパッケージの作成者が使用することを意図しており、クライアントが使用することを意図していません。
go generateコマンドは使い方が簡単です。ウォーミングアップとして、Yacc文法を生成する方法を以下に示します。
まず、GoのYaccツールをインストールします
go get golang.org/x/tools/cmd/goyacc
新しい言語の文法を定義するgopher.yというYacc入力ファイルがあるとします。文法を実装するGoソースファイルを生成するには、通常、次のようにコマンドを呼び出します
goyacc -o gopher.go -p parser gopher.y
-oオプションは出力ファイル名を指定し、-pはパッケージ名を指定します。
go generateにプロセスを駆動させるには、同じディレクトリ内の通常の(生成されていない).goファイルのいずれかに、ファイル内のどこにでもこのコメントを追加します
//go:generate goyacc -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 getによるインポートを意図している場合、ファイルが生成された後(そしてテストされた後!)は、クライアントが利用できるようにソースコードリポジトリにチェックインする必要があります。
せっかくなので、何か新しいことに使ってみましょう。go generateがどのように役立つかの非常に異なる例として、golang.org/x/toolsリポジトリにstringerという新しいプログラムがあります。これは、整数定数のセットに対して文字列メソッドを自動的に記述します。リリースされたディストリビューションの一部ではありませんが、簡単にインストールできます。
$ 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)
}
もちろん、この関数を書く方法は他にもあります。Pillでインデックス付けされた文字列のスライスを使用したり、マップを使用したり、他のテクニックを使用したりすることもできます。何をするにしても、薬のセットを変更した場合はそれを維持し、それが正しいことを確認する必要があります。(パラセタモールの2つの名前は、これを通常よりも複雑にしています。)さらに、どの方法を採用するかという問題自体が、型と値(符号付きか符号なしか、密か疎か、ゼロベースかそうでないかなど)に依存します。
stringerプログラムは、これらすべての詳細を処理します。単独で実行することもできますが、go generateによって駆動されることを意図しています。使用するには、型定義の近くなど、ソースにgenerateコメントを追加します。
//go:generate stringer -type=Pill
このルールは、go generateがstringerツールを実行して、型PillのStringメソッドを生成するように指定します。出力は自動的にpill_string.goに書き込まれます(-outputフラグで上書きできるデフォルト値です)。
実行してみましょう
$ go generate
$ cat pill_string.go
// Code generated by stringer -type Pill pill.go; DO NOT EDIT.
package painkiller
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の定義または定数を変更するたびに、実行するだけで済みます。
$ go generate
Stringメソッドを更新します。そしてもちろん、同じパッケージにこのように複数の型が設定されている場合、その単一のコマンドは、すべてのStringメソッドを単一のコマンドで更新します。
生成されたメソッドが醜いことは間違いありません。しかし、それは問題ありません。人間がそれを操作する必要がないからです。機械生成されたコードはしばしば醜いです。それは効率的であるために懸命に働いています。すべての名前は単一の文字列にまとめられ、メモリを節約します(膨大な数の名前があっても、すべての名前に対して1つの文字列ヘッダーしかありません)。次に、配列_Pill_indexが、単純で効率的な手法によって値から名前にマッピングします。また、_Pill_indexがuint8(スライスではなく配列であり、ヘッダーがさらに1つ削減されます)の配列であることにも注目してください。これは値の空間をカバーするのに十分な最小の整数です。値がもっと多い場合や負の値がある場合、生成された_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を創造的に活用してください。それは実験を奨励するためにあります。
たとえそうしなくても、新しいstringerツールを使って、あなたの整数定数用のStringメソッドを書いてください。機械に仕事をさせましょう。
次の記事: Gopher Galaは世界初のGoハッカソン
前の記事: Go 1.4がリリースされました
ブログインデックス