2

自从实用Kotlin之后,最近的项目中开始可以实践高阶类型了,确实能感受到带来的优美。但同时这又是个不那么容易理解的概念,尤其是Kotlin或者说Java的类型系统中由于本身不支持,而采用一些取巧的办法实现高阶类型的时候,这个概念变得更加晦涩难懂了。
那么下面就尽量已一种通俗易懂的方式带上实例简单介绍一下这个概念,以及它在目前应用中的一些使用。

下面都是使用Kotlin进行讲解


什么是高级类型

高阶类型 Higher Kinded Type是相比于普通类型而言的,比如我们可以定义这么一个函数:

fun testFun(value: List<Int>): List<String> =
    value.map { it.toString() }

这是一个将一个整数列表List<Int>变为字符串列表List<String>的函数

但这个函数只能试用于整数列表,我们如果这时需要一个处理浮点数列表的就需要再定义一次:

fun testFun(value: List<Float>): List<String> =
    value.map { it.toString() }

但很明显,我们写了重复代码了,那么我们怎么重构以重用函数的逻辑呢
没错,可以使用泛型:

fun <T> testFun(value: List<T>): List<String> =
    value.map { it.toString() }

这样,我们就可以处理所有的列表了

但是,这时候又有了另一个需求,我们需要能够处理Set容器的函数:

fun <T> testFun(value: Set<T>): Set<String> =
    value.map { it.toString() }.toSet()

或者现在有这么个需求,我们需要能够处理所有带有map方法的容器的函数,我们该如何描述?或者说我们可以在不修改List或者其他容器就描述出这么一个通用函数吗?
伪代码:

fun <C<_> : Mappable, T> testFun(value: C<T>): C<String> =
    value.map { it.toString() }

这里的问题在于,Java的泛型系统只能描述一个具体类型,无法描述一个类型构造或者说类型函数,即一种输入一个类型然后返回一个类型类型的函数,比如:
C<_>可以看成是描述了一个叫C的类型函数,如果我们输入Int,则输出C<Int>;如果输入Float则输出C<Float>
ListSet都可以看成是这样一种类型函数,对于List,如果我们输入Int,则输出List<Int>;如果输入Float则输出List<Float>
这时,我们就可以用C<_>指代所有这种单参数的类型函数
而这里的C<_>就是高阶类型,它是类型本身的抽象

回到上面的问题,我们可以描述吗?可以,用高阶类型就可以。


Kotlin上的实现

由于Java或者Kotlin不支持高阶类型,所以我们要使用一点技巧,可以看成将高阶类型扁平化

这里就需要使用一个伪类型

interface HK<out F, out A>

这时候我们就可以描述一个上面所说的函数了:

fun <F, A> testFun(value: HK<F, A>, functor: Functor<F>): HK<F, String> =
    functor.map(value) { it.toString() }

其中Functor是一个Typeclass:

@typeclass
interface Functor<F> : TC {
    fun <A, B> map(fa: HK<F, A>, f: (A) -> B): HK<F, B>

    ...
}

那么怎么使用呢?
这个函数可以这么理解:

凡是实现了Functor实例的类型都可以应用这个函数

它不仅可以用于List等数据容器,也可以用于ObservableSingle等Rx流,还能用于OptionEither等数据类型,甚至可以应用于kotlin.jvm.functions.Function1函数。是不是觉得这个函数应用范围相当广了?

具体怎么使用呢?

对于List,首先我们定义一个List的代理类ListKW

class ListKWHK private constructor()

typealias ListKWKind<A>  = arrow.HK<ListKWHK, A>

@higherkind
data class ListKW<out A> constructor(val list: List<A>) : ListKWKind<A>, List<A> by list

然后实现ListFunctor实例(arrow库自动生成的代码):

@instance(ListK::class)
interface ListKFunctorInstance : Functor<ForListK> {
    override fun <A, B> map(fa: ListKOf<A>, f: kotlin.Function1<A, B>): ListK<B> =
            fa.fix().map(f)
}

object ListKFunctorInstanceImplicits {
  fun  instance(): ListKFunctorInstance =
    object : ListKFunctorInstance {

    }
}

这样就可以用了:

testFun(listKW, instance())

实际使用

虽然很神奇,但可能有不少人认为这很画蛇添足:有必要写这么多额外的代码吗?以前没用高阶类型不也写得好好的吗?

我的结论是:

  1. 上面的模板代码确实很烦,但这是个一劳永逸的工作,写一次以后对这个类型就都不用写了。而且还已经有库帮我们全部写完了,那就是arrow
  2. 高阶类型由于更高的抽象度,确实能有效减少重复代码,甚至实现以前无法实现的抽象(下面会提到),简单说:优美
  3. 提到高阶类型就不得不说Typeclass(上面有提到过),当我们使用高阶类型,或者说FP编程思想的时候,代码的重用方式就相应改变了。它不再以继承为核心,而是采用组合的方式,完全避免无法多继承的麻烦问题(FP中本身就没有继承的概念),对于新的Typeclass,完全不需要修改原始数据类,只需要实现它对应的实例即可(这部分内容具体讲起来就太多了,以后再展开介绍)

相比它可能带来的繁琐,它所带来的更高阶的抽象能带来更多的益处,Java中虽然也可以实现,但Kotlin中实现更加优美,或者说已经具有实用性了

举例:
Rxjava2中最大的一个改变是,将以前的Observable给分为了SingleObservableMaybeFlowable,确实能够更细致地描述返回数据的类型(是多元素的流还是单个值还是不确定是否有的值),但同时也对以前操作的抽象问题带来了挑战。

比如和上面那个问题一样,实际我们的某个函数只是需要使用map函数,但由于分为了四种流,我们不得不每种都重复写一次:

fun testFun(single: Single<Int>): Single<String> =
        single.map {
            if(it > 10)
                "over 10"
            else
                "$it"
        }

但现在我们可以直接描述为:

fun <F> testFun(fa: HK<F, Int>, functor: Functor<F>): HK<F, String> =
        functor.map(fa) {
            if(it > 10)
                "over 10"
            else
                "$it"
        }

这样所有实现FunctorTypeclass的类型都可以使用这个函数了


结语

FP提倡使用已有的数据结构来描述现有问题,这就意味着它的现有数据结构必须要足够通用。或者换句话说,它提供了比OOP更高阶的算法抽象。
它很灵活,每一个Typeclass和DataType都是很多算法的高阶抽象,相互组合可以有无限的可能。
它的代码组织方式也和OOP完全不同

这里浅尝辄止的介绍了一下高阶类型,更多的内容和实用还需要大家自己去理解


参考资料:
arrow
Higher kinded types for Java
Cats
Functional Programming in Scala
fpinkotlin
写给程序猿的范畴论


Yumenokanata
30 声望157 粉丝

Haskell中毒极深的Android开发工程师


引用和评论

0 条评论