Lo ไลบรารี่สไตล์ Lodash สำหรับภาษา Go และการสร้าง Utility Functions ด้วยตนเองผ่าน Generics
Developer ฝั่ง JavaScript มีไลบรารี่ช่วยชีวิตชื่อ Lodash ที่ช่วยย่นโค้ดยาวเฟื้อยให้เหลือสั้นลงด้วยพลานุภาพแห่งฟังก์ชันใน Lodash
สำหรับภาษา Go นั้นเราก็มีไลบรารี่ทำนองเดียวกันอยู่บ้าง เช่น Go Funk ทว่าไลบรารี่ดังกล่าวในขณะที่เขียนบทความนี้ยังไม่สนับสนุนการใช้งานกับ Generics อันเป็นฟีเจอร์ใหม่ใน Go 1.18
เราจะมารู้จักกับไลบรารี่ Lo อีกหนึ่งไลบรารี่ที่มีฟังก์ชันช่วยชีวิตคล้ายคลึงกับ Lodash แถมยังสนับสนุนการทำงานกับ Lodash อีกด้วย นอกเหนือจากจะนำเสนอวิธีการใช้งานฟังก์ชันของ Lo แล้ว เราจะลองใช้ภาษา Go เพื่อสร้างการทำงานที่ใกล้เคียงกับการทำงานของฟังก์ชันใน Lo กันด้วย
บทความนี้ผู้อ่านจำเป็นต้องทราบการใช้งาน Generics ใน Go ก่อน หากต้องการข้อมูลเพิ่มเติมสามารถอ่านได้จาก Go Generics คืออะไร? เรียนรู้การใช้งาน Generics ผ่าน Type Parameters
การติดตั้ง Lo
ใช้คำสั่งต่อไปนี้เพื่อติดตั้ง Lo ผ่าน terminal
1go get github.com/samber/lo
Lo นั้นประกอบด้วยฟังก์ชันมากมายให้เรียกใช้งาน สามารถดูรายการการใช้งานได้จาก ที่นี่ สำหรับบทความนี้เราจะนำเสนอเฉพาะบางฟังก์ชันเท่านั้นครับ
การเรียกใช้ฟังก์ชันจาก lo นั้น เริ่มต้นด้วยการ import แพคเกจนี้เข้ามาก่อนในชื่อของ lo
1import "github.com/samber/lo"
เมื่อใช้งานฟังก์ชันใด ๆ จาก lo จึงนำด้วยชื่อแพคเกจคือ lo ขึ้นก่อน เช่นเมื่อต้องการใช้ฟังก์ชัน Uniq จึงมีรูปแบบการใช้งานดังนี้
1import "github.com/samber/lo"23names := lo.Uniq([]string{"Somchai", "Somsree", "Somset", "Somchai"})
ฟังก์ชันของ lo สามารถแบ่งหมวดหมู่ได้หลายประเภท แต่ละกลุ่มฟังก์ชันจะจำเพาะกับการใช้งานผ่านชนิดข้อมูลที่แตกต่างกัน เช่น Map และ Uniq เป็นกลุ่มฟังก์ชันที่ใช้กับ Slice เป็นต้น
กลุ่มฟังก์ชันสำหรับใช้งานกับ Slice
ฟังก์ชันกลุ่มนี้ เช่น Filter, Map, FlatMap, Reduce, Times, Uniq เป็นต้น
การสร้างและใช้งาน Uniq
Uniq เป็นฟังก์ชันสำหรับกำจัดค่าซ้ำใน slice ด้วยการคืน slice ใหม่ที่ไม่มีอีลีเมนต์ใดซ้ำกันเลย
1// ผลลัพธ์: [Somchai Somsree Somset]2names := lo.Uniq([]string{"Somchai", "Somsree", "Somset", "Somchai"})
การพิจารณาว่าซ้ำหรือไม่ซ้ำนั้นนั่นคือการเปรียบเทียบอีลีเมนต์ในแต่ละช่อง แสดงว่า Slice นั้นสามารถกำหนดแต่ละอีลีเมนต์ให้เป็น comparable ได้ เมื่อทำการสร้าง Uniq ด้วยตนเองจึงได้โค้ดดังนี้
1func Uniq[T comparable](items []T) []T {2 result := make([]T, 0, len(items))3 occurrence := make(map[T]struct{}, len(items))45 for _, item := range items {6 if _, seen := occurrence[item]; !seen {7 result = append(result, item)8 occurrence[item] = struct{}{}9 }10 }1112 return result13}
การสร้างและใช้งาน GroupBy
GroupBy เป็นฟังก์ชันสำหรับการจัดหมวดหมู่ให้กับแต่ละอีลีเมนต์ภายใต้ Slice ตามเงื่อนไขที่กำหนด ผลลัพธ์ที่ได้จะเป็นชนิดข้อมูล map ที่มีค่าของ key เป็นค่าการจัดหมวดหมู่นั้น ๆ GroupBy นั้นจะมีการส่งฟังก์ชันที่ทำการคืนค่ากลับเป็นค่า key ของ map ผลลัพธ์ key ดังกล่าวจะใช้เพื่อการจัดกลุ่มให้กับข้อมูล
พิจารณาตัวอย่างต่อไปนี้ เราต้องการแปลง Slice ของ []string{"Mr. Somchai", "Mrs. Somsree", "Mr. Somset"}
ให้เป็นผลลัพธ์คือ
1map[string]string{2 "Mr": []string{"Mr. Somchai", "Mr. Somset"},3 "Mrs": []string{"Mrs. Somsree"}4}
นั่นแสดงว่าเราใช้คำนำหน้าชื่อเป็นตัวแบ่งกลุ่ม ฟังก์ชันสำหรับการจัดกลุ่มจึงต้องคืนค่า Mr หรือ Mrs เพื่อเป็นค่า key
รูปแบบของการเรียกใช้งาน GroupBy เป็นดังนี้
1names := []string{"Mr. Somchai", "Mrs. Somsree", "Mr. Somset"}2groups := lo.GroupBy(names, func(name string) string {3 return strings.Split(name, ". ")[0]4})
ต่อไปนี้เป็นการสร้าง GroupBy ด้วยตัวของเราเอง เนื่องจาก Slice ที่เป็นค่าเริ่มต้นจะประกอบด้วยอีลีเมนต์ที่มีชนิดข้อมูลเป็นอะไรก็ได้
เราจึงกำหนดให้ค่า input มีชนิดข้อมูลเป็น []T
เมื่อ T กำหนด constraint เป็น any
ส่วนค่า key นั้นเราต้องทำการเปรียบเทียบเพื่อจัดกลุ่มอีลีเมนต์ตามค่า key เราจึงกำหนด key ให้มีชนิดข้อมูลเป็น K
เมื่อ K มี constraint เป็น comparable
1func GroupBy[T any, K comparable](items []T, fn func(item T) K) map[K][]T {2 result := map[K][]T{}34 for _, item := range items {5 key := fn(item)6 result[key] = append(result[key], item)7 }89 return result10}
การสร้างและใช้งาน RangeFrom
ในสถานการณ์ที่เราต้องการสร้าง Slice ของตัวเลขแบบต่อเนื่อง เช่น []int{5, 6, 7, 8, 9}
เราสามารถใช้ฟังก์ชัน RangeFrom ด้วยการระบุสองอาร์กิวเมนต์ เมื่อตัวแรกคือค่าเริ่มต้น เช่น 5
ในขณะที่ตัวสุดท้ายเป็นค่าสิ้นสุดบวกด้วย 1 เช่น 10
1// []int{5, 6, 7, 8, 9}2// ผลลัพธ์ไม่รวม 103lo.RangeFrom(5, 10)
ค่าของสิ่งที่จะส่งเป็นอาร์กิวเมนต์ได้ต้องเป็นตัวเลขไม่ว่าจะเป็นตระกูล Interger หรือ Float
เราจึงกำหนดให้อาร์กิวเมนต์มีชนิดข้อมูลเป็น T เมื่อ T ถูกกำหนด constraint เป็น constraints.Integer | constraints.Float
และนี่คือโค้ดที่เราสร้างได้ด้วยตนเอง
1import "golang.org/x/exp/constraints"23func RangeFrom[T constraints.Integer | constraints.Float](from T, to T) []T {4 var result []T56 for i := from; i < to; i++ {7 result = append(result, i)8 }910 return result11}
การสร้างและใช้งาน Map
เมื่อเรามี slice ของข้อมูลชุดหนึ่ง บางครั้งเราอาจอยากแปลงข้อมูลแต่ละตัวใน slice เพื่อให้เกิดเป็นข้อมูลใหม่
โดยจำนวนสมาชิกใน slice ก่อนและหลังการดำเนินการต้องเท่ากัน ตัวอย่างเช่นต้องการแปลง []int{1, 2, 3, 4}
ให้เกิดเป็น slice ใหม่ที่นำ 2 ไปคูณสมาชิกทุกตัวก่อผลลัพธ์เป็น []int{2, 4, 6, 8}
ลักษณะเช่นนี้เราสามารถดำเนินการได้ผ่าน Map
1lo.Map([]int{1, 2, 3, 4}, func(item int, _ int) int {2 return item * 23})
สิ่งที่สำคัญของการเรียก Map คือการส่งอาร์กิวเมนต์สองค่าเมื่อค่าแรกคือ slice และค่าสุดท้ายคือฟังก์ชันเปลี่ยนค่า
ฟังก์ชันนี้จะรับพารามิเตอร์สองค่าโดยค่าแรกคือสมาชิกแต่ละตัวของ slice ที่จะถูกวนลูปส่งมาในแต่ละรอบ
ค่าที่สองของฟังก์ชันจะเป็นเลขลูปหรือลำดับสมาชิกโดยเริ่มจากศูนย์ จากชุดข้อมูล []int{1, 2, 3, 4}
รอบแรกของการเรียกฟังก์ชันจะทำตัวแปร item มีค่าเป็น 1 ในขณะที่ _
มีค่าเป็น 0
สิ่งใดก็ตามที่คืนจากฟังก์ชันสิ่งนั้นจะกลายเป็นผลลัพธ์ของสมาชิก slice ใหม่ เมื่อเราคูณ 2 ในทุกสมาชิกพร้อมคืนค่านี้จากฟังก์ชัน
ผลลัพธ์สุดท้ายจึงได้เป็น []int{2, 4, 6, 8}
ต่อไปนี้คือชุดคำสั่งที่เราสร้าง Map ด้วยตนเองผ่าน Generics
1func Map[T, R any](items []T, fn func(T, int) R) []R {2 result := make([]R, len(items))34 for i, item := range items {5 result[i] = fn(item, i)6 }78 return result9}
การสร้างและใช้งาน Filter
ในบางครั้งเราต้องการกรองเฉพาะข้อมูลที่สนใจใน slice เช่น กรองเอาเฉพาะตัวเลขที่เป็นลูกคู่จาก slice ของตัวเลข กรณีเช่นนี้เราใช้ slice
1// ผลลัพธ์คือ []int{2, 4}2lo.Filter([]int{1, 2, 3, 4}, func(x int, _ int) bool {3 return x%2 == 04})
Filter จะรับพารามิเตอร์ 2 ค่าเมื่อค่าแรกคือ slice ส่วนค่าที่สองคือฟังก์ชันที่ใช้ตรวจสอบเงื่อนไข หากผลลัพธ์ที่คืนจากฟังก์ชันนี้เป็น true สมาชิกของ slice จะไปปรากฎในผลลัพธ์ ในทางตรงข้ามเมื่อฟังก์ชันคืนค่า false สมาชิกที่วนลูปขณะนั้นจะไม่ปรากฎใน slice ผลลัพธ์
เราสามารถสร้าง Filter ด้วยตนเองผ่าน Generics ได้ ดังนี้
1func Filter[T any](items []T, fn func(item T, _ int) bool) []T {2 var result []T34 for i, item := range items {5 if fn(item, i) {6 result = append(result, item)7 }8 }910 return result11}
กลุ่มฟังก์ชันสำหรับใช้งานกับ Map
ฟังก์ชันในกลุ่มนี้เน้นจัดการกับข้อมูลประเภท map ได้แก่ฟังก์ชัน Keys, Values, Entries และ FromEntries เป็นต้น
การสร้างและใช้งาน Keys
Keys คือฟังก์ชันที่ใช้เพื่อดึงค่าของ keys ทั้งหมดของ Map กลับคืนมาเป็น slice
1// ผลลัพธ์คือ []string{"C++", "Java"}2lo.Keys(map[string]int{"C++": 3, "Java": 2})
จากตัวอย่างการใช้งานฟังก์ชัน Keys จะทำการรับพารามิเตอร์ที่มีชนิดข้อมูลแบบ Map เหตุเพราะค่า key ของ Map ต้องเป็นชนิดข้อมูลที่เปรียบเทียบค่าได้จึงต้องกำหนด key ด้วย constraint แบบ comparable
1func Keys[K comparable, V any](m map[K]V) []K {2 keys := make([]K, 0, len(m))34 for k := range m {5 keys = append(keys, k)6 }78 return keys9}
การสร้างและใช้งาน Values
ตรงกันข้ามกับ Keys ที่คืนค่าของ key ทั้งหมดใน Map ออกมา สำหรับ Values นั้นจะคืนค่าของ value ทั้งหมดใน Map เป็น Slice
1// ผลลัพธ์คือ []int{3, 2}2lo.Values(map[string]int{"C++": 3, "Java": 2})
วิธีการสร้างฟังก์ชัน Values แทบไม่แตกต่างจาก Keys เท่าไร หากแต่เลือกคืนค่า value แทนที่จะเป็นค่า key
1func Values[K comparable, V any](m map[K]V) []V {2 values := make([]V, 0, len(m))34 for _, v := range m {5 values = append(values, v)6 }78 return values9}
กลุ่มฟังก์ชัน Intersection Helpers
ตัวอย่างของฟังก์ชันในกลุ่มนี้ เช่น Contains และ Every เป็นต้น
การสร้างและใช้งาน Contains
Contains เป็นฟังก์ชันสำหรับการตรวจสอบว่าค่าข้อมูลที่เราสนใจนั้นปรากฎใน Slice ที่เราระบุหรือไม่ หากค้นหาพบฟังก์ชันนี้จะคืน true และคืน false เมื่อการค้นหานั้นไม่พบสิ่งที่ต้องการ
1// 5 ปรากฏใน Slice ผลลัพธ์จึงเป็น true2Contains([]int{1, 2, 3, 4, 5}, 3)
ฟังก์ชันนี้ต้องอาศัยการเปรียบเทียบค่าที่เราสนใจกับแต่ละอีลีเมนต์ใน Slice เมื่อเป็นเช่นนี้ Type Parameter จึงต้องกำหนด constraint เป็น comparable
1func Contains[T comparable](items []T, element T) bool {2 for _, item := range items {3 if item == element {4 return true5 }6 }78 return false9}
การสร้างและใช้งาน Every
Every เป็นฟังก์ชันสำหรับการตรวจสอบว่า Slice ที่เรากำหนดเป็นส่วนหนึ่งของอีก Slice หรือไม่ ผลลัพธ์จากการทำงานของฟังก์ชันจะเป็น bool
1// เนื่องจากค่า 0 และ 2 ปรากฏทั้งสองค่าใน Slice ของอาร์กิวเมนต์แรก2// ผลลัพธ์จากการทำงานจึงคืน true3lo.Every([]int{0, 1, 2, 3, 4, 5}, []int{0, 2})
วิธีการสร้างฟังก์ชันนี้อาศัยการทำงานของ Contains อีกรอบ เพื่อตรวจสอบว่าแต่ละค่าใน subset ต้องปรากฏใน Slice หลักทุกค่า
1func Every[T comparable](items []T, subset []T) bool {2 for _, item := range subset {3 if !Contains(items, item) {4 return false5 }6 }78 return true9}
การสร้างและใช้งาน Some
การทำงานของ Some จะคล้ายกับฟังก์ชัน Every ต่างกันตรงที่ว่าฟังก์ชัน Some นั้นหากใน subset ปรากฏใน Slice หลักเพียงบางค่า การทำงานก็จะคืนผลลัพธ์เป็น true แล้ว
1// แม้ 9 จะไม่เป็นส่วนหนึ่งของมันแต่ 0 ปรากฏใน Slice หน้า2// แบบนี้ก็จะถือว่า Slice หลังมีบางค่าตรงกับใน Slice หน้าแล้ว3// การทำงานจึงคืนค่าเป็น true4Some([]int{0, 1, 2, 3, 4, 5}, []int{0, 9})
กลุ่มฟังก์ชันสำหรับการค้นหา
ฟังก์ชันในกลุ่มนี้เน้นไปเพื่อการค้นหาค่าข้อมูล เช่น Find, IndexOf และ Sample เป็นต้น
การสร้างและใช้งาน Find
สำหรับการค้นหาข้อมูลใน Slice พร้อมคืนค่าแรกที่ค้นพบนั้นเป็นหน้าที่ของฟังก์ชัน Find Find เป็นฟังก์ชันค้นหาที่คืนค่ากลับสองค่า ค่าแรกคือข้อมูลแรกที่ค้นเจอส่วนค่าที่สองคือสถานะที่บอกว่าพบหรือไม่พบค่านี้ โดยชนิดข้อมูลของมันคือ bool
การเรียกใช้งาน Find ต้องทำการส่งอาร์กิวเมนต์สองค่า ค่าแรกคือ Slice ที่ต้องการค้นหาค่า ส่วนค่าที่สองคือฟังก์ชันค้นหาที่มีเงื่อนไขระบุว่าการค้นหานั้นเป็นไปตามกฎเกณฑ์ใด โดยผลลัพธ์ที่คืนจากฟังก์ชันต้องเป็น bool
1// ค้นหาเลขคู่ตัวแรกใน Slice ผลลัพธ์ที่ได้คือ 2, true2item, ok := lo.Find([]int{1, 2, 3, 4, 5}, func(i int) bool {3 return i%2 == 04})
วิธีการสร้างฟังก์ชัน Find จะใช้การตรวจสอบค่าด้วยการส่งแต่ละอีลีเมนต์ใน Slice ไปทดสอบกับฟังก์ชัน ถ้าฟังก์ชันคืนค่าเป็น true แสดงว่าเราค้นหาสิ่งที่ต้องการพบแล้วจึงทำการคืนค่านั้นกลับพร้อมค่าสถานะเป็น true
1func Find[T comparable](items []T, fn func(T) bool) (T, bool) {2 for _, item := range items {3 if fn(item) {4 return item, true5 }6 }78 var result T9 return result, false10}
การสร้างและใช้งาน Sample
Sample เป็นฟังก์ชันสำหรับการสุ่มค่าจาก Slice ที่กำหนด
1// อาจสุ่มได้ค่า 1 หรือ 2 หรือ 32lo.Sample([]int{1, 2, 3})
วิธีการสร้างฟังก์ชันนี้จะใช้ rand.Intn
เพื่อทำการสุ่มค่า
1func Sample[T any](items []T) T {2 rand.Seed(time.Now().UnixNano())3 index := rand.Intn(len(items))45 return items[index]6}
สรุป
lo ประกอบด้วยฟังก์ชันที่ช่วยให้การเขียนโค้ดง่ายขึ้น อาศัยคุณสมบัติของ Generics ในภาษา Go จึงสนับสนุนให้ lo ทำงานกับชนิดข้อมูลแบบต่าง ๆ ได้อย่างมีประสิทธิภาพ สำหรับการใช้งาน lo เพิ่มเติม ผู้อ่านสามารถดูรายละเอียดได้จาก ที่นี่ครับ
สารบัญ
- การติดตั้ง Lo
- กลุ่มฟังก์ชันสำหรับใช้งานกับ Slice
- กลุ่มฟังก์ชันสำหรับใช้งานกับ Map
- กลุ่มฟังก์ชัน Intersection Helpers
- กลุ่มฟังก์ชันสำหรับการค้นหา
- สรุป