标签 scala 下的文章

我注意到在我前些天的一篇回答的评论里,Swift 和 Scala 都被拿来和 Kotlin 做比较。

我于是忽然想写一点没什么技术水平、也没什么内容结构的随笔,谈谈一些关于 data class 的一些有意思的地方。

我写 Swift 时也注意到,Swift 里的 enum 中每一项是可以有参数的,亦即「associated values」。例如:

enum Gender {
    case male
    case female
    case other(description: String)
}

于是写 Kotlin 的时候,我就在想,为什么 Kotlin 的 enum 不能带参数呢…… 后来我才意识到,其实「带 associated values 的 enum」实际上可以用 sealed class 的语法来等同:

sealed class Gender {
    object Male: Gender()
    object Female: Gender()
    data class Other(val description: String): Gender()
}

Sealed class 和 data class 这两个东西,用在一起真是蛮合适呢。其实这个例子里,不加 sealeddata 两个修饰符,也能编译通过。但是,加上这两个修饰符,似乎更能表达类似于 enum 的语义。为什么呢?

在 Kotlin 里,sealed class 也是为了协助 when 语句的 exhaustiveness check 的;也就是说,对于一个 sealed class,如果 when 已经处理了它的所有已知子类,那就不再需要 else 分支。事实上,Sealed class 这个概念在 Scala 里也有完全相同的存在——相似地,它的主要用途是协助 pattern matching 的 exhaustiveness check。

而 Scala 里也有着和 data class 相似的存在,那就是 case class。它们同样用于表示「仅仅用于组织数据的类」,也就是类似于「带了 tags 的 tuple」;并且,它们同样提供了符合直觉的 equals 方法的实现。

然而,在 Scala 里,正如「case class」这个名字所暗示,其一个很大的用途是在 pattern matching 中。在 Scala 中,假设有

case class Bar(noun: String, verb: String)

那么就可以对变量 foo: Bar 做

foo match {
    case Bar(_, "药丸") => "后面有药丸"
    case Bar("青果", predicate) => "青果" + predicate + "了"
    case _ => "啥都没有"
}

所以:Kotlin 不提供 pattern matching 简直就是反人类!

Improved Pattern Matching in Kotlin 提供了一些增强 when 语句的奇技淫巧。当然,这样的奇技淫巧的代价,就是丧失 exhaustiveness check 的功能——因为 when 语句的 exhaustiveness check 非常弱,仅仅能识别「is XXX」这样的条件。

利用类似的奇迹淫巧,再加上更肮脏的反射,我们就能对 Kotlin 的 data class 做类似的事情——至少实现上面那段 Scala 中那样,对 foo 中的属性的值做 matching。这里利用的是,Kotlin 对于 data class 会生成 componentN() 方法 。

「奇技淫巧」所需要的一点 bolterplating:

object any // 因为没法直接用 _

class MatchDataClass<T: Any>(private val kClass: KClass<T>, private val params: Array<out Any>) {
    init {
        if (!kClass.isData) { throw IllegalArgumentException("Not a data class!") }
    }
    operator fun contains(input: Any): Boolean { // 实现「in」操作符
        return if (!kClass.isInstance(input)) false else (
            params.mapIndexed { index, criteria ->
                (criteria is any) || (kClass.java.getMethod("component" + (index + 1)).invoke(input) == criteria)
            }.all { it }
        )
    }
}

inline fun <reified T: Any> match(vararg params: Any) = MatchDataClass(T::class, params)

于是,我们现在就可以写出跟上面的 Scala 代码有点像的 Kotlin 代码来:

when (foo) {
    in match<Bar>(any, "药丸") -> "后面有药丸"
    in match<Bar>("青果", any) -> { val (_, predicate) = foo; "青果" + predicate + "了" }
    else -> "啥都没有"
}

注意到,第二个 case 的代码比较恶心。这是因为 Scala 能够在 matching 的同时创建变量并赋值,而 Kotlin 再怎么 hack 也无法做到。幸而 Kotlin 还算支持 destructing。

当然,这样的写法有很多问题:

  • 用了反射,这样不仅慢,而且很脏
  • 编译器不会为你做类型检查,写出 match<Bar>(1, 2) 编译器也不会指出错误
  • 同样,exhaustiveness check 和 smart cast 就通通没法帮你了

所以,似乎我们还是应该期待 Kotlin 提供完整的 pattern matching 支持,才能发挥 sealed class 和 data class 的最大功力呢。

此文同时发布于 https://zhuanlan.zhihu.com/p/27050720

Different languages have different behaviors for a loop over a list that modifies the list itself:

JavaScript:


var l = [16]
l.forEach(function(x) {
    var p = x / 2
    if (l.indexOf(p) < 0) {
        l.push(p)
    }
})
l

[16, 8] is returned (results are the same using for-in loop). Newly-added item (8) is not iterated over. PHP acts in the similar way.

Python:

l = [16]
for x in l:
    p = x/2
    if p not in l:
        l.append(p)
l

[32, 16, 8, 4, 2, 1, 0] is returned. Newly-added items are iterated over. Ruby acts in the similar way.

Scala:

val l = MutableList(16)
for (x <- l) {
    val p = x/2
    if (!(l contains p)) { 
        l += p 
    }
}
l

A list of (32, 16, 8) is returned (as of Scala 2.10.4) (results are the same using .foreach). I cannot think of any reason that Scala does this.

:(

Scala does not provide anything like EnumSet in Java standard library. However, since Scala 2.10, Enumeration.ValueSet is providing toBitMask method. Therefore it is possible to store structures like MySQL's SET type in some database systems supporting only integers.

With Slick's lifted embedding, we can have it converting the structure to/from the integer type used in database automatically, utilizing MappedColumnType:

// Enumeration definition
object Weekday extends Enumeration {
    val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
    val mapping = MappedColumnType.base[ValueSet, Long](_.toBitMask(0), long => ValueSet.fromBitMask(Array(long))) // not necessarily to be defined here
}

// Table definition
class Timeslot(tag: Tag) extends Table[Weekday.ValueSet, Int](tag, "timeslot") {
    def weekday= column[Weekday.ValueSet]("weekday")(Weekday.mapping)
    def hour = column[Int]("hour")
    def * = (weekday, hour)
}

In this way, for example, the set of "Tuesday, Thursday and Saturday" will be represented by 42 in the database.

By the way, to convert a Enumeration.ValueSet it to a List of String, one can use:

(input: Weekday.ValueSet) => input.toList.map(_.toString)

To convert a List of String back to a Enumeration.ValueSet:

(input: List[String]) => Weekday.ValueSet(input.map(Weekday.withName):_*)