Slide 1

Slide 1 text

型パラメータが 使えるようになったので LINQを実装してみた Go Conference 2022 Spring  牧内大輔(MakKi/@makki_d)

Slide 2

Slide 2 text

自己紹介 ● 牧内大輔 ○ MakKi ● KLab株式会社 ○ スマホゲームとかつくってます ○ オンライン対戦の中継サーバを Goで書いたり ● 過去の発表 ○ ホットリロードツールの作り方 ■ 2021 Spring ○ JavaプログラムをGoに移植するためのテクニック ――継承と例外 ■ 2019 Spring, 2019 Summer in Fukuoka @makki_d makiuchi-d

Slide 3

Slide 3 text

LINQとは

Slide 4

Slide 4 text

LINQとは 様々なコレクションを クエリのように操作できる機能 ● 高階関数 ○ コレクションに対する処理の抽象化 ● Generics ○ 静的型付け ● 遅延評価 ○ ToArray等されて初めて並べ替えなどの処理が行われる

Slide 5

Slide 5 text

LINQとは 様々なコレクションを クエリのように操作できる機能 ● 高階関数 ○ コレクションに対する処理の抽象化 ● Generics ○ 静的型付け ● 遅延評価 ○ ToArray等されて初めて並べ替えなどの処理が行われる using System.Linq; var src = new int[] { 3, 8, 2, 1, 5, 7, 4, 6 }; var e = src .Where(n => n % 2 == 0) .OrderBy(n => n) .Select(n => $"{n}"); var arr = e.ToArray(); Console.WriteLine(string.Join(",", arr)); // "2,4,6,8"

Slide 6

Slide 6 text

LINQの仕組み ● IEnumerable ○ コレクションを抽象化 ■ 配列・リスト・辞書など ○ 1要素ずつ順番に取り出せるもの ○ Generic型 ■ あらゆるコレクションに対応 ● LINQのメソッド ○ IEnumerableの拡張メソッドとして実装 ○ 1要素ずつ取り出して操作する ○ MoveNextが呼ばれるまで処理を遅延させる interface IEnumerable { IEnumerator GetEnumerator(); } interface IEnumerator { T Current; bool MoveNext(); void Reset(); }

Slide 7

Slide 7 text

Go 1.18 リリースおめでとうございます

Slide 8

Slide 8 text

型パラメータ 待望のジェネリクスが導入

Slide 9

Slide 9 text

そうだ  LINQ、  実装しよう。

Slide 10

Slide 10 text

型パラメータを使ったLINQ 実装しました:github.com/makiuchi-d/linq Aggregate All Any Average AverageFunc Chunk Concat Contains ContainsFunc Count DefaultIfEmpty Distinct DistinctBy ElementAt ElementAtOrDefault Empty Except ExceptBy First FirstOrDefault ForEach FromMap FromSlice GroupBy GroupJoin Intersect IntersectBy Join Last LastOrDefault Max MaxBy MaxByFunc Min MinBy MinByFunc OrderBy OrderByDescending OrderByFunc Range Repeat Reverse Select SelectMany Single SingleOrDefault Skip SkipLast SkipWhile Sum Sumf SumByFunc SumByFuncf Take TakeLast TakeWhile ThenBy ThenByDescending ToMap ToMapFunc ToSlice Union UnionBy Where Zip

Slide 11

Slide 11 text

使用例 using System.Linq; var src = new int[] { 3, 8, 2, 1, 5, 7, 4, 6 }; var e = src .Where(n => n % 2 == 0) .OrderBy(n => n) .Select(n => $"{n}"); var arr = e.ToArray(); Console.WriteLine(string.Join(",", arr)); // "2,4,6,8" src := []int{ 3, 8, 2, 1, 5, 7, 4, 6 } e1 := linq.FromSlice(src) e2 := linq.Where(e1, func(n int)(bool, error){ return n%2==0, nil }) e3 := linq.OrderBy(e2, func(n int)(int, error) { return n, nil }) e4 := linq.Select[int](e3, func(n int) (string, error) { return strconv.Itoa(n), nil }) arr, _ := linq.ToSlice(e4) fmt.Println(strings.Join(arr, ",")) // "2,4,6,8"

Slide 12

