--- tags: FP, SoftwareDesign, Golang --- # Golang FP調教日記 - 1 (生態篇) Golang是我從很久之前就開始使用的程式語言,那時候應該是我最喜歡的程式語言,直到有一天我發現了沒有Generics的問題,這時我就跟他分手了。2年的戀情,居然熬不過一個Generics!之後看他的缺點越來越多,使用率也下降了非常多,但golang 1.18來了,這個我等了差不多3年的東西他來了!Golang 1.18一出,一堆奇奇怪怪的FP庫都開始蹦出來,但是他們總是有奇奇怪怪的問題... ## Lo lo應該是目前最多star的go FP庫,雖然在awesome-go裡面他被列為utilities,但這不重要,我還是把他當成FP庫看待,自稱在一些方面會跟lodash有點像,基本上提供的函數是差不多,並且蠻全面的,也不僅限於列表,甚至提供了Parallel處理的一些函數,但他有一些奇怪的問題... ### 函數的函數參數太不彈性了 什麼意思,舉個例子,lo有一個函數叫Filter,很經典的一個列表一個函數的兩個參數的函數,但是函數參數的參數... ```go= arr := []int{1, 2, 3, 4} lo.Filter(arr, func(x int, index int) bool { return x % 2 == 0 }) ``` 一開始看下去沒什麼大問題,直到我自己使用才發現問題所在,就是強迫函數要有一個index這樣會造成不彈性的問題。 ```go= func LessThan(x int, y int) bool { return x < y } arr := []int{1, 2, 3, 4} lo.Filter(arr, lo.Partial(LessThan, 2)) ``` > `lo.Partial(LessThan, 2)`差不多等於直接寫`func (y int) bool { LessThan(2, y) }` 這個例子不能編譯,因為Filter的第二個參數型別為`func(T, int) bool`。我們可以看看Elixir語言是怎麼處理這個問題的。 ```elixir= [1, 2, 3, 4] |> Enum.filter(&(&1 > 2)) ``` 這是在Elixir我們一般會使用的寫法,但如果我們要Haskell風格的話 ```elixir= @spec add(integer) :: (integer -> integer) def add(x), do: &(x + &1) [1, 2, 3, 4] |> Enum.filter(add(2)) ``` 也是可以很方便的使用的,因為Elixir的filter的型別用Go的型別系統來寫應該會是`func(T) bool`。至於為什麼會需要這些東西?我會回答說,這樣的寫法會更簡短,減少了很多多餘的資訊,不是說直接寫一個函數就是不好,但是在一些簡單的情況,如果我們能拿掉那個函數,那絕對是最好的。我本來打算把他當在Go使用函數式的主力的,但遇到這個問題還是算了。 ## Mo 同一個作者開發的將熱門的monads帶入Go的庫,被FP-TS、Scala、Rust這些在FP圈子鼎鼎大名的東西啟發,本來這個想法不錯,雖然也有可能是語言的問題,但我還是要講一下... ### 語法是Ok,但無法回傳別的型別 ```go= option1 := mo.Some(42) // Some(42) option1. FlatMap(func (value int) Option[int] { return mo.Some(value * 2) }). FlatMap(func (value int) Option[int] { return mo.Some(value % 2) }). FlatMap(func (value int) Option[int] { return mo.Some(value + 21) }). OrElse(1234) ``` 這個是稍微調整過後的官方repo的例子,從這個例子當中可以看出一個問題,就是回傳值一定是`Option[int]`,因為Go generics的一個問題: ```go= type Foo[T any] struct {} func (foo Foo[T]) Bar[K any]() {} ``` 這個代碼是不能運行的(不理dead code的話),因為在Go,型別使用了Generics,那他的函數就不能夠使用Generics了,所以很多人會這樣做: ```go= type Foo[T any] struct {} func Bar[T any, K any](foo Foo[T]) {} ``` 這個在一開始看起來沒什麼太大問題,但一旦遇到了FP,事情就嚴重了,在Rust當中同樣的東西,到了Go寫法就必須變成這樣: ### Rust ```rust= some. do_first_thing(). do_second_thing(). last(); ``` ### Go ```go= Last(DoSecondThing(DoFirstThing(some))) ``` 你覺得能讀嗎?所以只能退一步... ```go= v1 := DoFirstThing(some) v2 := DoSecondThing(v1) result := Last(v2) ``` 不過也比剛剛的好一點 ### 反射反射還是反射 我到現在都還是不懂為什麼這麼多庫對於Option(Maybe)、Either等等這些monads的實現方式都是用類似這樣的方法: ```go= type Option[T any] struct { isPresent bool value T } ``` ```go= switch option.isPresent { case true: // ... case false: // ... } ``` 神奇的是這種實現方式多半在部落格和影片被提及,但在生態庫,前者才是最常見的情況,其實前者後者並沒有差多大。 ```go= switch v := option.(type) { case Some[T]: // ... case None[T]: // ... } ``` 但前者幾個致命的缺點是,用戶是有辦法碰到不安全的資料的而且會需要一直想辦法給沒有資料的參數zero value,而後者不需要填任何東西,所以我個人還是比較喜歡後者,即使後者的效能可能是比較慢的,畢竟需要轉型。 ## fp-go fp-go這次不只是提供了一些helpers函數還提供了2個monads,Option和Either。helper函數一共10個,他們是: - Every - Filter - Flat - FlatMap - Map - Reduce - Some - Compose - Pipe - Curry 比起lo我是比較喜歡fp-go的,因為他提供的函數都會currying。 ```go= func isPositive(x int) bool { return x > 0 } func main() { filterPositive := fp.Filter(isPositive) numbers := []int{1, 2, 3, 4, 5} positives := filterPositive(numbers) } ``` 這對於Go是非常棒的設計,但可惜的是目前他的函數太少了,雙手就能數完。 ### 沒有提供一些運算符的Helper 這雖然有點雞蛋裡面挑骨頭,不過對我來說,在Go有一些跟Ramda一樣可以Curry的運算符函數,還是挺重要的。 ### 奇怪的設計 雖然不怎麼妨礙我這個的使用,而且比起lo來說已經非常好了,但是我還是得說一下,為什麼要提供三個幾乎一模一樣的函數?他們會為每個函數提供2個幾乎一樣的函數。舉個例子Map就有Map、MapWithIndex、MapWithSlice。 ```go= fp.Map[int, string](func(x int) {...}) fp.MapWithIndex[int, string](func(x int, i int) {...}) fp.MapWithSlice[int, string](func(x int, i int, xs []int) { ... }) ``` 對,我們偶爾確實需要知道index和得知同一個slice長怎麼樣,但是我們可以提供別的函數來解決這個問題,例如學Elixir,我們有`Enum.with_index/1`,我們還有`Enum.zip/1`,所以只要提供一模一樣的東西就可以減少很多重複的函數了。 ### 終於發現了回傳資料型別的問題了 fp-go是我看下來這麼多,唯一一個懂得不使用struct函數的庫,這樣就能回傳另一個型別的東西了。像`option.Map`的定義就是... ```go= func Map[T, R any](fn func(value T) R) func(o Option[T]) Option[R] ``` 這樣就可以 ```go= package main import ( "fmt" "github.com/repeale/fp-go/option" "github.com/repeale/fp-go" ) func Lazy[T any](value T) fp.Lazy[T] { return func () T { return value } } func Add(x int) func(int) int { return func (y int) int { return x + y } } var PlusTwo = opt.Match(Lazy(0), Add(2)) func main() { x := opt.Some(10) fmt.Printf("%d\n", PlusTwo(x)) } ``` 看起來不錯,這個庫還有提供Compose和Pipe(區別在於Compose是從最後一個開始執行,Pipe是從第一個),然後我就用他們稍微寫了一段代碼: ```go= package main import ( "fmt" "github.com/repeale/fp-go" ) func Add(x int, y int) int { return x + y } func LessThan(x int, y int) bool { return x < y } var CurryAdd = fp.Curry2(Add) var CurryLessThan = fp.Curry2(LessThan) var CoolFunction = fp.Pipe3( fp.Filter(CurryLessThan(1)), fp.Map(CurryAdd(2)), fp.Reduce(Add, 0), ) /* or fp.Compose3( fp.Reduce(Add, 0), fp.Map(CurryAdd(2)), fp.Filter(CurryLessThan(1)), ) */ func main() { xs := []int{1, 2, 3} fmt.Printf("%v\n", CoolFunction(xs)) } ``` 完美,這就是我要的感覺,就決定是fp-go了!。 # 總結 還有很多庫還沒講,但我怕篇幅太長,涉及到的東西太多,所以我就先不要講這麼多,總之,調教是十分成功的,雖然沒到非常完美,但目前來看是可以用了。