สรุปจาก Robert Griesemer & Ian Lance Taylorเรื่อง ジェネリックในงาน Gophercon 2021
ดูได้จาก
3สิ่งหลักๆเกี่ยวกับ ジェネリックที่ ロバートอยากพูดถึงก็คือ型パラメーターสำหรับ 関数และ 種別 กำหนด 種類ลงไปใน インターフェースได้ 型推論กับ ジェネリック
หน้าตาแบบนี้
และเวลาเอาไปเรียกใช้ก็ทำแบบนี้
และเราก็สามารถทำแบบนี้ได้ด้วย
ต่อมา ลองมาดูการทำ パラメータ型กับ カスタムタイプดูบ้างดีกว่า
ขอพักเรื่องนี้มาคุยเรื่อง 制約กันอีกสักหน่อย
โดยปกติแล้ว 制約ใน 試みจะเป็น インターフェースซึ่งแต่เดิมมันใช้ 定義メソッドเพียงอย่างเดียว แต่ตอนนี้มันสามารถใส่ 種類ลงไปได้ด้วย หน้าตาประมาณนี้
สิ่งที่แปลกตาในนี้คือ ~文字列นี่มีความหมายว่า 種類ใดก็ตามที่มี 基礎เป็น 文字列
และเราสามารถทำ イン・ラインแบบนี้ก็ได้
เราจะเอาโค้ดก่อนหน้านี้มาดูอีกสักครั้ง
ในส่วนของ 庵ก็มาอธิบายอีก ユースケースหนึ่งของการเขียน ジェネリックผมขอละไว้ก่อน แต่ส่วนที่น่าสนใจคือ 庵พูดถึงเรื่องที่ว่า แล้วเมื่อไรที่เราควรใช้ ジェネリックซึ่ง 庵บอกว่านี่เป็นแค่คำแนะนำเท่านั้นนะ ไม่ได้เป็นกฎระเบียบแต่อย่างใด
อย่าไปติดหล่มด้วยการออกแบบ ジェネリックก่อน แต่ให้เริ่มจากเขียนโค้ดให้มันทำงานได้ แล้วค่อยใส่ ジェネリックไปทีหลังก็ไม่สาย
機能ที่ทำงานกับ スライス、マップและ チャンネルโดยไม่สนใจ 種類ข้างใน งานแบบนี้ถ้าเอา ジェネリックมาใช้ก็น่าจะเป็นประโยชน์ データ構造สำหรับงานสารพัดประโยชน์ ยกตัวอย่างเช่น
และคำแนะคำเมื่อต้องทำ 操作ใดก็ตามกับ 型パラメータแนะนำให้ทำเป็น 機能แทนที่จะทำเป็น 方法อีกแบบคือ 方法ที่มองเห็น スライスไม่ว่าจะ スライスของ 種類ไหนก็ตามก็ไม่ได้ต่างกันในแง่การทำงาน (ก็แบบเดียวกับข้อบนนี่หว่า)
ก็เช่นถ้าต้องการแค่เรียก 方法ของ 種類นั้น ก็ใช้ インターフェースแบบเดิมไป หรือต้องการเรียก 方法ชื่อเดียวกันจาก 種類ที่ต่างกัน ก็ยังใช้ インターフェースต่อไปแหล่ะ อีกกรณีคือ ถ้าต้องการทำ 操作ที่ไม่สามารถทำได้กับทุก 種類ได้เหมือนๆกัน แบบนี้ก็ไม่ควรพยายามเอา ジェネリックมาใช้นะ สุดท้าย 庵ให้ข้อคิดว่า อย่าพยายามคิดจะสร้างต้นแบบ อย่าใช้ 型パラメータก่อนเวลาอันควร ให้รอจนกว่าจะแน่ใจว่า เรากำลังเขียนโค้ดเดิมซ้ำๆ นั่นถึงจะเป็นเวลาที่เหมาะสม
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 ให้ถูกที่ถูกเวลา ยิ่งสำคัญกว่า
Reference
この問題について(สรุปจาก Robert Griesemer & Ian Lance Taylorเรื่อง ジェネリックในงาน Gophercon 2021), 我々は、より多くの情報をここで見つけました https://dev.to/pallat/srupcchaak-robert-griesemer-ian-lance-taylor-eruueng-generics-ainngaan-gophercon-2021-13n7テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol