DEV Community

Pallat Anchaleechamaikorn
Pallat Anchaleechamaikorn

Posted on

สรุปจาก Robert Griesemer & Ian Lance Taylor เรื่อง Generics ในงาน GopherCon 2021

#go

ดูได้จาก YouTube

3 สิ่งหลักๆเกี่ยวกับ generics ที่ Robert อยากพูดถึงก็คือ

  1. Type Parameter สำหรับ functions และ types
  2. กำหนด type ลงไปใน interface ได้
  3. Type inference กับ generics

เริ่มจากเรื่อง Type Parameter

หน้าตาแบบนี้

[P, Q constraint#1, R constraint#2]
Enter fullscreen mode Exit fullscreen mode

คือการที่สามารถใส่ type เป็น parameter ให้กับ function ได้ตรงๆ แบบเดียวกับที่ใส่ค่าลงไปใน function นั่นแหล่ะ เพียงแค่ต้องใส่ในวงเล็บก้ามปู แทนวงเล็บปกติ มาดูตัวอย่างฟังก์ชัน min กัน

func min(x, y float64) float64 {
  if x < y {
    return x
  }
  return y
}
Enter fullscreen mode Exit fullscreen mode

ฟังก์ชันนี้ทำงานกับ float64 เท่านั้น โดยเราสามารถเปลี่ยนให้มันทำงานกับ type ตัวเลขอื่นได้ด้วยการทำให้มันเป็น generics function แบบนี้แทน

func min[T constraints.Ordered](x, y T) T {
  if x < y {
    return x
  }
  return y
}
Enter fullscreen mode Exit fullscreen mode

constraints.Ordered เราเรียกเจ้าสิ่งนี้ว่า type constraint มันคือ meta-type หรือก็คือ type ของ type อีกทีนั่นแหล่ะ ในที่นี้มาจาก package ใน standard ชื่อ constaints ความหมายของ constraint ตัวนี้คือ type ใดๆที่สามารถเอาค่ามาเปรียบเทียบด้วยเครื่องหมาย < ได้
และเวลาเอาไปเรียกใช้ก็ทำแบบนี้

m := min[int](2, 3)
Enter fullscreen mode Exit fullscreen mode

การที่เรากำหนด type ด้วยวิธีการนี้ [int] เราเรียกว่า instantiation เรารู้แค่วิธีใช้ก็พอไม่ต้องลงลึกมากเนอะ

และเราก็สามารถทำแบบนี้ได้ด้วย

fmin := min[float64]
m := fmin(2.71, 3.14)
Enter fullscreen mode Exit fullscreen mode

ก็คือสร้าง fmin ที่ไม่ใช่ generic function ละ เพราะเราทำ instantiation มันแล้ว

ต่อมา ลองมาดูการทำ Parameter Type กับ custom type ดูบ้างดีกว่า

type Tree[T interface{}] struct {
  left, right *Tree[T]
  data        T
}

func (t *Tree[T]) Lookup(x T) *Tree[T]

var stringTree Tree[string]
Enter fullscreen mode Exit fullscreen mode

นี่คือ generic type


Type constraint ก็คือ interface

ขอพักเรื่องนี้มาคุยเรื่อง constraint กันอีกสักหน่อย

โดยปกติแล้ว constraint ใน Go จะเป็น interface ซึ่งแต่เดิมมันใช้ define method เพียงอย่างเดียว แต่ตอนนี้มันสามารถใส่ type ลงไปได้ด้วย หน้าตาประมาณนี้

interface {
  int|string|bool
}
Enter fullscreen mode Exit fullscreen mode

มาดูของที่เราใช้จาก constraints กันดีกว่า

package constraints

type Ordered interface {
  Integer|Float|~string
}
Enter fullscreen mode Exit fullscreen mode

ความหมายของมันคือ มันคือ constraint ที่สามารถเป็น ทุก type ของ integer ของ floating-point และ string และไม่มี method ใดๆ

สิ่งที่แปลกตาในนี้คือ ~string นี่มีความหมายว่า type ใดก็ตามที่มี underlying เป็น string

และเราสามารถทำ in line แบบนี้ก็ได้

[S interface{ ~[]E }, E interface{}]
Enter fullscreen mode Exit fullscreen mode

และพอเห็นแบบนี้ก็ยังสามารถย่อเหลือแค่นี้ได้อีก

[S ~[]E, E interface{}]
Enter fullscreen mode Exit fullscreen mode

และเนื่องจากเราก็จะเห็น empty interface{} อยู่บ่อยมาก ก็เลยมี alias type ของมันเกิดขึ้นมาใหม่เพื่อให้โค้ดดูสั้นลงแบบนี้

[S ~[]E, E any]
Enter fullscreen mode Exit fullscreen mode

Type inference

เราจะเอาโค้ดก่อนหน้านี้มาดูอีกสักครั้ง

func min[T constraints.Ordered](x, y T) T

var a, b, m float64

m = min[float64](a, b)
Enter fullscreen mode Exit fullscreen mode

เนื่องจากเราประกาศ type ของ argument ไว้ก่อนแล้วแบบนี้ จะทำให้ compiler สามารถเดา type ที่แท้จริงของ T ได้เอง ทำให้โค้ดสามารถเขียนแค่นี้ได้

m = min(a, b)
Enter fullscreen mode Exit fullscreen mode

Robert บอกว่า เรื่อง type inference นี้มีความซับซ้อนมาก แต่สุดท้ายแค่มันใช้ง่ายก็พอแล้ว


ในส่วนของ Ian ก็มาอธิบายอีก use case หนึ่งของการเขียน generics ผมขอละไว้ก่อน แต่ส่วนที่น่าสนใจคือ Ian พูดถึงเรื่องที่ว่า แล้วเมื่อไรที่เราควรใช้ generics ซึ่ง Ian บอกว่านี่เป็นแค่คำแนะนำเท่านั้นนะ ไม่ได้เป็นกฎระเบียบแต่อย่างใด

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

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

แล้วงานประเภทไหนกันบ้างที่น่าใช้ generics

  • function ที่ทำงานกับ slices, maps และ channels โดยไม่สนใจ type ข้างใน งานแบบนี้ถ้าเอา generics มาใช้ก็น่าจะเป็นประโยชน์
  • data structure สำหรับงานสารพัดประโยชน์

ยกตัวอย่างเช่น

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
}
Enter fullscreen mode Exit fullscreen mode

