Twitter GitHub

The Go Blog 日本語訳

The Go Blogの日本語訳を公開しています。修正は https://github.com/ymotongpoo/goblog-ja/ まで。

コードのジェネレート (Generating code)

コードのジェネレート

Generating code by Rob Pike

普遍的な計算の性質、チューリング完全、とは、コンピュータプログラムがコンピュータプログラムを書けるということです。 これは、実際に評価されるべきほどには評価されていない、強力な考え方です。たとえば、これはコンパイラの定義の 大きな部分を占めています。また go test コマンドの動作にも関わってきます。 go test はテスト対象の パッケージをスキャンして、そのパッケージ用のテストハーネスを含んだGoプログラムを書き出して、 それをコンパイルして実行します。現代のコンパイラは非常に速いので、このコストが高そうな一連の処理も 1秒以内に完了できます。

プログラムがプログラムを書く例は他にもたくさんあります。たとえば Yacc は、 文法の記述を読み込んで、文法をパースするプログラムを書き出します。Protocol Bufferの「コンパイラ」は インターフェースの記述を読み込んで、構造の定義とメソッドとその他必要なコードを生成します。 あらゆる種類の設定ツールも同様の動作をします。メタデータや環境を確認して、ローカルの状態に合わせた 足場を生成してくれます。

それゆえプログラムを書くプログラムはソフトウェアエンジニアリングにおいて重要な要素ですが、 ソースコードを生成するYaccのようなプログラムは、その出力がコンパイルされるように ビルドプロセスに組み込まれる必要があります。Makeのような外部のビルドツールが使われる場合k, これは非常にたやすいことです。しかしGoでは、go toolがソースコードから必要なビルド情報を すべて取得するので、問題が有ります。go toolだけでYaccを走らせる簡単な機構がないのです。

いままでは、ありませんでした。

最新のGoのリリースである、1.4ではそのようなツールを実行しやすくする 新しいコマンドが追加されました。 go generate というコマンド名で、Goのソースコード内の特別なコメントを スキャンすることで動作します。 go generatego 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 generateyacc を起動して 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 generatestringer ツールを実行して 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_indexuint8 の配列であり、 スライスではないことに気をつけてください。 uint8 の配列なのは、値の空間をつなぐのに 必要十分な最小の整数だからです。もっと多くの値がある、あるいは負の値がある場合には、 生成される _Pill_index の型は uint16int8 になるでしょう。ようするに、対応ができる ベストな型です。

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 ツールを使ってください。 機械に仕事をさせましょう。

あわせて読みたい