2014年7月28日月曜日

golangをいじりはじめてintefaceで悩んでみた.

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
golangをいじりはじめてintefaceで悩んでみた.
「驚き最小原則」という話がありますが, golangをいじっていて一番驚いた(困惑した)話をしたいと思います. 最終的にちょっとした実験の結果, 納得はしたのですが, じゃあ適切に使ってないんでは?と感じ始めてまだ答えが出てません. 知っている方のコメントをいただけると幸いです.

出てきたのは最近でもないけど, twitterTLを見ていてユーザーが劇的に増えた感じもしない (観測範囲の問題かも), golangですが, 個人的には非常に良い言語だと思います. なんせ無駄なモノがない. 覚えることが少なくていい. go getとかも考えが行き届いている感があります. 構文的にもC系の言語を知っているならかなり素早くなじめるでしょう.

で、他の言語であまり見かけない機構でかつgolangを使っていく上で避けて通れない機能がinterfaceです. 参考. intefaceという単語自体は頻出の語彙だと思うのですが, なんか違うらしいです. 他の言語をよく分かっていないので, どう違うかは語りません. 私の理解では, どういうメソッドを持っているか?というメソッドの集合がintefaceで, 気にするモノは
  • 名前
  • 引数(仮引数の名前, 型)
  • 返り値の型
です.
ほかのinterfaceを使って指定することもできます. i.e.
type X interface {
    Foo()
}

type A interface {
    X
}
        
inteface Aが持つメソッドを指定するのに他のinteface Xを指定することでFooを持つことが指定されます. この辺はio.Readerとio.Seekerからio.ReadSeekerが定義されるとかで出てきます.

これだけではなんか手応え無いので, printlnしてみましょう. playgroundで試す.
package main

type X interface{}

type x struct {
 value int
}

func MakeX(value int) X {
 r := &x{value: value}
 println(r)
 return r
}

func main() {
 v := MakeX(1)
 u := MakeX(2)
 println(v)
 println(u)
}
        
fmt.Printlnじゃなくてprintlnなのが味噌で, より低水準なモノをはいてくれるようです. なんだかこんな様なoutputを出してくれる.
0xfeee1f4c
0xfeee1f48
(0x50e20,0xfeee1f4c)
(0x50e20,0xfeee1f48)
    
1つめ, 2つめはMakeXの中でのprintlnの結果で, rが保持するアドレスです. では3つめ, 4つめのは何かというと, これがintefaceの実体(以下inteface値と呼ぶ)で, これは先頭がintefaceを表現する型へのptrで, 後ろがdataへのptrです. 値の一致の仕方から了解いただけると思う. 気になる人は実装でも追っかけてください. 参考: http://research.swtch.com/interfaces"Go Data Structures: Interfaces"

intefaceなんやねんという話からちょっとずれますが, MakeXが*xじゃなくてXなのがポイントで, structの中味と名前xをpackageの外に公開せずに済みます. (参考: go言語のコンストラクタでinterfaceを返す) testで何か標準以外のリッチな機構を使うべきか?という議論はありますが, カプセル化とかそういう話は変わらないはず.

また, inteface値が型とDataへのptrのペアだという理解に立てば, MakeXが*Xを返すのがナンセンスだという事になります. documentには明示的に書かれていないので, 習作ではやらかしました. はい.
同様にintとかの組込型でassertするのもナンセンスだと了解できます. なぜならassertという動的なテストはinteface値が保持している型情報を読み出す操作だから. その場合適切にstruct, inteface, typeでwrap(boxing?)すべし, といえるだろう. 静的に決定可能ならコンパイル時に解決されるはずで, okとか不要でしょ.

最後にinterfaceは名前を気にしない, という話.
package main

type X interface {}

type A interface {
    X
}

type B interface {
    X
}

type a struct {
}

type b struct {
}

func MakeA() A {
    return &a{}
}

func MakeB() B {
    return &b{}
}

func MakeX() X {
    return MakeB()
}

func main() {
    m := MakeX()

    switch m.(type) {
    case A:
        println("A")
    case B:
        println("B")
    case X:
        println("X")
    }
}
    
このコードの実行結果は
A
    
となる. playgroundで試す. また, caseのAとXを入れ替えるとXになるし, caseのBとAを入れ替え, かつMakeXの中味をMakeXAにするとBになる. これはintefaceの名前を見ないで, メソッドの集合のみを見て, マッチした最初のcaseを採用しているからだ. もちろんA, Bに適宜メソッドを追加し, a, b側で実装すれば, 見慣れた継承っぽい挙動をするようになる. 問題は, そのような挙動を得るためにメソッドを追加する行為が設計として正しいか?ということだ. 私にはよく分からないが, たぶん好ましいことではないだろう.

聴いてなかったりします. orz. https://twitter.com/mattn_jp/status/462403160443596800 golang 上達の道は interface 設計と channel の有効活用すね。その辺は lestrrat さんが #rebuildfm で大事さ喋ってるので聴いてない人は聴くべし。

0 件のコメント: