从Kotlin的设计小窥编程语言的组合性

最近在复习Kotlin的协程,偶然在这篇文章里遇到一道有趣的题目,也就稍微做了一下。

题目第一眼看起来还是挺有难度的,要在Kotlin里面实现Haskell那样的do-notation语法。不过,有了前一篇文章的提示,再加上一些Kotlin/Coroutine和函数式编程的基础,倒也不难做。大概的想法是用协程的挂起和恢复来模拟出Monad的组合。具体的细节就不展开了,有兴趣可以自己试试。显然,Optional或者Nullable本身也是Monad,那么怎么用这个语法表现Optional也就不言而喻了。

也许也是一个对「Monad就是『可组合的计算上下文』」的一个阐释吧。

做完之后又看到这篇关于coroutine优势的讨论。简而言之,Kotlin的coroutine用同一个系统就实现了其他语言里可能分离实现的若干语言特性。Kotlin在底层实现好了编译期CPS变换,但用户可以自行提供调度器或是对不同线程池的封装。

想起了之前看到的关于有栈协程和无栈协程的争论,还有最近一年来一直很火的Project Loom,也许这也是一个很好的证明编程语言的组合性和正交性的例子吧:

添加coroutine(背后的核心还是continuation),让控制流成为表达式一样的东西,就可以简便表达出许多其他语言里难以实现的异步或多线程原语,甚至可以跳出OOP的范畴去模拟一些FP的概念。

相对应的,远离可变性和可空性,让函数和表达式更容易被reasoning,也让并行和多线程变得更容易,这可不是TDD靠堆测试数就能做到的事情。

而且换个视角来说,nullable或者optional,容器,任务,这些东西的背后都隐藏着某些共通的属性(如果从OOP的角度,一定要避免把那个M开头的词说出来的话)。哪怕限于OOP和子类型的限制,只能在这方面做有限的探索,也能够改善程序的组合性呢。

不过话说过来,其实作为一种实用导向的OOP语言,Kotlin的组合性也不是无限扩展的。回到前面的例子的话,如果把Optional类型换成List的话,会在试图第二次抽取元素的时候抛出错误:

1
Exception in thread "main" java.lang.IllegalStateException: Already resumed

说到底,还是因为coroutine是single-shot的,所以遇到List这种有多种可能输出的数据结构,就会报错。一些Kotlin上面的函数式编程库可能会重新实现一种multi-shot的协程机制,但这就不是这个Codewars的题目涉及到的话题了。

不过基于single-shot的原理,OptionalEither/Result或者其他只有一个元素的容器(也许也包括IO?),应该是能用类似的思路写出for-comprehension的。

It's not possible at the moment to implement monad comprehension for List, Flow, and other non-deterministic data structures that emit more than one value. The current implementation of continuations in Kotlin is single shot only. This means a continuation can resume a program with a single emitted value.

这里解释了这一现象的存在。

另外一个限制就是和subtying有关的了。哪怕是假设List也能写出for-comprehension的前提下,也会发现,我们为每个类型都写了一个单独的for-comprehension的实现。在90%的代码都几乎一样的情况下,很明显,开发者会希望能够只维护一个实现代码就能让它用在不同的地方。

但这在Kotlin里面是做不到的,因为ListOptional之类的类型是语言自带的,而它们所继承的基类和接口也是不一样的。具体来说,Optional是Java自带的类型,而Java人应该是没理由把Optional继承自Collection基类的。

也许你会想,要是能够让用户自行扩展继承关系,让Optional类型也继承Collection就好了。Swift似乎是可以这么做的,但是在做这个加法之前,显然我们又会遇到「这样真的能提高组合性吗」的问题。

这次也就先到这里了。


从Kotlin的设计小窥编程语言的组合性
http://inori.moe/2024/02/18/composition-pl-kotlin/
作者
inori
发布于
2024年2月18日
许可协议