สรุปจาก Robert Griesemer & Ian Lance Taylorเรื่อง ジェネリックในงาน Gophercon 2021

17001 ワード

ดูได้จาก
3สิ่งหลักๆเกี่ยวกับ ジェネリックที่ ロバートอยากพูดถึงก็คือ
  • 型パラメーターสำหรับ 関数และ 種別
  • กำหนด 種類ลงไปใน インターフェースได้
  • 型推論กับ ジェネリック
  • เริ่มจากเรื่อง 型パラメータ


    หน้าตาแบบนี้
    [P, Q constraint#1, R constraint#2]
    
    คือการที่สามารถใส่ 種類เป็น パラメータให้กับ 機能ได้ตรงๆ แบบเดียวกับที่ใส่ค่าลงไปใน 機能นั่นแหล่ะ เพียงแค่ต้องใส่ในวงเล็บก้ามปู แทนวงเล็บปกติ มาดูตัวอย่างฟังก์ชัน 分กัน
    func min(x, y float64) float64 {
      if x < y {
        return x
      }
      return y
    }
    
    ฟังก์ชันนี้ทำงานกับ float 64เท่านั้น โดยเราสามารถเปลี่ยนให้มันทำงานกับ 種類ตัวเลขอื่นได้ด้วยการทำให้มันเป็น ジェネリック関数แบบนี้แทน
    func min[T constraints.Ordered](x, y T) T {
      if x < y {
        return x
      }
      return y
    }
    
    制約注文するเราเรียกเจ้าสิ่งนี้ว่า 型制約มันคือ メタタイプหรือก็คือ 種類ของ 種類อีกทีนั่นแหล่ะ ในที่นี้มาจาก パッケージใน 標準ชื่อ コンスタンスความหมายของ 制約ตัวนี้คือ 種類ใดๆที่สามารถเอาค่ามาเปรียบเทียบด้วยเครื่องหมาย <ได้
    และเวลาเอาไปเรียกใช้ก็ทำแบบนี้
    m := min[int](2, 3)
    
    การที่เรากำหนด 種類ด้วยวิธีการนี้ [int]เราเรียกว่า インスタンス化เรารู้แค่วิธีใช้ก็พอไม่ต้องลงลึกมากเนอะ
    และเราก็สามารถทำแบบนี้ได้ด้วย
    fmin := min[float64]
    m := fmin(2.71, 3.14)
    
    ก็คือสร้าง Fminที่ไม่ใช่ 汎用関数ละ เพราะเราทำ インスタンス化มันแล้ว
    ต่อมา ลองมาดูการทำ パラメータ型กับ カスタムタイプดูบ้างดีกว่า
    type Tree[T interface{}] struct {
      left, right *Tree[T]
      data        T
    }
    
    func (t *Tree[T]) Lookup(x T) *Tree[T]
    
    var stringTree Tree[string]
    
    นี่คือ ジェネリックタイプ

    型制約ก็คือ インターフェース


    ขอพักเรื่องนี้มาคุยเรื่อง 制約กันอีกสักหน่อย
    โดยปกติแล้ว 制約ใน 試みจะเป็น インターフェースซึ่งแต่เดิมมันใช้ 定義メソッドเพียงอย่างเดียว แต่ตอนนี้มันสามารถใส่ 種類ลงไปได้ด้วย หน้าตาประมาณนี้
    interface {
      int|string|bool
    }
    
    มาดูของที่เราใช้จาก 制約กันดีกว่า
    package constraints
    
    type Ordered interface {
      Integer|Float|~string
    }
    
    ความหมายของมันคือ มันคือ 制約ที่สามารถเป็น ทุก 種類ของ 整数ของ 浮動小数点และ 文字列และไม่มี 方法ใดๆ
    สิ่งที่แปลกตาในนี้คือ ~文字列นี่มีความหมายว่า 種類ใดก็ตามที่มี 基礎เป็น 文字列
    และเราสามารถทำ イン・ラインแบบนี้ก็ได้
    [S interface{ ~[]E }, E interface{}]
    
    และพอเห็นแบบนี้ก็ยังสามารถย่อเหลือแค่นี้ได้อีก
    [S ~[]E, E interface{}]
    
    และเนื่องจากเราก็จะเห็น 空のインターフェースอยู่บ่อยมาก ก็เลยมี エイリアスタイプของมันเกิดขึ้นมาใหม่เพื่อให้โค้ดดูสั้นลงแบบนี้
    [S ~[]E, E any]
    

    型推論


    เราจะเอาโค้ดก่อนหน้านี้มาดูอีกสักครั้ง
    func min[T constraints.Ordered](x, y T) T
    
    var a, b, m float64
    
    m = min[float64](a, b)
    
    เนื่องจากเราประกาศ 種類ของ 引数ไว้ก่อนแล้วแบบนี้ จะทำให้ コンパイラสามารถเดา 種類ที่แท้จริงของ T ได้เอง ทำให้โค้ดสามารถเขียนแค่นี้ได้
    m = min(a, b)
    
    ロバートบอกว่า เรื่อง 型推論นี้มีความซับซ้อนมาก แต่สุดท้ายแค่มันใช้ง่ายก็พอแล้ว
    ในส่วนของ 庵ก็มาอธิบายอีก ユースケースหนึ่งของการเขียน ジェネリックผมขอละไว้ก่อน แต่ส่วนที่น่าสนใจคือ 庵พูดถึงเรื่องที่ว่า แล้วเมื่อไรที่เราควรใช้ ジェネリックซึ่ง 庵บอกว่านี่เป็นแค่คำแนะนำเท่านั้นนะ ไม่ได้เป็นกฎระเบียบแต่อย่างใด

    จงเขียนโค้ด อย่าไปออกแบบ type


    อย่าไปติดหล่มด้วยการออกแบบ ジェネリックก่อน แต่ให้เริ่มจากเขียนโค้ดให้มันทำงานได้ แล้วค่อยใส่ ジェネリックไปทีหลังก็ไม่สาย

    แล้วงานประเภทไหนกันบ้างที่น่าใช้ ジェネリック

  • 機能ที่ทำงานกับ スライス、マップและ チャンネルโดยไม่สนใจ 種類ข้างใน งานแบบนี้ถ้าเอา ジェネリックมาใช้ก็น่าจะเป็นประโยชน์
  • データ構造สำหรับงานสารพัดประโยชน์
  • ยกตัวอย่างเช่น
    type Tree[T any] struct {
      cmp  func(T, T) int
      root *node[T]
    }
    
    type node[T any] struct {
      left, right *node[T]
      data
    }
    
    func (bt *Tree[T]) find(val T) **node[T] {
      pl := &bt.root
      for *pl != nil {
        switch cmp := bt.cmp(val, (*pl).data); {
          case cmp < 0: pl = &(*pl).left
          case cmp > 0: pl = &(*pl).right
          default: return pl
        }
      }
      return pl
    }
    
    โดยรวมๆแล้ว ให้ทำตาเบลอๆแล้วมองในตัวฟังก์ชันครับ เช่นใน 見つけるสิ่งเดียวที่มันทำกับค่าที่มี タイプTก็คือ 比較するด้วย >และ <แค่นั้น แบบนี้ก็เหมาะจะใช้ ジェネリックละ
    และคำแนะคำเมื่อต้องทำ 操作ใดก็ตามกับ 型パラメータแนะนำให้ทำเป็น 機能แทนที่จะทำเป็น 方法
  • อีกแบบคือ 方法ที่มองเห็น スライスไม่ว่าจะ スライスของ 種類ไหนก็ตามก็ไม่ได้ต่างกันในแง่การทำงาน (ก็แบบเดียวกับข้อบนนี่หว่า)
  • type SliceFn[T any] struct {
      s []T
      cmp func(T, T) bool
    }
    
    func (s SliceFn[T]) Len() int { return len(s.s) }
    func (s SliceFn[T]) Swap(i, j int) {
      s.s[i], s.s[j] = s.s[j], s.s[i]) }
    func (s SliceFn[T]) Less(i, j int) bool {
      return s.cmp(s.s[i], s.s[j]) }
    
    func SortFn[T any](s []T, cmp func(T, T) bool) {
      sort.Sort(SliceFn[T]{s, cmp})
    }
    
    จากตัวอย่างก็คือฟังก์ชันสำหรับ ソートスライスใดๆ ตอนที่มันทำงาน มันไม่ได้สนว่า スライスจะเป็น スライスของอะไรเลย แบบนี้เขียนเป็น ジェネリックก็เข้าท่าดี

    สุดท้ายที่ 庵พูดถึงคือ แล้วอะไรที่ไม่ควรใช้ ジェネリック

  • ก็เช่นถ้าต้องการแค่เรียก 方法ของ 種類นั้น ก็ใช้ インターフェースแบบเดิมไป
  • หรือต้องการเรียก 方法ชื่อเดียวกันจาก 種類ที่ต่างกัน ก็ยังใช้ インターフェースต่อไปแหล่ะ
  • อีกกรณีคือ ถ้าต้องการทำ 操作ที่ไม่สามารถทำได้กับทุก 種類ได้เหมือนๆกัน แบบนี้ก็ไม่ควรพยายามเอา ジェネリックมาใช้นะ
  • สุดท้าย 庵ให้ข้อคิดว่า อย่าพยายามคิดจะสร้างต้นแบบ อย่าใช้ 型パラメータก่อนเวลาอันควร ให้รอจนกว่าจะแน่ใจว่า เรากำลังเขียนโค้ดเดิมซ้ำๆ นั่นถึงจะเป็นเวลาที่เหมาะสม

    การมี tools ที่ดีนั้นสำคัญ แต่การใช้ tools ให้ถูกที่ถูกเวลา ยิ่งสำคัญกว่า