ดูได้จาก YouTube
3 สิ่งหลักๆเกี่ยวกับ generics ที่ Robert อยากพูดถึงก็คือ
- Type Parameter สำหรับ functions และ types
- กำหนด type ลงไปใน interface ได้
- Type inference กับ generics
เริ่มจากเรื่อง Type Parameter
หน้าตาแบบนี้
[P, Q constraint#1, R constraint#2]
คือการที่สามารถใส่ type เป็น parameter ให้กับ function ได้ตรงๆ แบบเดียวกับที่ใส่ค่าลงไปใน function นั่นแหล่ะ เพียงแค่ต้องใส่ในวงเล็บก้ามปู แทนวงเล็บปกติ มาดูตัวอย่างฟังก์ชัน min กัน
func min(x, y float64) float64 {
if x < y {
return x
}
return y
}
ฟังก์ชันนี้ทำงานกับ float64 เท่านั้น โดยเราสามารถเปลี่ยนให้มันทำงานกับ type ตัวเลขอื่นได้ด้วยการทำให้มันเป็น generics function แบบนี้แทน
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 ใดๆที่สามารถเอาค่ามาเปรียบเทียบด้วยเครื่องหมาย <
ได้
และเวลาเอาไปเรียกใช้ก็ทำแบบนี้
m := min[int](2, 3)
การที่เรากำหนด type ด้วยวิธีการนี้ [int]
เราเรียกว่า instantiation เรารู้แค่วิธีใช้ก็พอไม่ต้องลงลึกมากเนอะ
และเราก็สามารถทำแบบนี้ได้ด้วย
fmin := min[float64]
m := fmin(2.71, 3.14)
ก็คือสร้าง 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]
นี่คือ generic type
Type constraint ก็คือ interface
ขอพักเรื่องนี้มาคุยเรื่อง constraint กันอีกสักหน่อย
โดยปกติแล้ว constraint ใน Go จะเป็น interface ซึ่งแต่เดิมมันใช้ define method เพียงอย่างเดียว แต่ตอนนี้มันสามารถใส่ type ลงไปได้ด้วย หน้าตาประมาณนี้
interface {
int|string|bool
}
มาดูของที่เราใช้จาก constraints กันดีกว่า
package constraints
type Ordered interface {
Integer|Float|~string
}
ความหมายของมันคือ มันคือ constraint ที่สามารถเป็น ทุก type ของ integer ของ floating-point และ string และไม่มี method ใดๆ
สิ่งที่แปลกตาในนี้คือ ~string นี่มีความหมายว่า type ใดก็ตามที่มี underlying เป็น string
และเราสามารถทำ in line แบบนี้ก็ได้
[S interface{ ~[]E }, E interface{}]
และพอเห็นแบบนี้ก็ยังสามารถย่อเหลือแค่นี้ได้อีก
[S ~[]E, E interface{}]
และเนื่องจากเราก็จะเห็น empty interface{} อยู่บ่อยมาก ก็เลยมี alias type ของมันเกิดขึ้นมาใหม่เพื่อให้โค้ดดูสั้นลงแบบนี้
[S ~[]E, E any]
Type inference
เราจะเอาโค้ดก่อนหน้านี้มาดูอีกสักครั้ง
func min[T constraints.Ordered](x, y T) T
var a, b, m float64
m = min[float64](a, b)
เนื่องจากเราประกาศ type ของ argument ไว้ก่อนแล้วแบบนี้ จะทำให้ compiler สามารถเดา type ที่แท้จริงของ T ได้เอง ทำให้โค้ดสามารถเขียนแค่นี้ได้
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 สำหรับงานสารพัดประโยชน์
ยกตัวอย่างเช่น
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 ไหนก็ตามก็ไม่ได้ต่างกันในแง่การทำงาน (ก็แบบเดียวกับข้อบนนี่หว่า)
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 ให้ถูกที่ถูกเวลา ยิ่งสำคัญกว่า
Top comments (0)