โดยรวมๆแล้ว ให้ทำตาเบลอๆแล้วมองในตัวฟังก์ชันครับ เช่นใน find สิ่งเดียวที่มันทำกับค่าที่มี type T ก็คือ compare ด้วย > และ < แค่นั้น แบบนี้ก็เหมาะจะใช้ generic ละ

และคำแนะคำเมื่อต้องทำ operating ใดก็ตามกับ type parameter แนะนำให้ทำเป็น function แทนที่จะทำเป็น method

  • อีกแบบคือ method ที่มองเห็น slice ไม่ว่าจะ slice ของ type ไหนก็ตามก็ไม่ได้ต่างกันในแง่การทำงาน (ก็แบบเดียวกับข้อบนนี่หว่า)
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})
}
Enter fullscreen mode Exit fullscreen mode

จากตัวอย่างก็คือฟังก์ชันสำหรับ sort slice ใดๆ ตอนที่มันทำงาน มันไม่ได้สนว่า slice จะเป็น slice ของอะไรเลย แบบนี้เขียนเป็น generic ก็เข้าท่าดี

สุดท้ายที่ Ian พูดถึงคือ แล้วอะไรที่ไม่ควรใช้ generics

  • ก็เช่นถ้าต้องการแค่เรียก method ของ type นั้น ก็ใช้ interface แบบเดิมไป
  • หรือต้องการเรียก method ชื่อเดียวกันจาก type ที่ต่างกัน ก็ยังใช้ interface ต่อไปแหล่ะ
  • อีกกรณีคือ ถ้าต้องการทำ operation ที่ไม่สามารถทำได้กับทุก type ได้เหมือนๆกัน แบบนี้ก็ไม่ควรพยายามเอา generics มาใช้นะ

สุดท้าย Ian ให้ข้อคิดว่า อย่าพยายามคิดจะสร้างต้นแบบ อย่าใช้ type parameter ก่อนเวลาอันควร ให้รอจนกว่าจะแน่ใจว่า เรากำลังเขียนโค้ดเดิมซ้ำๆ นั่นถึงจะเป็นเวลาที่เหมาะสม

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

Top comments (0)