`
jywhltj
  • 浏览: 47118 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

在 Kotlin 中“实现”trait/类型类

阅读更多

本文也发在我的个人博客上:https://hltj.me/kotlin/2020/01/11/kotlin-trait-typeclass.html 。

trait 与类型类都是什么

trait 与类型类(type class)分别是 Rust 与 Haskell 语言中的概念,用于特设多态(ad-hoc polymorphism)函数式编程等方面。

值得一提的是虽然英文都是“trait”, Scala 的特质跟 Rust 的 trait [注1] 却并不相同。 Scala 的特质相当于 Kotlin 与 Java 8+ 的接口,能实现子类型多态;而 Rust 的 trait 更类似于 Swift 的协议与 Haskell 的类型类,能实现特设多态。简单来说,trait 应同时具备以下三项能力[注2]

  1. 定义“接口”并可提供默认实现
  2. 用作泛型约束
  3. 给既有类型增加功能

Haskell 的类型类不仅同时具备这三项能力,还能定义函数式编程中非常重要的 Functor、Applicative、Monad 等。 当然这是废话,因为它们在 Haskell 中本来就是类型类 - -||。 实际上这也不是 trait 与类型类的差异,能否支持 Functor 等的关键在于语言的泛型参数能否支持类型构造器(或者说语言能否支持高阶类型)。

【注1】trait:Scala 中文社区倾向于译为“特质”,Rust 中文社区倾向于不译。 
【注2】按说“接口”不支持默认实现也可实现 Rust/Haskell 式的特设多态,但其易用性与表现力都要大打折扣。 

在 Kotlin 中寻求对应

在 Kotlin 中并没有同时具备这三项能力的对应,只有分别提供三项能力的特性。 其中 Kotlin 的接口同时具备前两项能力。

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

例如,定义一个带有默认实现的接口:

interface WithDescription {
    val description: String get() = "The description of $this"
}

Kotlin 的接口中可以定义属性与方法,二者都可以有默认实现,简便起见,示例中用了具有默认实现的属性。它可以这么用:

class Foo: WithDescription {
    // Foo 类为 description 属性提供了自己的实现
    override val description = "This is a Foo object"
}

// 对象 Bar 的 description 属性采用默认实现
object Bar: WithDescription

println(Foo().description)
println(Bar.description)

在 Kotlin REPL 中执行会得到类似这样的输出:

This is a Foo object
The description of Line_7$Bar@5bf4764d

用作泛型约束

接下来还可以将之前定义的 WithDescription 接口用在泛型函数、泛型类或者其他泛型接口中作为泛型约束,例如:

fun <T : WithDescription> T.printDescription() = println(description)

在 REPL 中执行:

>>> Bar.printDescription()
The description of Line_7$Bar@5bf4764d

遗憾的是,在 Kotlin 中不能给既有类型(类或接口)实现新的接口,比如不能为 Boolean 或者 Iterable 实现 WithDescription。 即接口不具备第三项能力,因此它不是 trait/类型类。

给既有类型增加功能

在 Kotlin 中给既有类型增加功能的方式是扩展,可以给任何既有类型声明扩展函数与扩展属性。例如可以分别给 Int 与 String 实现二者间的乘法操作符函数:

operator fun Int.times(s: String) = s.repeat(this)

operator fun String.times(n: Int) = repeat(n)

于是就可以像 Python/Ruby 那样使用了:

>>> "Hello" * 3
res11: kotlin.String = HelloHelloHello
>>> 5 * "汉字"
res12: kotlin.String = 汉字汉字汉字汉字汉字

在 Kotlin 中“实现”trait/类型类

如上文所述,Kotlin 分别用接口与扩展两个不同特性提供了 trait/类型类的三项能力,因此在 Kotlin 中没有其直接对应。 那么如果把两个特性以某种方式结合起来,是不是就可以“实现”trait/类型类了?——还别说,真就可以! Arrow 中的类型类就是这么实现的。

我们继续以 WithDescription 为例,不同的是,这回要这么声明:

interface WithDescription<T> {
    val T.description get() = "The description of $this"
}

这里利用了分发接收者可以子类化、扩展接收者静态解析的特性,可以为任何既有类型添加实现。 例如分别为 CharString 实现如下:

object CharWithDescription : WithDescription<Char> {
    override val Char.description get() = "${this.category} $this"
}

// 采用默认实现
object StringWithDescription: WithDescription<String>

不过使用时会麻烦一点,需要借助 run() 或者 with() 这样的作用域函数在相应上下文中执行:

println(StringWithDescription.run { "hello".description })

with(CharWithDescription) {
    println('a'.description)
}

在 REPL 中执行的输出如下:

The description of hello
LOWERCASE_LETTER a

用作泛型约束也不成问题:

fun <T, Ctx : WithDescription<T>> Ctx.printDescription(t: T) = println(t.description)

StringWithDescription.run {
    CharWithDescription.run {
        printDescription("Kotlin")
        printDescription('①')
    }
}

这里实现的 printDescription() 与上文的函数签名不同,因为接收者类型用于实现基于作用域上下文的泛型约束了,这也是利用接口、扩展、子类型多态以及作用域函数这些特性来“实现”trait/类型类的关键所在。 当然,如果仍然希望目标类型(如例中的 CharString)作为 printDescription 的接收者,只要将其接收者与参数互换即可:

fun <T, Ctx : WithDescription<T>> T.printDescription(ctx: Ctx) = ctx.run {
    println(description)
}

"hltj.me".printDescription(StringWithDescription)

上述两种方式中提供泛型约束的上下文要么占用了函数的扩展接收者、要么占用了函数参数。实际上还有一种方式——占用分发接收者,显然只要在 WithDescription 内声明 printDescription() 就可以了。 不过我们这里要假设 printDescription() 是自己定义的函数,而 WithDescription 是无法修改的既有类型,那么还能做到吗?——当然不成问题!只要用一个新接口继承 WithDescription 就可以了:

interface WithDescriptionAndItsPrinter<T>: WithDescription<T> {
    fun T.printDescription() = println(description)
}

object StringWithDescriptionAndItsPrinter: WithDescriptionAndItsPrinter<String>

object CharWithDescriptionAndItsPrinter:
    WithDescriptionAndItsPrinter<Char>, WithDescription<Char> by CharWithDescription

StringWithDescriptionAndItsPrinter.run {
   CharWithDescriptionAndItsPrinter.run {
        "hltj.me".printDescription()
        '★'.printDescription()
    }
}

将三种方式放一起对比会更直观:

// 方式 1
StringWithDescription.run {
    printDescription("hltj.me")
}

// 方式 2
"hltj.me".printDescription(StringWithDescription)

// 方式 3
interface WithDescriptionAndItsPrinter { /*……*/ }
object StringWithDescriptionAndItsPrinter: WithDescriptionAndItsPrinter<String>
StringWithDescriptionAndItsPrinter.run {
    "hltj.me".printDescription()
}

第三种方式的优点是提供泛型约束的上下文既不占用扩展接收者也不占用参数,但其代价是需要为每个用到的目标类型(如例中的 CharString)提供新接口(如例中的 WithDescriptionAndItsPrinter<T>)的相应实现,并且依然需要借助作用域函数 run() 或 with()。 因此通常采用前两种方式即可,但是如果要自定义操作符函数或者中缀函数时就只能采用第三种方式了,例如:

interface DescriptionMultiplier<T>: WithDescription<T> {
    infix fun T.rep(n: Int) = (1..n).joinToString { description }

    operator fun T.times(n: Int) = this rep n
}

object CharDescriptionMultiplier:
    DescriptionMultiplier<Char>, WithDescription<Char> by CharWithDescription

println(object : DescriptionMultiplier<String> {}.run { "hltj.me" rep 2 })

println(CharDescriptionMultiplier.run { 'A' * 3 })

在 REPL 中执行的输出为:

The description of hltj.me, The description of hltj.me
UPPERCASE_LETTER A, UPPERCASE_LETTER A, UPPERCASE_LETTER A

扩展与成员的优先级

我们知道,在 Kotlin 中扩展与成员冲突时总是取成员。 但是在使用基于作用域上下文的泛型约束时却并非如此,例如:

interface WithLength<T> {
    val T.length: Int
}

object StringWithFakeLength: WithLength<String> {
    override val String.length get() = 128
}

fun <T, U: WithLength<T>> U.printLength(t: T) = println(t.length)

StringWithFakeLength.run {
    printLength("hltj.me")
}

在 REPL 中运行输出是 128,表明 printLenth() 取到的 length 是 StringWithFakeLength 中定义的扩展属性而不是 String 自身的属性。因此使用时需要特别注意。 唯有 Any 的三个成员 toString()hashCode()equals() 会始终调用成员函数,即便在泛型约束上下文中声明了具有相同签名的扩展函数也是一样。

“实现”Functor 等

按照上文介绍的方式,我们可以轻松实现 ShowEqOrd 等简单类型类,无需赘述。 但是如果要实现 FunctorApplicativeMonad 等却会遇到问题。 以 Functor 为例,按说要这么定义:

interface Functor<C<*>> {
    fun <T, R> C<T>.fmap(f: (T) -> R): C<T>
}

但遗憾的是上述代码无法通过编译,因为 Kotlin 目前不支持高阶类型,在泛型参数中用 C<*> 表示类型构造器只是假想的语法 。 因此,需要有一种方式来变通。按 Arrow 的方式引入 Kind 接口来表示:

interface Kind<out F, out A>

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

然后写一个标记类,让具体类型作为 Kind<标记类, T> 的实现类。再定义一个由 Kind<标记类, T> 向具体类型转换的扩展函数 fix(),以便在具体实现中使用。 例如:

class ForMaybe private constructor()

sealed class Maybe<out T> : Kind<ForMaybe, T> {
    object `Nothing#` : Maybe<Nothing>() {
        override fun toString(): String = "Nothing#"
    }
    data class Just<out T>(val value: T) : Maybe<T>()
}

