vlambda博客
学习文章列表

高阶函数式编程:在 Kotlin 中“实现”函子(Functor)

开篇



先讲个笑话,我上周才真正知道什么是函子(functor)。



在开始之前先推荐一篇文章:。文章是 Kotlin 中文站站长贾哥(灰蓝天际)写的,本文中的许多论点都基于这篇文章,甚至可以算是这篇文章的延伸讨论。后文统称这篇文章为:。



什么是函子?


到底什么是函子?函子(functor)是函数式编程中的一个高阶概念,也是一个核心概念,是函数式编程中函子(functor)-> 应用函子(applicative)-> 单子(monod)概念体系中的第一环。本文使用三言两语很难彻底解释清楚什么是函子,因此后文只会简介,如果想了解,可以参阅《Haskell 趣学指南》第七章的 7.10 小节、第十一章的 11.1、11.2 两小节以及《魔力 Haskell》的第十一章。


函子首先是一个类型类(type class)。那什么是类型类?按照参照文的说法,类型类应该具备以下三项能力:


1. 定义“接口”并可提供默认实现

2. 用作泛型约束

3. 给既有类型增加功能


在 Kotlin 中,接口(interface)同时具备前两项能力,但是不具备第三项能力。因为我们没法让一个已存在的类实现某个接口(以不能修改类本身为前提)。


所以参照文中使用接口 + 扩展函数的方式,分别以三种变体在 Kotlin 中表达了类型类,可以基本达到类型类的三能力要求,具体细节请参见参照文。


说回函子,函子除了是类型类以外,还是代表一种能装有单一任意类型元素的“容器”。用 Kotlin 来举例的话,假设存在泛型参数 T、S,那么 List<T>、Set<T> 都是装有任意类型元素的“容器”,而 Map<T, S> 则不是,因为 Map<T, S> 中有两个未知的泛型(类型)参数。


当然函子的核心是 Functor 类型类的实例必须实现 famp 函数。包含 fmap 函数声明的 Functor 类型类在 Haskell 中的定义如下:


class Functor f where    fmap :: (a -> b) -> f a -> f b


fmap 首先可以被认为是:


