テーマ:

こんにちは

技術本部でエンジニアというかプログラマをしております、okzkと申します。

最近ようやくとっかかり始めたgo言語についてグダグダ書いてみます。とはいえgo歴1ヵ月程度のgo弱ですので、生暖かい目で読んでみてください。

go言語について

Google謹製の比較的新しめのプログラミング言語です。
詳細は「golang」でググってみてください。

最近ではDockerに代表されるようにgoで作られたメジャーなプロダクトも出てきてますし、そろそろこのビッグウェーブに(ryと思って一か月くらい試行錯誤してみた上での個人的印象は次のようなカンジです。

  • 言語設計における機能の取捨選択が非常に特徴的。
  • CSPをベースにしているだけに、並列プログラムのサポートがイケてる。
  • gopher君はまあともかくとして、擬人化マダー?

なお言語設計については、go言語FAQをみると「言語として何を取捨選択しているか」を伺い知ることができます。必読です!

go言語のgeneric型

言語仕様として切り捨てられた例外や型継承とは対照的に、FAQの中で珍しく「いつかは実装するんじゃねーの?」というカンジで言及されてる機能にgenerics型があります。
まあ、とはいえ現時点では実装されてないことには変わりないのですが、以下のような気になる記述もあります。

Meanwhile, Go's built-in maps and slices, plus the ability to use the empty interface to construct containers (with explicit unboxing) mean in many cases it is possible to write code that does what generics would enable, if less smoothly.

……ふむふむ、interface{}で頑張ればなんとかなるって?
おーけい、んじゃ試しにそれで頑張って何か実装してみようじゃないかい!

# さて、ここらへんからgo言語書いたことない人を完全に置いてけぼりにします。すみません。

んじゃ、何を書く?

そういやgo書いててsliceに対する操作のサポートが十分じゃないことにイラっとくることないですか?
ほら、map処理やselect処理とか、そういうのさらっと書きたくないですか?そんなことないですか?私は書きたいです。

ということで、goでmap処理を書いてみることにしましょう。
(golangのsliceでmap処理実装というのはこちらに先行ポストがありますが、本記事とは実装上のアプローチが全然違うのでパクリって言わないでください)

さてさて、なにも考えずに普通に型制約のあるカタチでmap処理を書くと以下のようになるかと思います。

func twice(i int) int {
    return i * 2
}

func Map(in []int, f func(i int) int) []int {
    len := len(in)
    out := make([]int, len, len)
    for i, v := range in {
        out[i] = f(v)
    }
    return out
}

func main() {
    src := []int{1, 2, 3}
    Map(src, twice) // => []int{2, 4, 6}
}

……なんとなく気持ちよくないですね。メソッドチェイン形式で記述できないからですかね?
やっぱりsrc.Map(twice).Map(twice)みたいに書きたいですよね!

型制約は後程ゴニョゴニョするとして、まずはメソッドチェインできるようにしてみましょう。

オペレータの導入

レシーバとしてテンポラリのオペレータを導入してみます。
先のMapを書き換えてみます。

type Op struct {
    Slice []int
}

func NewOp(slice []int) *Op {
    return &Op{slice}
}

func (op *Op) Map(f func(i int) int) *Op {
    len := len(op.Slice)
    out := make([]int, len, len)
    for i, v := range op.Slice {
        out[i] = f(v)
    }
    return &Op{out}
}

func main() {
    src := []int{1,2,3}
    NewOp(src).Map(twice).Map(twice).Slice // => []int{4, 8, 12}
}

メソッドチェインで書けるようになって、ちょっと気持ちよくなりました。

型制約からの解放

では、型制約を外してみましょう。
とりあえず、型はすべてinterface{}にします。

type Op struct {
    Slice interface{}
}

func (op *Op) Map(f interface{}) *Op {
    // 実装は後程
}

func main() {
    src := []int{1, 2, 3}
    NewOp(src).Map(twice).Map(twice).Slice.([]int) // => []int{4, 8, 12}
}

最後に型アサーションでの取り出しが必要になちゃいましたけど、まあ許容範囲ですね。

では肝心のMapの実装ですが、型情報が失われているためリフレクションで実行時に型をハンドリングする必要があります。

func (op *Op) Map(f interface{}) *Op {
    // Value型に変換
    vs := reflect.ValueOf(op.Slice)
    vf := reflect.ValueOf(f)

    // sliceの長さ取得
    len := vs.Len()

    // fの返り値の型でsliceを作成。appendで追加するので初期長はゼロ
    vos := reflect.MakeSlice(reflect.SliceOf(vf.Type().Out(0)), 0, len)

    // fを実行して値をつめていく。
    for i := 0; i < len; i++ {
        vos = reflect.Append(vos, vf.Call([]reflect.Value{vs.Index(i)})[0])
    }

    return &Op{vos.Interface()}
}

引数で渡すfunctionの返り値の型でsliceを作るようにしているので、型変換を伴うマップ処理もできるようになってます。

func toString(i int) string {
    return fmt.Sprintf("%d", i)
}

func main() {
    src := []int{1, 2, 3}
    NewOp(src).Map(twice).Map(twice).Map(toString).Slice.([]string) // => []string{"4", "8", "12"}
}

引数に渡すfunctionの返り値の型に合わせて、[]stringが返ってきます。
ちょっとだけイイカンジですね!

まとめ

FAQの通り、generic型がなくても、interface{}型とリフレクションでなんとかなることを示しました。

完全なソースコードはgithubで公開しています(ドキュメント等は全然ないですけど)。
やっつけですがinject/select/all/any/sort/shuffle等の処理も実装してますし、記事中では省略した実行時の型整合性チェックもしているのでよかったら見てください。

でもここまで紹介しといてアレですが、今回実装したやつ、benchmarkすると相当遅いです。
リフレクション使ってるせいでしょうけど、数十倍~数百倍というオーダです。
おまけにコンパイル時の型チェックもきかないし、ミスると実行時にpanicするしで、正直常用するにはちょっとツライですね orz...

そんなわけで早いトコ、golangにもgeneric型が導入されて静的にコンパイルできるようになってほしいと思います。

それではみなさん、ステキなgolang生活を。
ワッフルワッフル!

いいね!した人  |  リブログ(0)

サイバーエージェント 公式エンジニアブログさんの読者になろう

ブログの更新情報が受け取れて、アクセスが簡単になります

同じテーマ 「サービス・技術」 の記事