Slide 12 text

コレクションの抽象化 Enumerator[T]インターフェイス ● Next() ○ 要素を1つずつ取り出すメソッド ○ IEnumeratorのMoveNext()とCurrentを統合 ■ Reset()はC#でも使われていないので無視 ○ 全要素を取り終えたら EOC (EndOfCollection) エラー interface Enumerator[T any] { Next() (T, error) } type Error string const EOC Error = "End of the collection"

Slide 13

Slide 13 text

sliceを扱う ● FromSlice ○ 型引数Sを~[]Tとして定義 ■ []Tを元にした型すべてに対応 ● sliceEnumerator ○ 元のsliceとindexを保持 ○ indexを増やしながら要素を返す ○ indexがlenを超えたらEOC ■ 戻り値def ● 宣言のみはゼロ値初期化される ○ 0だったりnilだったりする ● ver def T を書いても良い func FromSlice[T any, S ~[]T](src S) Enumerator[T] { return &sliceEnumerator[T]{ src: src } } type sliceEnumerator[T any] struct { src []T i int } func (e *sliceEnumerator[T]) Next() (def T, _ error) { if e.i >= len(e.src) { return def, EOC } i := e.i e.i++ return e.src[i], nil }

Slide 14

Slide 14 text

sliceへ ● ToSlice ○ src.Next()で一つずつ取り出すループ ■ EOCで終了 ○ sliceにappendしていく ○ どんなEnumerator[T]でもsliceに func ToSlice[T any](src Enumerator[T]) ([]T, error) { s := make([]T, 0) for { v, err := src.Next() if err != nil { if isEOC(err) { break } return s, err } s = append(s, v) } return s, nil }

Slide 15

Slide 15 text

mapを扱う ● KeyValue ○ C#のDictionaryを模倣 ○ IEnumerable> ● FromMap ○ ~map[K]Vであらゆるmapに対応 ○ mapのキーはcomparable ● mapEnumerator ○ 初回呼び出し時キーリストを生成 ○ キーリストとindexでmapから取り出す ○ KeyValueにしてreturn type KeyValue[K comparable, V any] struct { Key K Value V } func FromMap[T ~map[K]V, K comparable, V any](m T) Enumerator[KeyValue[K, V]] { return &mapEnumerator[K, V]{m: m} } type mapEnumerator[K comparable, V any] struct { m map[K]V k []K i int } func (e *mapEnumerator[K, V]) Next() (def KeyValue[K, V], _ error) { if e.k == nil { ks := make([]K, 0, len(e.m)) for k := range e.m { ks = append(ks, k) } e.k = ks } if e.i >= len(e.k) { return def, EOC } k := e.k[e.i] e.i++ return KeyValue[K, V]{Key: k, Value: e.m[k]}, nil }

Slide 16

Slide 16 text

Selectの実装 ● 変換関数を受け取る ○ S→T ○ 型引数が2つ ● 一つずつ取り出して関数適用 ○ src.Next()のエラー ■ EOCも含まれる ● そのまま返せばOK func Select[S, T any]( src Enumerator[S], selector func(v S) (T, error)) Enumerator[T] { return &selectEnumerator[S, T]{src: src, sel: selector} } type selectEnumerator[S, T any] struct { src Enumerator[S] sel func(v S) (T, error) } func (e *selectEnumerator[S, T]) Next() (def T, _ error) { v, err := e.src.Next() if err != nil { return def, err } return e.sel(v) }

Slide 17

Slide 17 text

Whereの実装 ● フィルタする関数を受け取る ○ trueのものだけを集める ● ループ内で1つずつ取り出して処理 ○ フィルタ関数がtrueになったら返す ○ falseなら次の要素 ○ フィルタされたものだけが返ってくる func Where[T any](src Enumerator[T], pred func(v T) (bool, error)) Enumerator[T] { return &whereEnumerator[T]{src: src, pred: pred} } type whereEnumerator[T any] struct { src Enumerator[T] pred func(v T) (bool, error) } func (e *whereEnumerator[T]) Next() (def T, _ error) { for { v, err := e.src.Next() if err != nil { return def, err } ok, err := e.pred(v) if err != nil { return def, err } if ok { return v, nil } } }

