インタフェースの型リストを用いた列挙型の考察
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 Guide(knsh14による和訳)にも記載されています。もちろん、ゼロ値として特定の値を用いたい場合は、単にiota
と記述して0
から始めても問題ありません。
iota
を使った疑似的な列挙型は、以下のように想定してる範囲を超えた値を使用される場合があります。Status
型の値を1
、2
、3
以外の値にした場合にコンパイルエラーにすることはできません。あくまで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つの型に限定される上に、それらはエクスポートされていないため、パッケージ外では変数StatusA
、StatusB
、StatusC
を上書きできません。
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] }
)
これは筆者の完全な妄想のコードなので型リストを持つインタフェースが制約以外に使えるようになったとしても、おかしな点があるかもしれません。
おわりに
この記事では、型リストを持つインタフェースが制約以外にも使えるようになった場合に、列挙型の定義に使えないか考察を行いました。ここで示した方法が有効になるかどうかは分かりません。しかし、将来入る可能性がある機能を考察してみることで、問題点や有効性などを見つけることが出来るかもしれません。
ひとまずは、今年の年末には型パラメタを試せるようになると考えるとワクワクしますね!