はじめまして!
Liquidのバックエンドエンジニアの谷口と申します。(画像左)
GoでLiquid eKYCや新規プロダクトの開発を行っています。
ジェネリクスのproposalがacceptされたので、調べてみました。
今回解説するproposalの範囲
このproposalではtype list
という概念を使って、ジェネリクスを実現しています。
さらに、これを改良する新しいproposalも出されています。(https://github.com/golang/go/issues/45346)
type list
を導入するのではなく、type set
という新しい概念の導入によって、ジェネリクスを実現しようというものです。
これらの2つの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)) 型推論可能なので左のように記述できる
型制約と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 }
これでint
をunderlying type
に持つ型を使用できるようになります。
underlying type
のついては下記の資料が参考になります。
入門Go言語仕様 Underlying Type / Go Language Underlying Type
簡単に説明すると、下記のような型および、int
がint
のunderlying 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()) }
これはジェネリクスを使用して下記のように修正できるでしょう。
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}})) }
同じようなメソッドを書く必要がなくなるのでバグが減りそうです。
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()) }
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()) }
感想
コピペで作られがちなコードをまとめたり、interface{}
で書いている処理を型安全に書けるようになるので良いと感じました。
複数型に対応した処理を自動生成するケースでも、かなりの量をジェネリクスにリライトできると思います。
リリースが楽しみですね。
以上です。