インタフェースの型リストを用いた列挙型の考察

January 15, 2021

はじめに

先日、Goの公式ブログにてジェネリクス(型パラメタ)が正式なプロポーザルとして提案されたとアナウンスがありました。ブログによると、プロポーザルが承認された場合には、今年の年末にはGo 1.18のベータ版の一部として試せるものが出てくる可能性があるとのことです。

細かな仕様については、以前から公開されているDraft Designを読むか、私の年末のブログを確認してください。この記事では、プロポーザルで提案されているインタフェースの変更が今後拡張された場合にどのような応用ができるのか考察していきます。そのため、プロポーザルで提案された機能がリリースされたとしても、ここで考察している内容が実装出来る訳ではありません。ご注意ください。

型リストを持つインタフェース

プロポーザルでは、以下のようにインタフェースに型リストを記述することが出来るようになりました。

type addable interface {
    type int, int8, int16, int32, int64, uint, uint8,
    uint16, uint32, uint64, uintptr,
    float32, float64, complex64, complex128, string
}

func sum[T addable](x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

func main() {
    fmt.Println(sum([]int{1, 2, 3}))
    fmt.Println(sum([]string{"Hello, ", "World"}))
}

プロポーザルでは、このような型リストを持つインタフェースを型パラメタへの制約に用いることができます。つまり、関数sumの型パラメタTに指定できる型は、addableインタフェースに記述してある型にどれかまたはそれを規定型に持つ型に限定されます。

プロポーザルでは、型リストを持つインタフェースは制約にのみ用いることを許しています。しかし、プロポーザルが承認され、型パラメタの機能がリリースされた後に、通常のインタフェースとして使える様になる可能性もあります。

そこで、この記事では型リストを持つインタフェースが型パラメタの制約以外にも使えるようになったとして、それを用いて列挙型を定義する方法について考察します。

従来の列挙型の定義

Goには、Javaなどのように列挙型を定義する機能はありません。その代わりに、以下のようにiotaを使った列挙型のようなものを定義することが多いでしょう。

type Status int

const (
	StatusA Status = iota + 1
	StatusB
	StatusC
)

なお、iota + 1としているのは、ゼロ値がStatusAとなることを避けるためです。この方法は、Uber Go Style Guideknsh14による和訳)にも記載されています。もちろん、ゼロ値として特定の値を用いたい場合は、単にiotaと記述して0から始めても問題ありません。

iotaを使った疑似的な列挙型は、以下のように想定してる範囲を超えた値を使用される場合があります。Status型の値を123以外の値にした場合にコンパイルエラーにすることはできません。あくまでStatus型はint型を規定型にした型で、StatusAなどは単なる名前付き定数でしかありません。

// 1000に対応する名前付き定数は定義されていない
var _ = Status(1000)

型リストを持つインタフェースを用いた列挙型の定義

型リストを持つインタフェースが制約以外にも使えるようになった場合を考えます。以下のように、3種類のステータスのうちどれかの値を取るStatusインタフェースを定義します。

type Status interface {
	type statusA, statusB, statusC
	Is(Status) bool
}

また、Statusインタフェースを実装したstatusA型、statusB型、statusC型を以下のように定義します。この部分は自動生成するツールがあっても良いでしょう。

type statusA struct{}

func (statusA) Is(s Status) bool {
	_, ok := s.(statusA)
	return ok
}

type statusB struct{}

func (statusB) Is(s Status) bool {
	_, ok := s.(statusB)
	return ok
}

type statusC struct{}

func (statusC) Is(s Status) bool {
	_, ok := s.(statusC)
	return ok
}

Statusインタフェースを実装するには、statusA型、statusB型、statusC型のいずれかである必要があります。これらの型はエクスポートせずに、パッケージ外では値を生成できないようにしておきます。代わりに以下のようにStatus型の変数として公開することで、パッケージ外から扱えるようにします。

var (
	StatusA Status = statusA{}
	StatusB Status = statusB{}
	StatusC Status = statusC{}
)

Status型の値は、リストにある3つの型に限定される上に、それらはエクスポートされていないため、パッケージ外では変数StatusAStatusBStatusCを上書きできません。

Status型の値は、==では比較できないため以下のようにIsメソッドを用いて比較します。Isメソッドは前述の定義の通り、単に型による比較を行っています。

func f(s Status) {
	switch {
	case s.Is(StatusA):
		println("A")
	case s.Is(StatusB):
		println("B")
	case s.Is(StatusC):
		println("C")
	}
}

func main() {
	f(StatusA)
}

この方法で定義した列挙型(らしきもの)は、インタフェースであるためゼロ値はnilになります。そのため、nil以外のデフォルト値が必要な場合は注意が必要でしょう。

型パラメタを駆使して列挙型を定義しやすいように共通の処理をパッケージにまとめることもできそうです。

package enum

type Interface[T any] interface {
        type T
        Is(T) bool
}

type Base[T, I any] struct{}

func (Base) Is(v I) bool {
        _, ok := v.(T)
        return ok
}

これを使うとStatus型は以下のように記述できそうです。

type Status interface {
	type statusA, statusB, statusC
	enum.Interface[Status]
}

type (
	statusA struct{ enum.Base[statusA, Status] }
	statusB struct{ enum.Base[statusB, Status] }
	statusC struct{ enum.Base[statusC, Status] }
)

これは筆者の完全な妄想のコードなので型リストを持つインタフェースが制約以外に使えるようになったとしても、おかしな点があるかもしれません。

おわりに

この記事では、型リストを持つインタフェースが制約以外にも使えるようになった場合に、列挙型の定義に使えないか考察を行いました。ここで示した方法が有効になるかどうかは分かりません。しかし、将来入る可能性がある機能を考察してみることで、問題点や有効性などを見つけることが出来るかもしれません。

ひとまずは、今年の年末には型パラメタを試せるようになると考えるとワクワクしますね!