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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  4. 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"

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. そうだ
     LINQ、
     実装しよう。

    View full-size slide

  9. 型パラメータを使った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

    View full-size slide

  10. 使用例
    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"

    View full-size slide

  11. コレクションの抽象化
    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"

    View full-size slide

  12. 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
    }

    View full-size slide

  13. 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
    }

    View full-size slide

  14. 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
    }

    View full-size slide

  15. 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)
    }

    View full-size slide

  16. 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
    }
    }
    }

    View full-size slide

  17. 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]

    View full-size slide

  18. 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)),
    }
    }

    View full-size slide

  19. 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
    }

    View full-size slide

  20. ソートの実行
    ● 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
    }

    View full-size slide

  21. 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]
    }

    View full-size slide

  22. 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
    }
    }

    View full-size slide

  23. 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),
    },
    }
    }

    View full-size slide

  24. 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"

    View full-size slide

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

    View full-size slide

  26. 型推論の制限
    ● 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 })

    View full-size slide

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

    View full-size slide

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

    View full-size slide