fun <T> Kind<ForMaybe, T>.fix(): Maybe<T> = this as Maybe<T>

这样就可以为 Maybe 实现 Functor<ForMaybe> 了:

object MaybeFunctor : Functor<ForMaybe> {
    override fun <T, R> Kind<ForMaybe, T>.fmap(f: (T) -> R): Maybe<R> = when (val maybe = fix()) {
        is Maybe.Just -> Maybe.Just(f(maybe.value))
        else -> Maybe.`Nothing#`
    }
}

fun main() = with(MaybeFunctor) {
    println(Maybe.Just(5).fmap { it + 1 })
    println(Maybe.`Nothing#`.fmap { x: Int -> x + 1 })
}

可以看出这种实现方式会有明显的局限性:只能为 Arrow 中定义的类型或者按照 Arrow 方式实现的既有类型实现 FunctorApplicativeMonad 等接受类型构造器作为泛型参数的“类型类”。 好在 Arrow 已经自带了大量有用的类型,很多场景都够用。

需要注意的是这段代码无法在当前版本(1.3.61)的 Kotlin REPL 中运行,需要放在普通的 Kotlin 文件中编译运行。

Arrow

Arrow(按其官网写作 Λrrow)是 Kotlin 标准库的函数式“伴侣”。目前主要以下四套件:

