コード生成用のツールに欲しいもの3 機能をバラバラに使いたい

はじめに

(個人的なコード生成のための環境を作っている。まだ完成には程遠いので実際のチュートリアルなどは作らない)

コード生成用のツールに欲しいものを思いつくままにあげていく。

機能をバラバラに使いたい

1,2ではあると便利なものをそのままあげたのだけれど。3ではちょっと違った形で書く。この要望はコード生成用のツールに限らないかもしれない。

必要な機能に全部アクセスできるのが重要だという一方で、不要な機能は使わずにすませるということができるようになっていると嬉しい。例えば、機能がA,B,Cとあった場合に、以下のようにそれぞれの機能を独立して使い分けることができるような感じ。それぞれ好きな組み合わせで使える。

A,B,C
A,B
A,C
B,C
A
B
C

A=簡単なインデントに対応した出力機能

この機能は昔に書いたsrcgenに対する記事のgo用の簡略版。pythonで書くときには個人的には既にsrcgenを使っておらず代わりにprestringというものを作って使っているけれど。今回は機能を限定的にするためにsrcgen(のようなもの)程度の機能。両者はともに再帰的なインデント構造などに対応するのに必要な感じ(テンプレートエンジンだとちょっと無理)。

srcgenはpythonのwith syntaxを利用してインデント構造を表すものであったけれど。こちらはそれを単なる高階関数で表している。

例えば以下の様なコードを書く。一段階インデントされたような出力が欲しくなった場合には WithBlock() を使う(正確には {, } で囲まれたインデント)。

package main

import (
    "os"

    "github.com/podhmo/handwriting/indent"
)

func main() {
    o := indent.New(os.Stdout)
    o.WithBlock("func Sum(xs []int) int", func(){
        o.Println("n := 0")
        o.WithBlock("for _, x := range xs", func(){
            o.Println("n += x")
        })
        o.Println("return n")
    })
}

上のコードは実行すると以下のようなコードを出力する。基本的にはfmtパッケージをリスペクトして作っている。(実装は雑なのであとでもう少しマシにしたかったりはする)

func Sum(xs []int) int {
    n := 0
    for _, x := range xs {
        n += x
    }
    return n
}

インデント構造に対応した出力がほしい理由

以下の様に言っていた。これの理由をもう少し詳しく説明する。

再帰的なインデント構造などに対応するのに必要な感じ(テンプレートエンジンだとちょっと無理)。

似たような機能への出力対象なのだけれど。すこしだけインデント構造が変わるということはよくある。例えばポインターに対する演算が加わってくる場合だとか。ポインターなしの値に対しては以下の様なコードになるとする。

if p(x) {
    return x
}

これの対象がポインターだった場合に、nilチェックが欲しくなるかもしれない(今回の場合では条件分岐を1つにまとめるという書き方もないわけではない)。

if x != nil {
    x := *x
    if p(x) {
        return x
    }
}

こういうようなことにテンプレートエンジンだけでの出力の場合には対応しづらい。(もっとももgoに限って言うなら、インデント無しで出力してgofmtなどに任せるという形で対応しちゃっても大丈夫かもしれない)

B=型情報を利用した計算(go/types)

コード生成時に型情報を利用して計算したい。例えばnilチェックが必要な部分では常にnilチェックを追加したい。ということはgoの go/types パッケージで計算できる。

package main

import (
    "fmt"
    "go/types"
)

func main() {
    U := types.Universe
    intT := U.Lookup("int").Type()
    nilT := U.Lookup("nil").Type()

    // intはnilチェックが不要
    fmt.Println("int", types.AssignableTo(nilT, intT))

    // *intはnilチェックが必要
    fmt.Println("*int", types.AssignableTo(nilT, types.NewPointer(intT)))

    // []intもnilチェックが必要
    fmt.Println("[]int", types.AssignableTo(nilT, types.NewSlice(intT)))
}

計算した結果。

int false
*int true
[]int true

例えばこの機能と混ぜ合わせて使いたくなるかもしれない。

A,B 型情報を利用して分岐しての出力

nilチェックならインデントが増えるというような構造の出力を行おうと思えば簡単で。deferなどを上手く使うと。1つの関数でnilチェックも含んだ構造に対応することができる。

package main

import (
    "fmt"
    "go/types"
    "io"
    "os"

    "github.com/podhmo/tmp/indent"
)

func nillable(t types.Type) bool {
    U := types.Universe
    nilT := U.Lookup("nil").Type()
    return types.AssignableTo(nilT, t)
}

func writeCode(name string, t types.Type, w io.Writer) {
    o := indent.New(w)
    if nillable(t) {
        o.Printf("if %s != nil {\n", name)
        o.Indent()
        o.Printf("%s := *%s\n", name, name)
        defer func() {
            o.UnIndent()
            o.Println("}")
        }()
    }

    o.WithBlock(fmt.Sprintf("if p(%s)", name), func() {
        o.Printf("return %s\n", name)
    })
}

func main() {
    U := types.Universe
    intT := U.Lookup("int").Type()

    {
        fmt.Println("int")
        writeCode("x", intT, os.Stdout)
    }

    fmt.Println("--")

    {
        fmt.Println("map[int]int")
        writeCode("c", types.NewMap(intT, intT), os.Stdout)
    }
}

(インデントとアンインデントにブロックを組み合わせる部分は1つの関数になっていると嬉しいかもしれない)

実行結果

int
if p(x) {
    return x
}
--
map[int]int
if c != nil {
    c := *c
    if p(c) {
        return c
    }
}

再帰

そして、まず無いけれど。再帰で呼び出すように変えてあげると ************int みたいな型にも対応させられる。

func writeCodeRec(name string, t types.Type, o *indent.Output) {
    if nillable(t) {
        o.WithBlock(fmt.Sprintf("if %s ! nil", name), func() {
            o.Printf("%s := *%s\n", name, name)
            // 直接types.Pointerを決め打ちしているのは手抜き。
            writeCodeRec(name, t.(*types.Pointer).Elem(), o)
        })
    } else {
        o.WithBlock(fmt.Sprintf("if p(%s)", name), func() {
            o.Printf("return %s\n", name)
        })

    }
}

はい。

   fmt.Println("*****int")
    p := types.NewPointer
    t := p(p(p(p(p(intT)))))
    writeCodeRec("x", t, indent.New(os.Stdout))

こんなかんじに。

*****int
if x != nil {
    x := *x
    if x != nil {
        x := *x
        if x != nil {
            x := *x
            if x != nil {
                x := *x
                if x != nil {
                    x := *x
                    if p(x) {
                        return x
                    }
                }
            }
        }
    }
}

バラバラに使うということ(A,B,CやA,CやC)

他にも機能は色々あったりする。例えばgoのソースコードを読み込むloader部分をCと置いたり、あるいは複数ファイルへの出力部分をCと置いてみたり。どれでも良いのだけれど。それぞれの機能は独立していて欲しい。可能ならば読み込むパッケージも少なくなっていると嬉しい。そんなわけでindentの出力機能はhandwritingではなくhandwriting/indentになっている(goはpythonとは異なりfoo/barをimportしてもfooがimportされるということは無いのでその点も良い)。

この逆は何かというと全てが密に結合しているもので。gomockのmockgenなどは完全に1つのツールとして一個にまとまっている。それはそれで便利な場合もあるのだけれど。色々なコード生成を手軽にと言うような文脈の場合にはそれぞれの機能がバラバラに使えて欲しい。 <