高阶函数式编程三:在 Kotlin 中“实现”应用函子(Applicative)
Posted Kotlin 维修车间
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高阶函数式编程三:在 Kotlin 中“实现”应用函子(Applicative)相关的知识,希望对你有一定的参考价值。
继函子(Functor)和单位半群(Monoid)之后,《高阶函数式编程》系列终于更新了,这篇文章拖更已久其实主要是之前试了很多种方法实现应用函子(Applicative)但思路不对一直未能成功,今天来了点灵感,终于成功搞定。阅读本文前需要一点前置知识,欢迎回顾我的往期文章:
应用函子是什么?
应用函子(Applicative)是一种加强版的函子(Functor),先回顾一下我们之前实现的函子:
interface Functor<T, R : Functor<T, R, S>, S : Functor<*, *, S>> {
infix fun <A> fmap1(function: (T) -> A): (R) -> S
infix fun <A> fmap2(function: (T) -> A): S
}
infix fun <A, B, T, R : Functor<T, R, S>, S : Functor<*, *, S>> Functor<T, R, S>.fmap2Default(function: (T) -> A) =
fmap1(function)(this as R) as B
函子的核心是 fmap 函数,能够帮我们将一个 (T) -> A 类型的函数升格成一个持有这两个类型的函子形式。但是 fmap 不能满足所有的场景,比如函数 (T) -> A 如果也包裹在函子内部呢?
假设某个函子类型是 F,fmap 函数只能将 (T) -> A 类型的函数升格成为 (F<T>) -> F<A> 类型的函数,但无法直接处理 F<(T) -> A>,而应用函子则正是解决这一问题的有力助手。关于应用函子的概念性知识可以参照《Haskell 趣学指南》第 11 章,以及《魔力 Haskell》第 13 章。
先来看看 Applicative 类型类在 Haskell 中的定义:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
首先 Applicative 类型类的声明中就加了 Functor 约束,表明 Applicative 必须首先是一个 Functor,如果用 Kotlin 的 interface 表达类型类的话,这个关系类似于接口继承。pure 函数比较简单,为一个 a 类型的元素外层包裹上函子的最小上下文。而核心函数 <*> 实际上就是 fmap 的升级版本,我们用 Kotlin 实现 Applicative 的重点就是它。
实现的误区
之前我尝试过很多次在 Kotlin 中实现 Applicative 都以失败告终,原因就在于我使用了惯性思维将 Applicative 接口继承自 Functor 接口:
interface Applicative<T, R : Applicative<T, R, S>, S: Applicative<*, *, S>> : Functor<T, R, S>
上面的代码无法通过编译,因为编译器无法识别 R 与 S 类型是 Functor 类型的子类型。虽然我们自己会感觉逻辑自洽:Applicative 继承自 Functor,而 R 与 S 继承自 Applicative,所以 R 与 S 继承自 Functor 明明应该很合理才对,但 R 与 S 在 Applicative 内部声明,其类型也在 Applicative 内部约束,但 Applicative 继承自 Functor 在 Applicative 外部,R 与 S 作为参数传递给 Functor 也在 Applicative外部,编译器无法处理这种鸡生蛋蛋生鸡的问题,所以编译无法通过。编译器报错为:“Type argument is not within its bounds.”
正确的实现方式
上面我们讨论了 Applicative 直接继承自 Functor 无法通过编译,也就是这种思路其实不可行,那么我们如何保证 Applicative 是一个 Functor?仔细思考一下,我们其实不需要让编译器认为 Applicative 类型就是 Functor 类型的子类型,我们实际上要做的是,让所有 Applicative 类型的子类型必须是 Functor 类型的子类型就可以了。打个比方,我们有一个函数,它的参数类型是 Functor,如果 Applicative 的所有实现者都同时实现了 Functor,那么我们将 Applicative 的实现者的对象直接传入必然能正常运行。那如何做到这一点?或者说如何定义 Applicative 接口可以让编译器强制它的所有实现者必须实现 Functor 接口?还记得 Functor 接口定义中的 R 泛型参数吗,它代表的就是 Functor 接口的实现者自己,我们在定义 Applicative 同样也需要一个自限定的泛型来表示 Applicative 的实现者自身,因此我们只需对其进行 Functor 类型的约束声明就可以了:
interface Applicative<T, R, S> where
R : Applicative<T, R, S>,
R : Functor<T, R, S>,
S : Applicative<*, *, S>,
S : Functor<*, *, S>
Applicative 不直接继承自 Functor,我们通过 where 子句对 S 进行双重的类型约束,对于表达持有另一种元素的函子类型 S 也和 R 是同理的。
pure 函数很容易声明:
interface Applicative<T, R, S> where
R : Applicative<T, R, S>,
R : Functor<T, R, S>,
S : Applicative<*, *, S>,
S : Functor<*, *, S> {
infix fun pure(t: T): R
}
接下来就是核心函数 <*> 了。根据我之前在一文中的讨论,由于柯里化的存在,我们可以对 fmap 函数的含义产生两种解读,对于 Applicative 的 <*> 函数也是一样的,因此我们可知:
含义 1:<*> 函数表示一种接收一个包裹在应用函子内的函数 f (a -> b) 以及一个装有 a 类型的应用函子 f a 作为参数,返回一个装有 b 类型的应用函子 f b 作为结果的函数。
含义 2:<*> 函数也可以表示接收一个包裹在应用函子内的函数 f (a -> b) 作为参数,并将其转化为一个接收装有 a 类型的应用函子 f a 作为参数及装有 b 类型的应用函子 f b 作为返回值的函数。
为表达以上两种含义,两个不同的 <*> 函数我们分别命名为 `<*>` 与 `<**>`:
interface Applicative<T, R, S> where
R : Applicative<T, R, S>,
R : Functor<T, R, S>,
S : Applicative<*, *, S>,
S : Functor<*, *, S> {
infix fun pure(t: T): R
infix fun <A, F> `<*>`(function: F): S where F : Applicative<(T) -> A, F, *>, F : Functor<(T) -> A, F, *>
infix fun <A, F> `<**>`(function: F): (R) -> S where F : Applicative<(T) -> A, F, *>, F : Functor<(T) -> A, F, *>
}
我们在函数中声明了泛型 F 用以表达装有 a -> b 类型函数(在上面的 Kotlin 实现中是 (T) -> A)的函子。
这里实际上还有一个小瑕疵,关于包裹函数类型的函子 F 类型的声明问题。现在遇到了一个小困境,如果 F 像上面的实现中一样,作为函数的泛型参数,它只有在函数被调用的地方才能确定,这个时候函数调用者可以向函数传入任意一个应用函子,而不必使其和 R 与 S 一样是同一个 Applicative 接口实现者,这将一些可能的类型转换错误留到了运行时。还有一种思路是将 F 声明为 Applicative 接口的泛型参数,有人也许会说这个时候还不知道 A 的类型该怎么办?我们可以仿照 S 类型,将还未可知的类型使用 * 或 Any 来代替。这样的话 F 的类型限定就是:
where F : Applicative<(T) -> Any, F, *, *>
F : Functor((T) -> Any, F, *)
看似可行但编译器又会认为这是一个无限循环的类型约束从而报出上面的:“Type argument is not within its bounds.” 错误使编译无法通过。所以以上的 Applicative 接口的声明算是当前最恰当的方式,对于 Applicative 接口的使用者来说要严格注意一些编译器无法检测的约定。
List 应用函子
List 是我们的老朋友了,之前我们实现了函子 List,来回顾一下:
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> = fmap2Default(function)
override fun hashCode(): Int = coreList.hashCode()
override fun equals(other: Any?): Boolean = coreList == other
override fun toString(): String = coreList.toString()
}
应用函子作为强化版的函子,如果要实现应用函子 List,我们只需要对 FunctorList 进行一些扩充就可以了。
在应用函子 List 中,<*> 的作用是将一个列表中的所有元素应用到一个列表中的所有函数,这相当于是一个双层的遍历求值,然后将结果平铺展开为一个列表,举例来说,将列表:[1 .. 5] 应用给函数列表 [(-2), (+3) (*5)],结果为 [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 5, 10, 15, 20, 25]。
如何实现既然我们已经知道,那么实现自然不是难事:
class ApplicativeList<T>(private val coreList: List<T>) : List<T> by coreList,
Functor<T, ApplicativeList<T>, ApplicativeList<*>>,
Applicative<T, ApplicativeList<T>, ApplicativeList<*>> {
override infix fun <A> fmap1(function: (T) -> A): (ApplicativeList<T>) -> ApplicativeList<A> = { ApplicativeList(it.map(function)) }
override infix fun <A> fmap2(function: (T) -> A): ApplicativeList<*> = fmap2Default(function)
override infix fun pure(t: T): ApplicativeList<T> = ApplicativeList(listOf(t))
override infix fun <A, F> `<*>`(function: F): ApplicativeList<A> where F : Applicative<(T) -> A, F, *>, F : Functor<(T) -> A, F, *> =
ApplicativeList((function as? ApplicativeList<(T) -> A>)?.flatMap { func -> map { func(it) } } ?: throw ClassCastException("Param function must be ApplicativeList Type"))
override fun <A, F> `<**>`(function: F): (ApplicativeList<T>) -> ApplicativeList<A> where F : Applicative<(T) -> A, F, *>, F : Functor<(T) -> A, F, *> = { `<*>`(function) }
override fun hashCode(): Int = coreList.hashCode()
override fun equals(other: Any?): Boolean = coreList == other
override fun toString(): String = coreList.toString()
}
要将一个列表里的元素以应用于一个列表中的每一个函数计算出值之后再平铺对 Kotlin 来说不是个很麻烦的事情,我们直接借用标准库函数 flatMap 即可很容易的实现。
根据柯里化,当函数 `<*>` 的实现确定之后,`<**>` 函数的实现也应该唯一确定,所以我们定义一个类似 fmap2Default 的扩展函数用于提供 `<**>` 函数的默认实现:
infix fun <A, F, T, R, S> Applicative<T, R, S>.`<*Default*>`(function: F): (R) -> S where
R : Applicative<T, R, S>,
R : Functor<T, R, S>,
S : Applicative<*, *, S>,
S : Functor<*, *, S>,
F : Applicative<(T) -> A, F, *>,
F : Functor<(T) -> A, F, *> = { param -> param `<*>` function }
好了,ApplicativeList 的完整实现如下:
class ApplicativeList<T>(private val coreList: List<T>) : List<T> by coreList,
Functor<T, ApplicativeList<T>, ApplicativeList<*>>,
Applicative<T, ApplicativeList<T>, ApplicativeList<*>> {
override infix fun <A> fmap1(function: (T) -> A): (ApplicativeList<T>) -> ApplicativeList<A> = { ApplicativeList(it.map(function)) }
override infix fun <A> fmap2(function: (T) -> A): ApplicativeList<*> = fmap2Default(function)
override infix fun pure(t: T): ApplicativeList<T> = ApplicativeList(listOf(t))
override infix fun <A, F> `<*>`(function: F): ApplicativeList<A> where F : Applicative<(T) -> A, F, *>, F : Functor<(T) -> A, F, *> =
ApplicativeList((function as? ApplicativeList<(T) -> A>)?.flatMap { func -> map { func(it) } } ?: throw ClassCastException("Param function must be ApplicativeList Type"))
override fun <A, F> `<**>`(function: F): (ApplicativeList<T>) -> ApplicativeList<A> where F : Applicative<(T) -> A, F, *>, F : Functor<(T) -> A, F, *> =
`<*Default*>`(function) as (ApplicativeList<T>) -> ApplicativeList<A>
override fun hashCode(): Int = coreList.hashCode()
override fun equals(other: Any?): Boolean = coreList == other
override fun toString(): String = coreList.toString()
}
现在该到验证 ApplicativeList 的实现是否正确的时候了:
fun main() {
val listFunction = ApplicativeList(listOf(
{ x: Int -> "${x - 2}&" },
{ x: Int -> "${x + 3}&" },
{ x: Int -> "${x * 5}&" }
))
val listInt = ApplicativeList(listOf(1, 2, 3, 4, 5))
println(listInt `<*>` listFunction)
}
ApplicativeList listFunction 内装入了清一色的 (Int) -> String 类型的函数,我们将 Int 类型的数字进行不同的运算之后为了更容易的看出结果在末尾加上了 & 符号,运行输出结果如下:
[-1&, 0&, 1&, 2&, 3&, 4&, 5&, 6&, 7&, 8&, 5&, 10&, 15&, 20&, 25&]
然后来验证一下 `<**>` 函数:
fun main() {
val listFunction = ApplicativeList(listOf(
{ x: Int -> "${x - 2}&" },
{ x: Int -> "${x + 3}&" },
{ x: Int -> "${x * 5}&" }
))
val listInt = ApplicativeList(listOf(1, 2, 3, 4, 5))
val resultFunction = ApplicativeList(listOf<Int>()) `<**>` listFunction
println(resultFunction(listInt))
}
大同小异,只不过先通过执行 `<**>` 函数获得一个函数,然后再运行这个函数得到结果,输出如下:
[-1&, 0&, 1&, 2&, 3&, 4&, 5&, 6&, 7&, 8&, 5&, 10&, 15&, 20&, 25&]
验证应用函子定律
同函子一样,应用函子也有其必须遵守的定律,当我们自定义的应用函子符合这些定律的时候才是一个合格的应用函子,应用函子定律有多条,不过今天我不太想全部验证,我们来验证一条最重要的定律:
pure f <*> x = fmap f x
直接写验证代码:
fun main() {
val listInt = ApplicativeList(listOf(1, 2, 3, 4, 5))
val function = { x: Int -> "${x * 5}&" }
val utilList = ApplicativeList(listOf<(Int) -> String>())
val result = listInt `<*>` (utilList pure function) == listInt fmap2 function
println(result)
}
由于我们用 interface 来表达 type class 的语法限制,pure 是个成员函数,所以上面创建了一个空的 utilList 用于调用 pure,其他代码都比较好理解,最终输出结果为:true,验证通过。
以上是关于高阶函数式编程三:在 Kotlin 中“实现”应用函子(Applicative)的主要内容,如果未能解决你的问题,请参考以下文章
高阶函数式编程:在 Kotlin 中“实现”函子(Functor)
高阶函数式编程二:在 Kotlin 中实现单位半群(Monoid)
Kotlin函数式编程 ① ( 函数式编程简介 | 高阶函数 | 函数类别 | Transform 变换函数 | 过滤函数 | 合并函数 | map 变换函数 | flatMap 变换函数 )