高阶类型带来了什么

继承盆景
• 阅读 3803

自从实用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
写给程序猿的范畴论

点赞
收藏
评论区
推荐文章
浅梦一笑 浅梦一笑
4年前
Python小白零基础入门 —— 变量及简单的数据类型
最近想着出一个Python小白零基础入门系列的文章,希望能对入门的小伙伴有所帮助,内容会囊括简单的数据类型、列表、字典、循环以及函数的定义,对于一些概念会以图解的方式进行讲解。今天这篇文章就介绍一下Python中的变量以及简单的数据类型,文末还列了一些练手的题目,大家可以边学边练!一、变量1、变量是什么在Python中,变量是用来储存数据的,更直白点,变
Wesley13 Wesley13
3年前
java枚举类型enum的使用
java枚举类型enum的使用最近跟同事讨论问题的时候,突然同事提到我们为什么java中定义的常量值不采用enmu枚举类型,而采用publicfinalstatic 类型来定义呢?以前我们都是采用这种方式定义的,很少采用enum定义,所以也都没有注意过,面对突入起来的问题,还真有点不太清楚为什么有这样的定义。既然不明白就抽
Irene181 Irene181
4年前
一篇文章带你了解Python高阶函数
一、什么是高阶函数?高阶函数是在Python中一个非常有用的功能函数,所谓高阶函数就是一个函数可以用来接收另一个函数作为参数,这样的函数叫做高阶函数。通过案例代码分析,一步一步深入概念。二、变量可以指向函数以Python内置的求绝对值的函数abs()为例。调用该函数用以下代码:print(abs(10))运行结果:但是,如果只写abs呢?prin
Irene181 Irene181
4年前
一篇文章带你了解Python高阶函数
一、什么是高阶函数?高阶函数是在Python中一个非常有用的功能函数,所谓高阶函数就是一个函数可以用来接收另一个函数作为参数,这样的函数叫做高阶函数。通过案例代码分析,一步一步深入概念。二、变量可以指向函数以Python内置的求绝对值的函数abs()为例。调用该函数用以下代码:print(abs(10))运行结果:但是,如果只写abs呢?prin
Easter79 Easter79
3年前
swift高阶函数和函数式编程
Swift函数式编程,函数式编程的思想就是一切皆函数,可以是被当作变量,参数,返回值。高阶函数运用对swift编程很重要。基础一般常用的几个高阶函数如下letnumArr5,4,6,1,7//遍历所有并操作print(numArr.map{$01})//输出:6,5,7,2,8
菜园前端 菜园前端
2年前
什么是高阶函数?
原文链接:什么是高阶函数?有两种情况都可以被定义为高阶函数,第一种是把函数作为参数传递给另外一个函数,第二种是把函数作为另一个函数的返回结果。就像我们平时调用函数,一般都是传递值类型或者对象和数组等参数,或者是函数返回结果是值类型或者是对象和数组,高阶函数
待兔 待兔
4年前
Flutter开发必备Dart基础:Dart快速入门
<h1概述</h1<pDart从2.0开始变为强类型语言,静态类型。这点和Java、C等比较相似。也就是说在编译时就已经知道变量的类型那么就是静态类型语言。开发人员在开发的时候需要指定变量的类型。这有什么优点呢?就是所有类型检查都可以通过编译器来完成。可以提前预报一些琐碎的错误。<br同时Dart还是面向对象的编程语言。像python、Java、Kol
Stella981 Stella981
3年前
Python将字符串转换成ObjectId类型
MongoDB自动生成的_id是ObjectId类型的。我需要将MongoDB的_id存到ElasticSearch中,而ElasticSearch又只能存String类型的_id,所以就涉及到两种类型的转换。ObjectId类型—→String类型这个非常简单
Stella981 Stella981
3年前
Python中的函数式编程教程,学会用一行代码搞定
01前言在本文中,您将了解什么是函数范型,以及如何在Python中使用函数式编程。在Python中,函数式编程中的map和filter可以做与列表相同的事情。这打破了Python的禅宗规则之一,因此函数式编程的这些部分不被认为是“Python式的”。但是由于函数式编程高阶编程的必经之路,所以我们需要了解甚至熟练掌握。02
Wesley13 Wesley13
3年前
Java增强的包装类
java语言是面向对对象的编程语言,但这八种基本数据类型不支持面向对对象的编程的机制,基本数据类型的数据不具备“对象”的特征:没有成员变量、方法可以调用。java提供这8中基本数据类型,主要是为了照顾程序员的传统的习惯。但这也带来了麻烦,因为所有的引用类型都继承来自Object的类,可当成object的类型使用,所以当一个方法的参数是object类型参数时候
Wesley13 Wesley13
3年前
C++ 模板基础
我们学习使用C,肯定都要了解模板这个概念。就我自己的理解,模板其实就是为复用而生,模板就是实现代码复用机制的一种工具,它可以实现类型参数化,即把类型定义为参数;进而实现了真正的代码可重用性。模版可以分为两类:一个是函数模版,另外一个是类模版。举个最简单的例子,当在编写好了一个进行int型交换的swap函数,而此后若又要进行double型交换,那就得重
继承盆景
继承盆景
Lv1
南朝四百八十寺,多少楼台烟雨中。
文章
4
粉丝
0
获赞
0