Upgrade to Pro — share decks privately, control downloads, hide ads and more …

型パラメータが使えるようになったのでLINQを実装してみた

MakKi
April 23, 2022

 型パラメータが使えるようになったのでLINQを実装してみた

Go Conference Online 2022 Spring

MakKi

April 23, 2022
Tweet

More Decks by MakKi

Other Decks in Programming

Transcript

  1. 自己紹介 • 牧内大輔 ◦ MakKi • KLab株式会社 ◦ スマホゲームとかつくってます ◦

    オンライン対戦の中継サーバを Goで書いたり • 過去の発表 ◦ ホットリロードツールの作り方 ▪ 2021 Spring ◦ JavaプログラムをGoに移植するためのテクニック ――継承と例外 ▪ 2019 Spring, 2019 Summer in Fukuoka @makki_d makiuchi-d
  2. 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"
  3. LINQの仕組み • IEnumerable<T> ◦ コレクションを抽象化 ▪ 配列・リスト・辞書など ◦ 1要素ずつ順番に取り出せるもの ◦

    Generic型 ▪ あらゆるコレクションに対応 • LINQのメソッド ◦ IEnumerable<T>の拡張メソッドとして実装 ◦ 1要素ずつ取り出して操作する ◦ MoveNextが呼ばれるまで処理を遅延させる interface IEnumerable<T> { IEnumerator<T> GetEnumerator(); } interface IEnumerator<T> { T Current; bool MoveNext(); void Reset(); }
  4. 型パラメータを使った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
  5. 使用例 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"
  6. コレクションの抽象化 Enumerator[T]インターフェイス • Next() ◦ 要素を1つずつ取り出すメソッド ◦ IEnumerator<T>のMoveNext()とCurrentを統合 ▪ Reset()はC#でも使われていないので無視

    ◦ 全要素を取り終えたら EOC (EndOfCollection) エラー interface Enumerator[T any] { Next() (T, error) } type Error string const EOC Error = "End of the collection"
  7. 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 }
  8. 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 }
  9. mapを扱う • KeyValue ◦ C#のDictionaryを模倣 ◦ IEnumerable<KeyValuePair<Key, Value>> • 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 }
  10. 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) }
  11. 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 } } }
  12. 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]
  13. 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)), } }
  14. 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 }
  15. ソートの実行 • 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 }
  16. 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] }
  17. 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 } }
  18. 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), }, } }
  19. 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"
  20. メソッドチェイン出来ない • 要素の型以外の型引数が必要なケース • メソッドに型パラメータを付けられない ◦ 一応Proposalは出ている (#49085) ◦ Goのメソッドの主要な役割

    ▪ interfaceを実装すること ▪ 型パラメータ付きメソッドを持つ interface • コンパイルが複雑になる func (e Enumerator[S]) Select[T any](selector func(S) (T, error)) Enumerator[T] interface { SomeFunc[T any]() }
  21. 型推論の制限 • 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 })
  22. 型パラメータ以前のLINQ • github.com/ahmetb/go-linq • 特徴 ◦ 網羅率、再現度がかなり高い ◦ メソッドチェインも実現 •

    実装方法 ◦ interface{}による抽象化 ▪ コンパイル時型検査ができない ◦ reflectによる型チェック ▪ 実行時に行われる ▪ 型パラメータより複雑