此外还有若干套件/特性还在孵化中。 关于 Arrow 整体与模式的介绍也在 Arrow Core 的文档中,其中 Functional Programming Glossary 提供了一些使用 Arrow 进行函数式编程的背景知识可供参考。

一点意外

在尝试写这些示例时意外发现了一个会导致当前版本的 Kotlin JVM 编译器抛异常的 bug,最小重现代码如下:

interface WithIntId<T> {
    val T.intId get() = 1
}

object BooleanWithIntId : WithIntId<Boolean>

val x = BooleanWithIntId.run {
    true.intId
}

只影响 Kotlin JVM 编译器,Kotlin JS 与 Kotlin Native 都不存在这个问题。 查了下 YouTrack,看起来是个已知 bug。 不过文中的其他示例代码都能正常编译运行,尽可放心。

 

灰蓝天际 灰蓝天际

转载请勿修改,并注明作者:灰蓝天际 及许可协议:署名-非商业性使用-禁止演绎



欢迎关注:
GitHub:hltj    微博:灰蓝天际(@hltj)    Twitter:@jywhltj

weibo_qr.pngweibo_qr.png
公众号 微博
0
1
分享到:
评论

相关推荐

    sudoku-android,使用koltin/jvm kotlin/js kotlin/native的android、web和ios数独示例应用程序.zip

    至于iOS,Kotlin/Native编译器将Kotlin代码转换为原生的Objective-C或Swift代码,可以直接在Xcode中进行调试和部署。这意味着开发者可以使用Kotlin编写iOS应用,同时也能够利用Swift和Objective-C的生态系统。 总的...

    MpApt,kotlin native/js/jvm注释处理器库.zip

    在Kotlin中,我们可以使用`@Processor`注解来声明一个注解处理器,并利用Kotlin的类型安全特性来编写更强大的处理器。 ### MpApt的平台支持 - **Kotlin Native**:Kotlin Native是Kotlin的一个分支,它允许开发者...

    MpApt,Kotlin Native/JS/JVM Annotation Processor library.zip

    MpApt - Kotlin (Native/JS/JVM) Annotation Processor library I wrote an annotation processing libary that can detect annotations in Kotlin Native/JS and Jvm projects, because Kapt is only ...

    Kotlin语言实现FlexboxLayout流式布局替换Recycleview实现单选/多选

    在Kotlin中,我们可以定义一个数据类来存储列表项的数据,包括文本、图片等,以及一个布尔值来标记是否被选中: ```kotlin data class FlexItem(val title: String, val isSelected: Boolean = false) ``` 接着,...

    jackson-module-kotlin, 添加对Kotlin的序列化/反序列化支持的模块( http.zip

    jackson-module-kotlin, 添加对Kotlin的序列化/反序列化... 在Kotlin对象的对象上必须存在一个默认构造函数,以便将它的反序列化到对象。 通过这个模块,可以以自动使用单个构造函数类,并支持带有辅助构造函数或者 s

    最简单的方法来询问Android的运行时权限,无需扩展类或覆盖权限结果方法,选择你的方式:Kotlin / Coroutines / RxJava / Java7 / Java8.zip

    最简单的方法来询问Android的运行时权限,无需扩展类或覆盖权限结果方法,选择你的方式:Kotlin / Coroutines / RxJava / Java7 / Java8.zip,在android上请求运行时权限的最简单方法,不需要扩展类或重写...

    okhttp3.2与okio1.6.zip

    Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics at okio.Okio.source(Okio.kt) at okhttp3.internal.io.RealConnection.connectSocket(RealConnection.java:144) ...

    Kotlin,在kotlin中实现的所有算法.zip

    在这个开源项目"Kotlin,在kotlin中实现的所有算法.zip"中,我们可以深入学习到如何用Kotlin来实现各种算法,这对于提升我们的编程技能和理解算法的内在逻辑至关重要。 一、基础算法 这个项目中包含了基础算法的...

    Design-Patterns-In-Kotlin,在kotlin中实现的设计模式.zip

    在Kotlin中,我们可以使用工厂函数或者抽象工厂类来实现此模式,为不同类型的对象提供统一的创建接口。 3. **建造者模式**:建造者模式允许分步骤构建复杂对象。Kotlin的构造函数参数和默认值,以及它的扩展函数...

    kotlin实现的进度条

    本篇文章将深入探讨如何使用Kotlin在Android中实现进度条,并结合具体代码实例来解析`MyProgressBar`这个项目。 首先,我们需要在布局文件(如activity_main.xml)中定义一个ProgressBar。进度条有两种主要类型:...

    kotlin官方中文文档

    对于JavaScript开发者,Kotlin/JS提供了一种用Kotlin编写前端应用的方式,支持React框架,并提供了开发、调试和持续编译的工具链。 总的来说,《Kotlin官方中文文档》是学习和掌握Kotlin语言不可或缺的参考资料,...

    java和kotlin的内部类静态嵌套类

    在移动开发中,特别是Android应用开发,内部类和静态嵌套类经常用于实现回调、事件监听、以及封装与特定组件关联的行为。例如,你可以在一个Activity或Fragment中定义一个内部类来处理特定的点击事件,或者创建一个...

    Android-Kotlin实现RecyclerView数据列表Demo

    5. **Kotlin的类和函数**:在适配器中,通常会有一个inner class来表示ViewHolder,里面包含对视图的引用和一个`onBindViewHolder()`方法,该方法将数据与ViewHolder的视图关联起来。同时,适配器还需要重写` ...

    Android中使用Kotlin实现一个简单的登录界面

    `LoginUi`是一个内部类,实现了`AnkoComponent&lt;LoginActivity&gt;`,这个组件允许我们在Kotlin代码中直接定义UI结构。例如,`verticalLayout`、`imageView`和`editText`等都是Anko提供的DSL,它们对应于XML中的`...

    Kotlin跨界秀:深入探索Kotlin/JS与Kotlin/Native的跨平台魔力

    2. **互操作性**:Kotlin能够无缝地与Java代码集成,允许在现有的Java项目中使用Kotlin,或者在Kotlin项目中使用Java库。 3. **安全性**:Kotlin是一种静态类型语言,提供了编译时的类型检查,有助于避免常见的类型...

    kotlin 加密算法工具类

    本文将详细介绍在Kotlin中实现的几种主要加密算法,包括AES、DES、CBC/ECB模式、MD5、SHA1以及SHA256。这些算法在数据保护、网络安全以及密码学中扮演着关键角色。 1. **AES(Advanced Encryption Standard)**:...

    kotlin实现TabLayout吸顶顶部导航最简单的demo.rar

    在`MyFragment`类中,你可以定义每个页面的具体内容和行为。例如,你可以在onCreateView方法中返回一个包含页面内容的布局。 ```kotlin class MyFragment : Fragment() { override fun onCreateView(inflater: ...

    kotlin实现的简易计算器

    本项目"Kotlin实现的简易计算器"是一个使用Kotlin编写的简单计算器应用程序,旨在帮助初学者理解Kotlin的基础语法以及如何在实际项目中运用这些语法特性。 首先,Kotlin中的类定义是构建程序的基本单元。在这个简易...

    Android-实现在Kotlin中更方便使用canvas

    本篇文章将深入探讨如何在Kotlin中更有效地利用Canvas进行绘制,以实现丰富的视觉效果。 首先,我们需要了解Canvas的基本用法。在Android中,通常通过`onDraw()`方法在View的生命周期中调用Canvas,如在自定义View...

Global site tag (gtag.js) - Google Analytics