2017年5月

昨晚上 YD 说他的妹子因为做不出作业,而 YD 又不帮她做,而哭了。

我忽然想到,YD 平时大抵对她很好,才会宠得她这样吧。

这么想来,我对妹子确实是很糟糕呢。

突然有些难过。

在 2016 年的 4 月 15 日,我在这个博客里写下了提纲,举了我应该尽快从 ofo 辞职的理由。原文是:

  • 缺乏对细节的关注和对格调的追求。
  • 技术负责人水平不够,并且太过实用主义。
  • 不透明。
  • Hiring / firing 太过随意。

然而直到 2017 年的 4 月,整整一年后,我才真正辞职。尽管这一年里,ofo 的估值和规模都极大地上涨,但是,这些问题几乎没有好转,有的甚至愈演愈烈。

每个公司,有些骨子里东西并不是能够轻易改变的。至少,很难变好。

这篇文章依旧没有什么主题、没有什么深度,纯碎随笔,谈的也是 Kotlin 最最基础的问题。

我常常说 Kotlin 比 Swift 舒服的一点是,Kotlin 在 null-safety 问题上,会有所谓「smart cast」。例如你有

var a: Foo? = getFoo()
if (a != null) {
  print(a.foo) // 不再需要 !!
}

而在 Swift 里,你仍然需要手动再次「!」

var a: Foo? = getFoo()
if (a != null) {
  print(a!.foo) // 仍然需要 !
}

当然所有人都会很快注意到的一点是,Kotlin 对于局部变量可以做这样的处理,但如果是个类的成员变量(var),就不行:

class Bar {
  var a: Foo? = getFoo()
  fun bar() {
    if (a != null) {
      print(a.foo) // 编译无法通过
    }
  }
}

为什么 Kotlin 此时不予编译通过呢?因为在当前线程判断 a != null 之后,其它线程可能又修改了「a」的值。事实上,如果是用 val 声明的 field,此时就可以 smart cast。
而如果是 var 声明的呢?如果你加上 !!,变成 a!!.foo,就可以编译通过,但这么做的话,你就会被抓住游街。所以,正确的解决方式是,定义一个局部变量,这样就确保没有其它线程能动到它。

fun bar() {
  val localA = a
  if (localA != null) {
    print(localA.foo) // cool
  }
}

这样写的唯一问题,就是丑。定义了一个可能只用到一次的局部变量,而它并不被整个方法所需要,而仅仅只是临时用到而已。大约 Swift 程序员们也意识到了这一点,所以 Swift 里习惯的 practice 是:(摘自官方教程)

if let roomCount = john.residence?.numberOfRooms {
  print("John's residence has \(roomCount) room(s).")
} else {
  print("Unable to retrieve the number of rooms.")
}

也就是,为了写得更简明一些,Swift 从语法层面增加了 if let 语句解决问题。

Kotlin 里,当然也可以利用 let 来写得漂亮些。当然,Kotlin 的 DSL 能力那么好,何必再从语法层面解决问题?

print(john.residence?.numberOfRooms?.let { "John's residence has $it room(s)." } ?: "Unable to retrieve the number of rooms.")

一气呵成,不需要任何额外的语法。事实上,我实际写代码时,也经常会 foo?.let { … } 这么用。

上面语句的核心就是 let。和 Swift 不同,Kotlin 里的 let 不过只是一个普通的函数而已。事实上,很多 Kotlin 教程都不会提及标准库里这几个很赞的函数。它们实现得非常简单,但却可以作为良好 Kotlin 代码的典范:

public inline fun <T, R> T.run(block: T.() -> R): R = block()
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null

熟悉它们,能够帮你大大地缩短代码。

例如某个方法,在计算出返回值后,还需要做一些清理,之前你可能:

fun foo(): Int {
  val resource = getResource()
  val retVal = resource.calculate()
  resource.cleanUp()
  return retVal
}

而现在,仅靠标准库里的这几个方法,你就可以:

fun foo() = getResource().run { calculate().also { cleanUp() } }

标准库里的这几个方法仅仅是最最常用和最简单的对控制语句的补充。其实还有很多这样的扩展方法可以用,例如 funKTionale 里面的函数,都很棒呢。

v2-c72a92a564d55dd210ef2e640d0bd005_b.jpg

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

我注意到在我前些天的一篇回答的评论里,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

刚刚看 I/O 直播时听到 Kotlin gets officially supported,真的蛮意外的。

Kotlin 的 killer-app 大约就是用来写 Android app 的 Anko。从我初次用 Anko 到现在,已经过去快一年了。遗憾的是在之前的公司里,由于各种各样的原因,一直没法在线上产品中使用 Kotlin。

直到我上个月换了工作后——刚刚结束的四月里,使用 Anko,在不到一个月里就写完了两个漂亮的 Android App 并上线,生产力和舒适度实在前所未有。

当然,由于我很长时间里的主要在写 JavaScript,于是确实习惯了使用 Promise。我于是就简短地用 Kotlin 写了一个 Promise 实现—— https://github.com/kmxz/Votive/。API 尽可能地接近 JavaScript 中的 Promise;只是所有 Promise 的名称都改成了 Votive。

譬如,将 Android 的运行时权限申请封装为 Promise,只需要这么一点 boilerplating:

const val REQUEST_CODE_MIN = 6910
const val REQUEST_CODE_MAX = 7910

open class BaseActivity: AppCompatActivity() {

    private var lastRequestCode = REQUEST_CODE_MIN

    private var permissionCallback = mutableMapOf<Int, Pair<(Unit) -> Unit, ((Unit) -> Unit)>>()

    fun withPermission(permission: String) = Votive<Unit, Unit> { resolve, reject ->
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                val requestCode = lastRequestCode++
                if (lastRequestCode > REQUEST_CODE_MAX) { lastRequestCode = REQUEST_CODE_MIN }
                permissionCallbacks.put(requestCode, Pair(resolve, reject))
                ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
            } else {
                resolve(Unit)
            }
        } else {
            resolve(Unit)
        }
    }

    final override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode in REQUEST_CODE_MIN..REQUEST_CODE_MAX) {
            val callback = permissionCallbacks.remove(requestCode)
            if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
                callback?.first?.invoke(Unit)
            } else { // already removed anyway
                callback?.second?.invoke(Unit)
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }

}

就可以使用 Promise 风格去申请运行时权限:

withPermission(Manifest.permission.ACCESS_FINE_LOCATION)
    .thenSimple { Log.i(locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)) }
    .catchSimple { alert("Don't give me permission? I'll crash") }

嗯,以及我现在越来越觉得我一个月前辞职实在是绝赞的决定。

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