LoginSignup
36
15

More than 3 years have passed since last update.

GoでUnion型/直和型をいい感じに表現する方法

Posted at

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
}

ここでの問題はvalueinterface{}として扱う必要があり、型による保証を受けられず型安全ではありません

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を構成する元の型、IntStringは以下のように定義します。
この定義により、それぞれ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インターフェイスの実装

IntString共に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.Intnilの場合ランタイムエラーが起きるので、以下のような実装のほうがより安全でしょう。

type Int int

func (i Int) Match(c Cases) {
    if c.Int != nil {
        c.Int(i)
    }
}
36
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
15