TL;DR
直和型を実装するのではなく、(型の)パターンマッチの方を実装します。
型のパターンマッチはクロージャーを使って以下のように表現できます。
IntOrString.Match(Cases{ // 型のパターンマッチ
Int: func(i Int) { sum += int(i) }, // Int型だった場合の処理
String: func(s String) { sum += len(string(s)) }, // String型だった場合の処理
})
そもそもUnion型/直和型とは
Union型/直和型とはどちらか片方の型の値を取るような型のことで典型的には以下のような構文で表現されます。
type IntString = Int | String
この機能を持っている言語としては静的型付け関数型言語(Haskell/OCaml/F#)、強力な型機能のある言語(Rust/TypeScript)などがあります。
Goを使っていても、"複数の型を取りうる型"を考えたくなることがあり、この場合よくやられている方法として次のようなものがあると思います。
GoでUnion型/直和型を表現するこれまでの(あまりイケてない)やり方
型のswitch文を使う
以下のようなvalue.(type)
を使った書き方はよく見られます。
switch v := value.(type) {
case string:
sum += len(v)
case int:
sum += v
}
ここでの問題はvalue
をinterface{}
として扱う必要があり、型による保証を受けられず型安全ではありません。
structを使う
型を要素として含むようなstruct
を作るやり方もよく見られます。
type IntStringUnion struct {
Int int
String string
}
この方法の問題点は不整合な値が許容されることです。
具体的には複数のフィールドに非ゼロな値が入ってしまうことが可能で、こうなってしまうともやは直和型の値とは呼べません。
v := IntStringUnion{
Int: 10,
String: "hoge",
}
また、元となる型のzero-valueをどう表現するのかという問題もあります。
今回提案する表現方法
直和型を利用する際には、パターンマッチを使って元の型を取り出して処理しますが、このような処理を実装することを考えます。
具体的には以下のようなパターンマッチを表現するメソッドMatch
を持った型(インターフェイス)を考えます。
// IntとStringの直和型
type IntStringUnion interface {
Match(Cases)
}
メソッドMatch
に渡す構造体Cases
は、各型で行う処理を収めた構造体です。
// Int/String型の場合に実行する処理を格納する構造体
type Cases struct {
Int func(Int)
String func(String)
}
構造体Cases
に処理内容を入れて組み立て、メソッドMatch
を呼び出すことで各型ごとの処理へとディスパッチすることができます。
item.Match(Cases{
Int: func(i Int) { sum += int(i) }, // Int型だった場合の処理
String: func(s String) { sum += len(string(s)) }, // String型だった場合の処理
})
IntStringUnion
を構成する元の型、Int
とString
は以下のように定義します。
この定義により、それぞれIntStringUnion
のインターフェイスを満たすことに注意してください。
type Int int
func (i Int) Match(c Cases) { c.Int(i) } // IntStringUnionインターフェイスの実装
type String string
func (i String) Match(c Cases) { c.String(i) } // IntStringUnionインターフェイスの実装
Int
とString
共にIntStringUnion
のインターフェイスを満たすので、以下のような表現が可能となります。
unionArray := []IntStringUnion{String("1"), Int(2), String("123"), Int(4)}
全体のコードの例は以下のようになります。
package main
import (
"fmt"
)
// IntとStringの直和型
type IntStringUnion interface {
Match(Cases)
}
// Int/String型の場合に実行する処理を格納する構造体
type Cases struct {
Int func(Int)
String func(String)
}
type Int int
func (i Int) Match(c Cases) { c.Int(i) } // IntStringUnionインターフェイスの実装
type String string
func (i String) Match(c Cases) { c.String(i) } // IntStringUnionインターフェイスの実装
func main() {
// IntStringUnion型からなる配列
unionArray := []IntStringUnion{String("1"), Int(2), String("123"), Int(4)}
// 総和を計算する、Int => その値そのまま、String => 文字列の長さ、として各要素を評価して加算する
sum := 0
for _, item := range unionArray {
item.Match(Cases{ // ここで型のパターンマッチを行う
Int: func(i Int) { sum += int(i) }, // Int型だった場合の処理
String: func(s String) { sum += len(string(s)) }, // String型だった場合の処理
})
}
fmt.Printf("%d", sum) // => 10が出力される
}
補遺
上記のIntStringUnion
インターフェイスの実装だと例えばCase.Int
がnil
の場合ランタイムエラーが起きるので、以下のような実装のほうがより安全でしょう。
type Int int
func (i Int) Match(c Cases) {
if c.Int != nil {
c.Int(i)
}
}