Goにジェネリクスが入るので、調べてみた。

f:id:nasjp:20210707165118j:plain

はじめまして!

Liquidのバックエンドエンジニアの谷口と申します。(画像左)

GoでLiquid eKYCや新規プロダクトの開発を行っています。

ジェネリクスproposalがacceptされたので、調べてみました。

今回解説するproposalの範囲

このproposalではtype listという概念を使って、ジェネリクスを実現しています。

さらに、これを改良する新しいproposalも出されています。(https://github.com/golang/go/issues/45346)

type listを導入するのではなく、type setという新しい概念の導入によって、ジェネリクスを実現しようというものです。

これらの2つのproposalを理解するには下記の記事が非常に参考になります。

Go の "Type Sets" proposal を読む

type listではなく、type setが導入されると仮定して、どのようにeKYCに活用できるか考えてみます。

Go のジェネリクス

Goにジェネリクスが導入されると、下記のようなコードを記述できるようになります。

これはどちらのproposalでも変わりません。

type A[T any] []T
func B[T any](p T) { fmt.Println(p) }
type Constraint interface{ Do() }

func C[T Constraint](p T) { p.Do() }

anyは新しく導入される予約語です。どんな型でも受け付けます。

上記のTには型制約がついているので、これを実装した型以外を渡すとコンパイルエラーになります。

型制約はinterfaceでなければなりません。

それぞれ下記のように使用します。

var m A[int] = []int{1, 2, 3}
B[int](1)
type cImplemented int

func (c cImplemented) Do() { fmt.Println(c) }

C[cImplemented](cImplemented(2))
// C(cImplemented(2)) 型推論可能なので左のように記述できる

go2go Playground

型制約とtype list

従来のinterfaceしか型制約として使用できないと困るケースがあります。

+<といった演算ができません。

func D[T Comparable](s T) {
  result := s[0] > s[1] // これができない
  fmt.Prinltn(result)
}

そのため、accept済みのproposalではtype listという概念を導入して、これを解決しています。

type Comparable interface {
    type int
}

これでintunderlying typeに持つ型を使用できるようになります。

The Go Playground

underlying typeのついては下記の資料が参考になります。

入門Go言語仕様 Underlying Type / Go Language Underlying Type

簡単に説明すると、下記のような型および、intintunderlying typeです。

type MyInt int // これ
type MyMyInt MyInt // これも
type MyMyMyInt MyMyInt // これも

下記のように複数記述することで、いずれかの型をunderlying typeに持つ型を使用できます。

type Comparable interface {
    type int, int8, int16, int32, int64
}

type listではなくtype set

新しいproposalでは、type listは無くなり、type setという概念を導入しています。

下記のように記述します。

type Comparable interface {
    int
}

int型のみしか使用できなくなります。

underlying typeは使用できません。

type MyInt int // これは使えない

underlying typeを使用するには下記のように記述します。

type Comparable interface {
    ~int
}

type MyInt int // 使えるようになる

複数の型を許容したい場合は下記のように記述します。

type Comparable interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

使用例

エンティティを表現した構造体のsliceからIDを全件取得したい

エンティティを表現するときには構造体を使用すると思います。

このような構造体のsliceからIDを全件取得するメソッドをそれぞれのsliceにすべて記述するのは結構面倒です。

type A struct {
    ID     int
    FieldA string
    FieldB string
    FieldC string
}

type As []*A

func (as As) IDs() []int {
    ids := make([]int, 0, len(as))
    for _, a := range as {
        ids = append(ids, a.ID)
    }
    return ids
}

type B struct {
    ID     int
    FieldD string
}

type Bs []*B

func (bs Bs) IDs() []int {
    ids := make([]int, 0, len(bs))
    for _, a := range bs {
        ids = append(ids, a.ID)
    }
    return ids
}

func main() {
    fmt.Println(As{{ID: 1}, {ID: 2}, {ID: 3}}.IDs())
    fmt.Println(Bs{{ID: 1}, {ID: 2}, {ID: 3}}.IDs())
}

The Go Playground

これはジェネリクスを使用して下記のように修正できるでしょう。

type Entity interface {
    PrimaryKey() int
}

func PluckID[T Entity](es []T) []int {
    ids := make([]int, 0, len(es))
    for _, e := range es {
        ids = append(ids, e.PrimaryKey())
    }
    return ids
}

type A struct {
    ID     int
    FieldA string
    FieldB string
    FieldC string
}

func (a *A) PrimaryKey() int { return a.ID }

type B struct {
    ID     int
    FieldD string
}

func (b *B) PrimaryKey() int { return b.ID }

func main() {
    fmt.Println(PluckID([]*A{{ID: 1}, {ID: 2}, {ID: 3}}))
    fmt.Println(PluckID([]*B{{ID: 1}, {ID: 2}, {ID: 3}}))
}

The go2go Playground

同じようなメソッドを書く必要がなくなるのでバグが減りそうです。

type setが下記のような制約を許すようになれば、PrimaryKeyメソッドすら実装不要になるでしょう。

type Fooer interface {
    ~struct { Foo int; Bar string; ... }
}

type MyFoo struct {
    Foo int
    Bar string
    Baz float64
}

しかし、これを質問している方がすでにおり、回答を見る限り、今回のproposalには含まれなさそうです。

https://github.com/golang/go/issues/45346#issuecomment-812670939

I'm curious whether it would be possible to allow approximations of structs to match not only the underlying type of a particular struct, but also to allow matches for structs that have at least the exact fields that are listed as the approximate struct element?

https://github.com/golang/go/issues/45346#issuecomment-812661004

Thanks, I think this is an idea for later, likely with a different syntax. I don't think we want to overload the ~ syntax just for struct types.

ポインタに変換するだけのメソッド

構造体をjsonエンコードする際、特定のフィールドに対してnullを許容したいときポインタを使用することがあると思います。

type Response struct {
    A *int    `json:"a"`
    B *string `json:"b"`
    C *bool   `json:"c"`
}

func main() {
    a := 1
    b := true
    res := &Response{
        A: &a,
        B: nil,
        C: &b,
    }

    buf := bytes.NewBuffer(nil)

    json.NewEncoder(buf).Encode(res)

    fmt.Println(buf.String())
}

The Go Playground

a = 1のように一時変数に代入するのを避けるには

それぞれの型に対してメソッドを定義する必要があります。

func uintPtr(uint v) *uint {return &v }
func intPtr(int v) *int {return &v }
func strPtr(string v) *string {return &v }

ジェネリクスを使用することで、これらのメソッドを一つにまとめられます。

func Ptr[T any](v T) *T {
    return &v
}

type Response struct {
    A *int    `json:"a"`
    B *string `json:"b"`
    C *bool   `json:"c"`
}

func main() {
    res := &Response{
        A: Ptr(1),
        B: nil,
        C: Ptr(true),
    }

    buf := bytes.NewBuffer(nil)

    json.NewEncoder(buf).Encode(res)

    fmt.Println(buf.String())
}

The go2go Playground

感想

コピペで作られがちなコードをまとめたり、interface{}で書いている処理を型安全に書けるようになるので良いと感じました。

複数型に対応した処理を自動生成するケースでも、かなりの量をジェネリクスにリライトできると思います。

リリースが楽しみですね。

以上です。