Slide 18

Slide 18 text

OrderBy/ThenByの実装 ● 要素からソートキーを取り出す関数を渡す ○ ソートキーはconstraints.Ordered (大小関係のある型) ● まとめて処理する必要がある ○ ThenByで第二ソートキー以下を追加していく ○ 例えばスポーツで勝ち点が同じだったら得失点差で順位をつける場合 ■ 勝点(Points)でOrderByしたあと、得失点差(GoalDifference)でThenBy teams.OrderBy(team => team.Points).ThenBy(team => team.GoalDifference); func OrderBy[T any, K constraints.Ordered](src Enumerator[T], keySelector func(T) (K, error)) *OrderedEnumerator[T] func ThenBy[T any, K constraints.Ordered](src *OrderedEnumerator[T], keySelector func(T) (K, error)) *OrderedEnumerator[T]

Slide 19

Slide 19 text

OrderedEnumerator ● ThenByの引数の型を制限 ○ *OrderedEnumerator[T] ■ OrderBy/ThenByの戻り値 ○ OrderBy/ThenByの直後のみThenBy可能 ● newcmps ○ comparer生成関数のリスト ■ 比較ロジックオブジェクト ■ 遅延評価するため関数を保持 ● 最初のNext()で呼び出す ○ ThenByするごとにappendしていく type OrderedEnumerator[T any] struct { src Enumerator[T] newcmps []func([]T) (comparer, error) sorted []T i int } func OrderBy[T any, K constraints.Ordered]( src Enumerator[T], keySelector func(T) (K, error)) *OrderedEnumerator[T] { return &OrderedEnumerator[T]{ src: src, newcmps: []func([]T) (comparer, error){ newKeyComparer[kCmpAsc[K]](keySelector), }, } } func ThenBy[T any, K constraints.Ordered]( src *OrderedEnumerator[T], keySelector func(T) (K, error)) *OrderedEnumerator[T] { return &OrderedEnumerator[T]{ src: src.src, newcmps: append(src.newcmps, newKeyComparer[kCmpAsc[K]](keySelector)), } }

Slide 20

Slide 20 text

Nextメソッド ● 初回呼び出しでソート ○ e.sortedに保持 ○ あとはindexを増やしながら要素を返す ■ sliceと同じ func (e *OrderedEnumerator[T]) Next() (def T, _ error) { if e.sorted == nil { s, err := doSort(e.src, e.newcmps) if err != nil { return def, err } e.sorted = s } if e.i >= len(e.sorted) { return def, EOC } i := e.i e.i++ return e.sorted[i], nil }

Slide 21

Slide 21 text

ソートの実行 ● doSort ○ 一度sliceに書き出す ○ 標準ライブラリのsort.Sortでソート ○ sort.Interface ■ Len/Less/Swap ● Less ○ 要素を比較する ○ comparer.compareを順に試す ■ s.cmpsはOrderBy/ThenByされた順 func doSort[T any](src Enumerator[T], newcmps []func([]T) (comparer, error)) ([]T, error) { s, err := ToSlice(src) if err != nil { return nil, err } cmps := make([]comparer, len(newcmps)) for i, newcmp := range newcmps { cmps[i], err = newcmp(s) if err != nil { return nil, err } } sort.Sort(&sorter[T]{src: s, cmps: cmps}) return s, nil } func (s *sorter[T]) Less(i, j int) bool { for _, cmp := range s.cmps { switch c := cmp.compare(i, j); true { case c < 0: return true case c > 0: return false } } return true }

Slide 22

Slide 22 text

comparerの実体 ● ソートキーの配列 ○ 昇順用 kCmpAsc[K] ○ 降順用 kCmpDesc[K] ● compare ○ 比較するメソッド ■ 降順用は返す符号が逆 ■ 文字列もあるので引き算はできない ● swap ○ ソートする配列と合わせてソートキーも swap type comparer interface { compare(i, j int) int swap(i, j int) } type kCmpAsc[K constraints.Ordered] []K type kCmpDesc[K constraints.Ordered] []K func (c kCmpAsc[K]) compare(i, j int) int { switch { case c[i] < c[j]: return -1 case c[i] > c[j]: return 1 default: return 0 } } func (c kCmpAsc[K]) swap(i, j int) { c[i], c[j] = c[j], c[i] }

