コード生成用のツールに欲しいもの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つのツールとして一個にまとまっている。それはそれで便利な場合もあるのだけれど。色々なコード生成を手軽にと言うような文脈の場合にはそれぞれの機能がバラバラに使えて欲しい。 <

コード生成用のツールに欲しいもの2 パッケージの情報の利用

はじめに

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

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

パッケージの情報の利用

goの標準ライブラリににgo/typesという素晴らしいパッケージがある。せっかくなのでコード生成ツールなどにも利用したい。

型情報を取り扱って計算するということもできそうで夢は広がるけれど。今回はそこまでは触れない。今回の記事では表示などについてのみ触れる。

前回のコードを変えてパッケージの情報を見るようにしてみる

前回の記事でのコードに変更を加えて、パッケージの保持する情報を使うように変えてみたい。

前回のおさらいも兼ねて出力結果を再掲すると。以下のようなmessage.goというコードから。

message.go

package testdata

type Message string
const (
    MessageHello = Message("HELLO")
    MessageBye = Message("BYE")
)

以下の様な出力を得るコードを書いていた。

$ go run main.go
2018/04/04 00:43:39 package ./testcode is not found, creating.
open  message.go
----------------------------------------
package testdata

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)
}

戻り値の型を変えてみる

戻り値の型を変えてみたい。今まではstringだったのだけれど。定義したMessage型を返すようにしたい。もちろん"Message"という文字列を直接入力してしまえばかのうではあるけれど。それではあまり意味が無い。

今回はしっかりと go/types パッケージの値を利用して出力するように変えてみる。

具体的には、helloの出力部分を以下の様に変える。

   p.File("hello.go").Code(func(f *handwriting.File) error {
        message := f.PkgInfo.Pkg.Scope().Lookup("Message")
        f.Out.WithBlock(fmt.Sprintf("func Hello() %s", message.Name()), func() {
            f.Out.Println(`return MessageHello`)
        })
        return nil
    })

Code() が引数に取る *handwriting.File*loader.PackageInfo を保持していて、この値から、 *go/types/Package の値が取れる。後は通常通りにscopeから値を取り出して利用する(nilチェックは省いている)。

ちなみに変更前は以下の様なコードだった。

   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
    })

importを考慮してみる

実は先程の対応では不十分だったりする。importを考慮していない。例えば現在時刻を受け取って文字列を返すみたいなHelloWithNowみたいな関数を作ってみる(今回の戻り値はstring)。

注意点として、現在時刻を受け取る部分の引数部分に time.Time という表示で出力される必要がある。timeパッケージをimportして使っているので Time ではだめ。

期待する出力結果は以下のようなコード。

func HelloWithNow(now time.Time) string {
    return fmt.Sprintf("%s %s\n", MessageHello, now)
}

先程のコードでの型部分の出力のコードは fmt.Sprintf("func Hello() %s", message.Name() だった。message.Name()は単に自身の名前を文字列として返すだけなのでimportが考慮できていない。

importを考慮するにはResolverというものを使う。

    f2 := p.File("hello2.go")
    f2.Import("time")
    f2.Code(func(f *handwriting.File) error {
        t := f.Prog.Package("time").Pkg.Scope().Lookup("Time")
        f.Out.WithBlock(fmt.Sprintf("func HelloWithNow(now %s) string", f.Resolver.TypeName(t.Type())), func() {
            f.Out.Printfln(`return fmt.Sprintf("%%s %%s\n", MessageHello, now)`)
        })
        return nil
    })

timeパッケージに依存するのでimportする必要がある。あとは、f.Progに *loader/Program の値が格納されていて、そこからparse済みの*loader/PackageInfo の値が取得できる。f.Resolver で取り出したResolverの TypeName()types/Typeインターフェイスを実装する値を渡してあげると良い感じに出力してくれる。

$ go run main.go
... snip
open  hello2.go
----------------------------------------
package testcode

import (
    "time"
)

func HelloWithNow(now time.Time) string {
    fmt.Sprintf("%s %s\n", MessageHello, now)
}

別名でimportした場合

別名でのimportもサポートしたい。例えば以下のようなimportであった場合。

import xtime "time"

これは先程の f.Import("time") 部分を f.ImportWithName("time", "xtime") に変えてあげれば対応できる。

   f2 := p.File("hello2.go")
    f2.ImportWithName("time", "xtime")
    f2.Code(func(f *handwriting.File) error {
        t := f.Prog.Package("time").Pkg.Scope().Lookup("Time")
        f.Out.WithBlock(fmt.Sprintf("func HelloWithNow(now %s) string", f.Resolver.TypeName(t.Type())), func() {
            f.Out.Printfln(`return fmt.Sprintf("%%s %%s\n", MessageHello, now)`)
        })
        return nil
    })

大丈夫。

$ go run main.go
... snip
open  hello2.go
----------------------------------------
package testcode

import (
    xtime "time"
)

func HelloWithNow(now xtime.Time) string {
    return fmt.Sprintf("%s %s\n", MessageHello, now)
}

戻り値のタプルを考慮してみる

関数の戻り値で厄介なのはtupleのとき。今度は戻り値で現在時刻とメッセージ(message.goで定義した型)を返すようにしてみる。

期待する出力結果は以下のようなコード。

func HelloWithNow2() (Message, time.Time) {
    now := time.Now()
    return MessageHello, now
}

タプルの部分を直接 fmt.Sprintf("(%s, %s)", "Message", "time.Time") となるように書いてみても良いけれど。0~N個の引数を取り、0~N個の戻り値を返すというような関数のコードをを生成したい場合にちょっと困る(このような処理はmock用のコードの自動生成などでしばしば現れる)。

これは、素直に types.Tuple の値を自分で作る(このあたりは何かヘルパー関数があっても良いかもしれない)。そして型名の部分にはTypeNameForResults()を使う。

   f3 := p.File("hello3.go")
    f3.Import("time")
    f3.Code(func(f *handwriting.File) error {
        timepkg := f.Prog.Package("time").Pkg

        messageT := f.PkgInfo.Pkg.Scope().Lookup("Message").Type()
        timeT := timepkg.Scope().Lookup("Time").Type()
        retvalT := types.NewTuple(
            types.NewVar(token.NoPos, f.PkgInfo.Pkg, "", messageT),
            types.NewVar(token.NoPos, timepkg, "", timeT),
        )
        f.Out.WithBlock(fmt.Sprintf("func HelloWithNow2() %s", f.Resolver.TypeNameForResults(retvalT)), func() {
            f.Out.Println("now := time.Now()")
            f.Out.Println("return MessageHello, now")
        })
        return nil
    })

TypeNameForResults() を使う理由はtupleの長さによって表示が変わるため。

長さ0のとき func F0() { }

長さ1のとき func F1() X { }

長さ2のとき func F2() (X,Y) { }

長さNのとき func F2() (X0,X1,...,Xn) { }

特に0のときと1のときにカッコなどが表示されてほしくない。

$ go run main.go
... snip
open  hello3.go
----------------------------------------
package testcode

import (
    "time"
)

func HelloWithNow2() (Message, time.Time) {
    now := time.Now()
    return MessageHello, now
}