iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
1
Software Development

Functional Programming in Kotlin系列 第 11

Introduce Functor

在前面的很多篇中介紹了許許多多不同的“容器”,那這些“容器”對於 Category theory 又有什麼樣的意義?今天就來介紹其中一個:Functor。

Functor definition

在前一篇的文章中介紹了。 Category ,是由一些 Object 與 morphism 組合而成的,那麼,現在有多個 Category 的情況下,要怎麼描述他們之間的關係呢?請看下圖:

https://user-images.githubusercontent.com/7949400/93671599-df07d800-fad6-11ea-81f8-631ea305822b.png

左邊的 Category 中有 a 跟 b 兩個 object,相對應的,右邊的 Category 中有 a' 跟 b' 兩個 object。左邊的 a, b 之間有一個 morphism (function) f,可以把它想像成這是一個 Int 到 String 的函式。那麼相對應的,右邊的 a' 與 b' 也有一個 morphism f'。那麼我們就可以在中間連接幾條線,第一條是 a 到 a' ,第二條是 b 到 b',這邊可以想像成 a 到 a' 是一個 Int 到 Int? 的函式,b 到 b' 是一個 String 到 String? 的函式,這時候你就會發現,對於任何一個在左邊的 object(type),都會有一個 function 可以對應到右邊的 object',並且也有相對應的 morphism f'。而這個對應關係就是 Functor。

Functor 的其中一個特性是保持結構的完整,如果在原本的 Category 中只有一個 object,經過 Functor 的轉換之後,不可能會出現兩個以上的 object,也就是一對一的對應關係,不會讓結構扭曲,擴大,或縮小。

現在給 Functor 一個符號 F 來代表他,下面舉幾個簡單的例子:

val a: Int = 3
val Fa: Int? = 3
val b: String = "3"
val Fb: String? = "3"
val f: (Int) -> String = { it.toString }
val Ff: (Int?) -> String? = { it?.toString }

Identity and Associativity

Functor 也應該符合單位元與結合律這兩個特性,對於原本的 Category 來說是 Identity function 的話,經過 functor 的轉換,在另一個 Category 也應該要是 identity function ,不然這就不會是個一對一的對應關係了,而是有可能多對一。另一方面,結合律也會維持不變,不會到另一個 Category 時,結合律就消失了。

https://user-images.githubusercontent.com/7949400/93671604-e7f8a980-fad6-11ea-9aae-dbf5af161f56.png

那 Functor 如何對應到程式呢?其實 map 就是上述中的 morphism - f',只是用另外一個形式來表示罷了。 還記得前幾篇對於 map 的疑問嗎?一個是連續兩個 map,另一個是一個 map 然後講function 組合起來,這兩件事會等價嗎?看看下面這張圖吧!

https://user-images.githubusercontent.com/7949400/93671608-ee872100-fad6-11ea-9dbd-dcde9fe634fb.png

除了 a,b 之外,現在多了一個 object c,如此一來,就可以得到另一個 morphism g 與 g',也會有 g 跟 f 的組合:h = g。f,所以相對應的呢,也會有 h' 在右邊的這個 Category 裡,而這不就是兩個 function 的組合了嗎?所以在這邊得到第一個式子,h' = F(g。f),也就是 在 kotlin 裡的 map ( g compose f) 。再來看另外一件事,右邊的 Category 中 g' 與 f' 也組合成了 h' = g' 。f',也就是 Kotlin 中的 map (f).map(g) [在 map 中 compose 是反過來的,還記得 pipe 嗎?]。最後就得到這個結論了 :h' = F(g。f) = g' 。f'。

所以只要是 Functor,就一定會符合單位元與結合律,也同時因為這樣而在數學上保證了一對一的對應關係,所以可以安全的使用 map這個 operator。map 也同時符合結合律與 referential transparency,可以放心的使用它,保證不會有“意外”發生。藉由使用數學這套工具,就可以放心的將我們的商業邏輯交給Functor 處理。

Maybe Functor

讓我們介紹一個新的類別:Maybe,這個類別一樣是個容器,裡面裝的內容是一個值(Some),或是完全沒東西(Nothing),其實就跟 ? 很像,下面是該類別的實作:

sealed class Maybe<T>{
    class Some<T>(val value: T): Maybe<T>()
    class None<T> : Maybe<T>()

    fun <R> map(transform: (T) -> R): Maybe<R> {
        return when(this) {
            is Some -> Some(transform(value))
            is None -> None()
        }
    }
    
    companion object {
        fun <T> just(value: T?): Maybe<T> {
            return if (value == null) {
                None()
            } else {
                Some(value)
            }
        }
    }
}

現在來看看 Maybe 要怎麼使用:

fun foo() {
    val a: String? = null
    val b: String? = "not null"

    // None
    val maybeA = Maybe.just(a)
    // Some("not null")
    val maybeB = Maybe.just(b)
}

其實 just 就對應到了最一開始講的 a → a' 與 b → b',現在得到了 a' (maybeA) 與 b' (maybeB) 了。

    // None
    val textLengthA = maybeA.map { it.length }
    // Some(8)
    val textLengthB = maybeB.map { it.length }

使用 map 的時候,即使原本的內容是 None ,也不會因此而被 compilier 禁止操作,如此一來,也不用多一層的 if - else 來處理 null 的情況,不管中間有幾個 map ,null 的情況留到最後再處理即可。

請注意,在使用 map 的時候,還是有可能因為操作不當而產生“意外”,而這個操作不當就是 - side effect,前面花了很多篇幅所寫的 pure function, immutable, no side effect ,請務必好好遵守,才能讓 Functor 好好發揮出他應有的價值。

來舉個我們會實際碰到的例子吧!在 Android 中,要拿到另外一個 Activity 傳來的資料,只能藉由 Android framework 規定的方式拿,然而在取值的過程中很容易會遇到空值的情況!

val extras = intent.extras

if (extras != null) {
   val user = extras.getParceable("User") as User?
   if (user != null) {
      val address = user.address
      if (address != null) {
          // 沒完沒了啊~~
      }
   }
}

那如果用 Maybe 會是怎樣的情況呢?

val address = Maybe.just(intent.extras)
      .mapNullable { extras.getParceable("User") as User? }
      .mapNullable { it.address }

address.fold(somefun = {...}, nonefun = {...})

看起來是不是簡單多了呢?這裡多了一個新的 operator - mapNullable ,跟 map 不一樣的是,mapNullable 可以接收 nullable 的函式回傳值,以下是他的實作:

fun <R> mapNullable(transform: (T) -> R?): Maybe<R> {
    return when(this) {
        is Some -> {
            val result = transform(value)
            if (result == null) {
                None()
            } else {
                Some(result)
            }
        }
        is None -> None()
    }
}

小結

今天介紹了一個新的"容器" - Maybe ,可以表示有值“或”空值,這種“或”的關係,其實背後也是可以轉換為數學的,也就是下一篇會提到的 Algebra Data Type。那這個容器呢,在某些語言或平台被稱作 Option ,像是 Java 8 就提供了 Option,其實是相同的概念。

從 Functor 對於數學上的意義也可以看出,我們可以放心的使用 Functor,因為 Category 之間函式的組合(Identity and Associativity)已經被數學證明過是安全的,這些也是 functional programmer 們應該要在意的事情:寫出安全的程式碼。


上一篇
Category theory
下一篇
Algebraic Data Type
系列文
Functional Programming in Kotlin30

尚未有邦友留言

立即登入留言