สรุปจาก Robert Griesemer & Ian Lance Taylor เรื่อง Generics ในงาน GopherCon 2021
Tags: go
Published: 2022-03-25 14:50:56

ดูได้จาก [YouTube](https://www.youtube.com/watch?v=Pa_e9EeCdy8&list=WL&index=11&t=45s)

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

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

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

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

```go
[P, Q constraint#1, R constraint#2]
```

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

```go
func min(x, y float64) float64 {
  if x < y {
    return x
  }
  return y
}
```

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

```go
func min[T constraints.Ordered](x, y T) T {
  if x < y {
    return x
  }
  return y
}
```

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

```go
m := min[int](2, 3)
```

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

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

```go
fmin := min[float64]
m := fmin(2.71, 3.14)
```

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

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

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

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

var stringTree Tree[string]
```

นี่คือ generic type

---

## Type constraint ก็คือ interface

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

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

```go
interface {
  int|string|bool
}
```

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

```go
package constraints

type Ordered interface {
  Integer|Float|~string
}
```

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

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

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

```go
[S interface{ ~[]E }, E interface{}]
```

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

```go
[S ~[]E, E interface{}]
```

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

```go
[S ~[]E, E any]
```

## Type inference

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

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

var a, b, m float64

m = min[float64](a, b)
```

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

```go
m = min(a, b)
```

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

---

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

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

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

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

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

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

```go
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
}
```

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

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

- อีกแบบคือ method ที่มองเห็น slice ไม่ว่าจะ slice ของ type ไหนก็ตามก็ไม่ได้ต่างกันในแง่การทำงาน (ก็แบบเดียวกับข้อบนนี่หว่า)

```go
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})
}
```

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

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

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

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

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