Slide 23

Slide 23 text

comparer生成関数を生成する関数 ● ソートキーの配列 ○ keySelectorでキーを取り出す ● 型変数Cで型変換 ○ kCmpAsc[K] | kCmpDesc[K] ■ どちらも[]Kなので型変換できる ○ comparerを含むよう明示 ■ comparerとして返せる ■ 無いとコンパイルエラー type keyComparer[K constraints.Ordered] interface { kCmpAsc[K] | kCmpDesc[K] comparer } func newKeyComparer[C keyComparer[K], T any, K constraints.Ordered]( keysel func(T) (K, error)) func(s []T) (comparer, error) { return func(s []T) (comparer, error) { ks := make([]K, len(s)) for i, t := range s { k, err := keysel(t) if err != nil { return nil, err } ks[i] = k } return C(ks), nil } }

Slide 24

Slide 24 text

newKeyComparer[C, T, K]呼び出し ● 型引数Cは引数に含まれない ○ 型推論できないので明示する ● 型引数は途中からでも省略可能 ○ 書く順序は変更できない ○ 明示するものを先頭にして推論できるものを省略 func OrderBy[T any, K constraints.Ordered](src Enumerator[T], keySelector func(T) (K, error)) *OrderedEnumerator[T] { return &OrderedEnumerator[T]{ src: src, newcmps: []func([]T) (comparer, error){ newKeyComparer[kCmpAsc[K]](keySelector), }, } }

Slide 25

Slide 25 text

LINQが実装できた ● 静的型付け ○ e1~e3: Enumerator[int] ○ e4: Enumerator[string] ○ arr: []string ○ 引数の関数も型検査される ■ 違うとコンパイルエラー ● 遅延評価 ○ e4まではEnumerator[T]を作るだけ ○ ToSlice()されて初めてNext()が呼ばれる src := []int{ 3, 8, 2, 1, 5, 7, 4, 6 } e1 := linq.FromSlice(src) e2 := linq.Where(e1, func(n int)(bool, error){ return n%2==0, nil }) e3 := linq.OrderBy(e2, func(n int)(int, error) { return n, nil }) e4 := linq.Select[int](e3, func(n int) (string, error) { return strconv.Itoa(n), nil }) arr, _ := linq.ToSlice(e4) fmt.Println(strings.Join(arr, ",")) // "2,4,6,8"

Slide 26

Slide 26 text

メソッドチェイン出来ない ● 要素の型以外の型引数が必要なケース ● メソッドに型パラメータを付けられない ○ 一応Proposalは出ている (#49085) ○ Goのメソッドの主要な役割 ■ interfaceを実装すること ■ 型パラメータ付きメソッドを持つ interface ● コンパイルが複雑になる func (e Enumerator[S]) Select[T any](selector func(S) (T, error)) Enumerator[T] interface { SomeFunc[T any]() }

Slide 27

Slide 27 text

型推論の制限 ● e3 : *OrderedEnumerator[int] ○ OrderBy/ThenByの戻り値 ○ Enumerator[int]を実装している ■ メソッドを調べないと実装しているかわからない ● 型推論の条件 ○ 型変数以外の部分が一致している必要がある ■ Enumerator ≠ *OrderedEnumerator ■ どの型のインターフェイスを実装しているかはメソッドを調べないとわからない e3 := linq.OrderBy(e2, func(n int)(int, error) { return n, nil }) e4 := linq.Select[int](e3, func(n int) (string, error) { return strconv.Itoa(n), nil })

Slide 28

Slide 28 text

型パラメータ以前のLINQ ● github.com/ahmetb/go-linq ● 特徴 ○ 網羅率、再現度がかなり高い ○ メソッドチェインも実現 ● 実装方法 ○ interface{}による抽象化 ■ コンパイル時型検査ができない ○ reflectによる型チェック ■ 実行時に行われる ■ 型パラメータより複雑

Slide 29

Slide 29 text

まとめ ● ほとんどのLINQの関数を実装できた ○ Goの型パラメータは十分実用的 ● まだ使いにくいところもある ○ メソッドに型パラメータが使えない ○ 型推論が弱め