unexportedなmethodを持つGoのinterfaceとsum type
何か書けと後輩に詰められたので、寝る前に雑に調べごとをしてメモを残しておくことにする。 これは8日目。
Goのinterfaceに書けるメソッド名にはidentifier
が指定されているだけで特に制限が無い。
つまり、unexportedなメソッドを定義することができる。
InterfaceType = "interface" "{" { ( MethodSpec | InterfaceTypeName ) ";" } "}" . MethodSpec = MethodName Signature . MethodName = identifier . InterfaceTypeName = TypeName .
( https://golang.org/ref/spec#Interface_types より )
周りのコードを見渡してみると、例えば reflect.Type
なんかはこれに該当する。
type Type interface { ... common() *rtype uncommon() *uncommonType }
ちなみにgo doc
は通常exportedな情報しか表示してくれないが、-u
をつけるとちゃんとunexportedの情報も見える。
こういったinterfaceはunexportedなmethodを持つため、(もちろんunexportedな型を返り値に持っているというのもあるが)パッケージの外でこれを満たす型を定義することはできない。
package foo type A interface { Hoge() int } type B interface { Hoge() int piyo() int }
package main import ( "fmt" "github.com/taxio/playground/foo" ) type X struct{} func (x *X) Hoge() int { return 0 } func NewA() foo.A { return &X{} } type Y struct{} func (y *Y) Hoge() int { return 0 } func (y *Y) piyo() int { return 0 } func NewB() foo.B { return &Y{} } func main() { fmt.Println(NewA()) fmt.Println(NewB()) }
❯ go build # github.com/taxio/playground ./main.go:20:28: cannot use &Y literal (type *Y) as type foo.B in return argument: *Y does not implement foo.B (missing foo.piyo method) have piyo() int want foo.piyo() int
調べてみるとこういうのをSealed Interfaceというらしい。 続けて「sum typeをエミュレートするのにも使える」と記述されている。
-- https://blog.chewxy.com/2018/03/18/golang-interfaces/
Sealed interfaces can only be discussed in the context of having multiple packages. A sealed interface is an interface with unexported methods. This means users outside the package is unable to create types that fulfil the interface. This is useful for emulating a sum type as an exhaustive search for the types that fulfil the interface can be done.
sum typeというのはどうやら代数的データ型というトピックの中で出てくるものらしく1、Tagged Unionなど様々な呼ばれ方があるらしい2 (wikipedia調べ)。
Tagged Unionで適当にggってると、TypeScriptの記事がよく目につく。
これはTypeScript 2.0で入った機能らしく、これを応用することでそれぞれのメンバが条件的に網羅されているかを検証できるらしい3。
確かに言われてみれば、switch文のdefault節の中にnever
で網羅性をチェックするコードを書いた記憶がある。
凄い、知らない間に代数的データ型の恩恵を受けていたのか。 数学的なバックグラウンドは全く分からないので、今度会社の人に優しそうな型の入門書を聞いてみよう4。
さて、こんな便利な機能がGoでも使える可能性があると分かったのであれば試すほかない。
先の記事で紹介されていたgo-sumtype
のREADMEを眺めながら手元で動かしてみる。
❯ go get -u github.com/BurntSushi/go-sumtype
サンプルのコードを少しいじって以下のようにしてみた。
構造体A
,B
はインターフェースX
を満たす。
package main import "fmt" //go-sumtype:decl X type X interface { sealed() } type A struct{} func (a *A) sealed() {} type B struct{} func (b *B) sealed() {} func NewXA() X { return &A{} } func main() { x := NewXA() switch x.(type) { case *A: fmt.Println("A!") case *B: fmt.Println("A!") default: panic("unreachable") } }
この状態ではgo-sumtype
は怒らない。
❯ go-sumtype main.go
しかし*A
のcaseを除くと、
... switch x.(type) { // case *A: // fmt.Println("A!") case *B: fmt.Println("A!") default: panic("unreachable") } ...
❯ go-sumtype main.go ~/go/src/github.com/taxio/playground/main.go:24:2: exhaustiveness check failed for sum type 'X': missing cases for A
しっかり条件が網羅されていないことを報告してくれた。
これを応用すればもしかして網羅的なerror
チェックみたいな面白いことができる...???
( 眠い頭で適当なことを言っています。)
おわりに
パッと調べただけだったが、思ったより面白い話が見つかった。 もうちょっとちゃんと調べて年末の技術書典で記事を出そうかな。知らんけど。
ではおやすみなさい。