含义 1:fmap 表示一个接收一个 a -> b 类型的函数作为参数,以及一个装有 a 类型元素(a 可以看作是一个泛型参数的函子实例(记得吗?函子表示一种装有单一任意类型数据的“容器”),并返回一个装有 b 类型元素的函子实例的元素。


其次由于柯里化(这里不具体解释了,感兴趣的可以查一下)的存在,Haskell 中的函数可以部分应用,即只传入函数参数列表前面的参数,所以 fmap 也被认为是:


含义 2:fmap 也可以表示一种接收 a -> b 类型的函数作为参数,返回一个 f a -> f b 类型函数作为返回值的函数。也就是说,它的作用是将 a -> b 类型的函数升格成为 f a -> f b 类型的函数。


参照文中的实现


在参照文中已经给出了一种函子在 Kotlin 中的实现(也是 Arrow 库中的实现方式)。由于 Kotlin 暂不支持高阶类型,所以通过定义一个 Kind 接口来辅助实现,如下所示:


interface Kind<out F, out A>
interface Functor<F> { fun <T, R> Kind<F, T>.fmap(f: (T) -> R): Kind<F, R>}


当然,参照文中也提到了一个缺憾,一个类如果要成为函子,就必须实现 Kind 接口,这几乎直接导致了我们的函子失去了类型类三能力中的第三条:给任意既有类型增加功能。当然,受限于 Kotlin 本身语法在函数式编程方面的支持程度,我也并没有找到一种新的实现方式来使 Functor 能做到这一点。不过,既然我们都只能让我们自己定义的类型成为函子,我们能否消除掉 Kind 接口,直接让实现类实现 Functor 接口?答案是也是可以的,只不过需要一点复杂的泛型编程。


初步实现


首先定义 Functor 接口,它需要一个泛型参数 T 来表示“容器”内部装载的元素类型:


interface Functor<T>


在声明 fmap 函数前,我们先来讨论几个事实。在 Kotlin 中决定函数类型的三个因素是:接收者的类型(成员函数、扩展函数)、参数的数量和类型、返回值的类型。但是函数接收者是面向对象语言中独有的概念,我们必须知道的是,对于函数来说,它本质是一个参数,这也是为什么 Kotlin 会将扩展函数编译为 Java 的静态方法,而扩展函数的接收者将作为静态方法的一个参数。


但对于成员函数来说,情况有所不同,因为接收者是一个特殊的参数,在成员函数中能访问接收者的私有成员(这对面向对象程序员来说是废话)。


回到函子的实现上。我们调用 fmap 的函数的时候,一定是用 Functor 接口的实现者去掉用的,也就是说具体的函子本身就已经是 fmap 函数的参数之一了,调用的时候大概是这个样子:


val functorInt: Functor<Int> = ......val functorChar: Functor<Char> = functorInt.fmap(functionIntToChar)


当然,上面的代码只是表达了 fmap 函数的含义 1;如果要表达含义 2,我们本身是不需要这个接收者参数的,但是没有办法,我们需要接收者来实现多态,所以我们在函数中不使用这个接收者来作为参数就好了(虽然给了,但是不用)。


现在 Functor 接口的定义差不多该如下所示:


interface Functor<T{    // 先别着急,这里的 R 和 S 还未定义 fun <A> fmap1(function: (T) -> A): (R) -> S fun <A> fmap2(function: (T) -> A): S}


很明显,现在还缺少的是表示当前 Functor 接口实现者类型的 R,它装有 T 类型元素;以及与 R 同属一个类(注意,不是类型)的类型 S,但它装有 A 类型的元素。且 R 和 S 的上界必须是 Functor。


如果要在接口中让接口本身知道自己的实现者是谁,那就需要使用到自限定的泛型(或者叫泛型的古怪循环):


interface Functor<T, R : Functor<T, R>> { fun <A> fmap1(function: (T) -> A): (R) -> S fun <A> fmap2(function: (T) -> A): S}


对于熟练使用面向对象语言进行泛型编程的人来说,自限定的泛型并不是什么神秘的新奇玩意。但现在的最关键问题在于类型 S 应该如何定义。


按照我们正常的思路,S 类型表示的“容器”内部装载的是 A 类型的元素,所以S 应该是属于 fmap 函数的泛型参数。于是我们可以得到这样的完整定义:


interface Functor<T, R : Functor1<T, R>> {    fun <A, S : Functor<A, S>fmap1(function: (T) -> A): (R) -> S    fun <A, S : Functor<A, S>fmap2(function: (T) -> A): S}


看起来完美无缺。我们现在来实现一个具体的函子,我们就挑 List 吧:


class FunctorList<T>(private val coreList: List<T>) : List<T> by coreList, Functor1<T, FunctorList<T>> {
    override fun <A, S : Functor1<A, S>fmap1(function: (T) -> A): (FunctorList<T>) -> S = { FunctorList(it.map(function)) as S } override fun <A, S : Functor1<A, S>> fmap2(function: (T) -> A): S = FunctorList(map(function)) as S }


我们已经尽可能的做到简单,在 Haskell 中 List 的 fmap 函数和 map 函数是等价的,所以我们充分利用这一点。我们实现了 List 接口,并且采用类委托把抽象函数的实现委托给参数 coreList。两个 fmap 函数的核心实现都是 map 函数本身。


这个实现有一个缺点,我们如何保证 R 与 S 两个类型所属同一个类?目前来看,这完全取决于程序员。上面提到了,Kotlin 不支持高阶类型,这里我们也发现,接口的实现者在实现接口的抽象函数时也没法给接口泛型抽象函数的泛型加更多限制。所以这就导致了编译器无法认定 R 与 S 同属于一个类。如果要确保这一点,这非常依赖于程序员在每次调用 fmap 函数的时候都传入正确的 S 类型的参数。大概类似于这样:


FunctorList(listOf('a', 'b', 'c')).fmap2<Int, FunctorList<Int>> { it.toInt() }


现在假设我们有一个和 FunctorList 如出一辙的 FunctorSet 类,它也是一个函子,但是和 FunctorList 没有任何子类型化关系,于是我们这样传递泛型参数:


FunctorList(listOf('a''b''c')).fmap2<Int, FunctorSet<Int>> { it.toInt() }


狗血的事情就发生了,编译器不会给出如何警告或报错,代码会正常编译,但是只要程序运行到这里就会因为 fmap 内部实现的强制类型转换失败而 crash。


改进版本


现在目标明确,我们要在具体的函子类定义的时候,直接确定 fmap 函数中 S 的类型,这样我们只需要在具体函子类定义的时候正确传入泛型参数一次,就可以让所有使用该具体函子类的用户在调用 fmap 时获得编译器正确的静态类型检查,因此,我们需要把 S 作为 Functor 接口的泛型参数,而不是 fmap 函数的泛型参数:


interface Functor<T, R : Functor<T, R, S>, S : Functor<*, *, S>> { fun <A> fmap1(function: (T) -> A): (R) -> S fun <A> fmap2(function: (T) -> A): S}


同样,S 也使用了类似的自限定泛型。但由于在接口定义的时候还不知道 S 函子内部装载的元素类型,所以这里只能用 Kotlin 泛型的星号投影,再接下来,S 的第二个泛型参数我们实际上也不关心它是什么,所以也使用星号投影,最后一个参数传入 S 来表示自限定。我们现在看看 FunctorList 该怎么实现:


class FunctorList<T>(private val coreList: List<T>) : List<T> by coreList, Functor<T, FunctorList<T>, FunctorList<*>> { override fun <A> fmap1(function: (T) -> A): (FunctorList<T>) -> FunctorList<A> = { FunctorList(it.map(function)) } override fun <A> fmap2(function: (T) -> A): FunctorList<A> = FunctorList(map(function))}


看,我们在实现 fmap 函数的时候已经可以直接确定 S 的类型就是 FunctorList<A>。由于 S 类型用于只出现在 out 位置,再加上接口定义的时候使用了星号投影,所以无论 S 的类型是 FunctorList 内装什么,都是类型安全的(我期待得到一个可以装有任意类型元素的 FunctorList,而你给了我一个装有 A 类型元素的 FunctorList,由于 A 必定属于任意类型,所以类型安全)。


验证函子定律


一个类型并不是仅仅是 Functor 类型类的实例并实现了 fmap 函数就可以叫做函子,它还必须满足以下两个函子定律。


定律 1:如果我们在函子值上映射 id 函数,那么返回的函子值应该和原先的值一样。


id 函数就是接收任意值作为参数,并将该值原封不动返回的函数,在 Kotlin 中应该这样表达:


fun <T> id(t: T): T = t


我们来验证 FunctorList 是否满足定律 1:


fun main() {    // 验证定律 1: val id: (Char) -> Char = ::id val functorListChar = FunctorList(listOf('a', 'b', 'c')) val functorListChar2 = functorListChar.fmap2(id) println(functorListChar == functorListChar2) // 输出:true}
fun <T> id(t: T): T = t
class FunctorList<T>(private val coreList: List<T>) : List<T> by coreList, Functor<T, FunctorList<T>, FunctorList<*>> { override fun <A> fmap1(function: (T) -> A): (FunctorList<T>) -> FunctorList<A> = { FunctorList(it.map(function)) } override fun <A> fmap2(function: (T) -> A): FunctorList<A> = FunctorList(map(function))
override fun hashCode(): Int = coreList.hashCode() override fun equals(other: Any?): Boolean = coreList == other}


输出 true,所以这个简单的验证就通过了。


定律 2:如果把两个函数组合起来,然后映射在一个函子上,结果和用一个函数映射函子,在结果上再用另一个函数映射的结果相同。


写出来大概是:fmap (f . g) = fmap f. fmap g。当然程序运行和编译器都没法验证两个函数是否相等,所以我们也可以写成 fmap (f . g) x = fmap f (fmap g x)。因此我们依旧使用 fmap2 来验证。


为了实现函数组合,我们先写个组合用的工具函数吧,类似于 Haskell 中的 . 函数:


operator fun <A, B, C> ((B) -> C).plus(param: (A) -> B): (A) -> C =  { x -> this(param(x)) }


然后再写俩函数,用来表示上面的 (B) -> C 和  (A) -> B:


fun intToString(i: Int): String = i.toString()fun charToInt(c: Char): Int = c.toInt()


验证开始:


fun main() { // 验证定律 2: val complex = ::intToString + ::charToInt val functorListChar = FunctorList(listOf('a', 'b', 'c')) val stringList1 = functorListChar.fmap2(complex) val stringList2 = functorListChar.fmap2(::charToInt).fmap2(::intToString) println(stringList1 == stringList2) // 输出:true}


输出:true。所以 FunctorList 已经是一个成熟的函子啦。


尾声


写这类文章也是为了加深我本人对函数式概念的思考,不出意外的话,后面等我学到应用函子(applicative)和单子(monad)的时候也会写,前提是如果我能用 Kotlin 实现它们的话。