プログラミング言語Kotlinは、今年特にAndroiderの間で注目を集めました。
実際今月だけでも多くの方がKotlinに関する記事を書かれています(すごい!)。
Kotlinについて詳しく知りたい方は公式サイトを参照してください。
雰囲気を掴みたい方は「Android開発を受注したからKotlinをガッツリ使ってみたら最高だった」を、最新動向や込み入ったトピックに興味のある方はKotlin Advent Calendar 2015を見てください。
日本語による書籍ですとSoftware Designで一時期連載がありました。単行本は目下執筆中です。
さて、本題です。
このようにKotlinユーザは増えてきているようですが、Kotlinはまだ若いことと、(Javaと比較して)機能が多いだけあってイディオムやパターンめいたものが定着していないように感じます。
ごく基本的なイディオムについては公式サイトで示されているので、ここでは別視点というかもっと攻めたコードを紹介したいと思います。
これが正解だっ!というつもりはなく、あくまで提案なので意見があればどんどんコメントください!(もしくはSlackのKotlin日本語部屋で議論するのも面白いでしょう。)
nullはifでチェックすればいいんだよ
まずは簡単なトピックから始めましょう。
結論から言うと、if
でnull
チェックすることは悪いことではない、ということです。
少し長くなりますが、背景を。
Javaには値の有無を表現する型Optional
があります。
次のコードは避けたい書き方です。
// Java
final Optional<User> user = findUser();
if (user.isPresent()) {
show(user.get());
}
Optional#get
はKotlinで言う!!
と同じ操作だからです。
こう直すべきでしょう。
// Java
user.ifPresent(u -> show(u));
user.ifPresent(this::show); // この例の場合はこちらも可
KotlinにはJavaのOptional#ifPresent
と似たことができるlet
という拡張関数が提供されています。
val user: User? = findUser()
user?.let { show(it) }
user?.let(::show) // この例の場合はこちらも可
Javaの話のようにKotlinでもlet
を使いif
での条件分岐はNG!ということはありません。
何故ならKotlinは、if
でnull
でないことを確認した後は、その変数をNotNullとして扱えるからです。
if (user != null) {
show(user)
}
Javaのようにif
ブロック内でuser!!
のような危険な操作をする必要がないのです。
基本的にはlet
などを使った方が奇麗に記述できる場合が多いですが、例えば複数のNullable変数をNotNullとして扱いたいときにif
は重宝します。
// ifバージョン
if (firstName != null && lastName != null) {
save(firstName, lastName)
}
// letバージョン
firstName.let { first ->
lastName.let { last ->
save(first, last)
}
}
流れるようなインタフェース?
Builderパターンのようにメソッドをチェーンさせて値をセットさせていく、いわゆる「流れるようなインタフェース」をJavaのライブラリではよく目にします。
これをそのままKotlinで使っても、すごく奇麗なコードになります。
しかしKotlinの機能をもっと活用するように改良してみたいと思います。
名前付き引数
大抵の場合はこれで解決です。
data class Person(
val name: String,
val age: Int,
val address: String
)
val taro = Person(
name = "たろう",
age = 27,
address = "葛西"
)
Builderクラス
data class Person(
val name: String,
val age: Int,
val address: String
) {
class Builder {
var name: String? = null
var age: Int? = null
var address: String? = null
fun build(): Person =
Person(
name = requireNotNull(name),
age = requireNotNull(age),
address = requireNotNull(address)
)
}
companion object {
fun build(f: Builder.() -> Unit): Person {
val builder = Builder()
builder.f()
return builder.build()
}
}
}
val taro = Person.build {
name = "たろう"
age = 27
address = "葛西"
}
[Deprecated] 拡張関数の使用にあえて手間をかけさせる
Kotlinの特に面白い機能のひとつに拡張関数というものがあります。
既存の型にメソッドを生やすような機能です。
実際には既存の型を書き換えることはせず、静的に解決されるので混乱の種にはなりにくいかも知れません。
名前に関しても同じことが言えます。
例えばContext
に対してtoast
という拡張関数をパッケージfoo
とbar
でそれぞれ定義したとき、パッケージbaz
配下でtoast
を使用する際にはfoo
の実装を使うのか、それともbar
の実装を使うのかインポートを行って明示する必要があります。
package baz
import foo.toast
// その他のインポート文は省略
class MainActivity: Activity() {
override fun onResume() {
super.onResume()
toast("Hello")
}
}
ここで問題提起ですが、インポート文でどの実装を使っているかを明記するだけで十分でしょうか?
コンパイラ的にはなんの問題もないのですがプログラマにとってはわかりやすいコードになっているのでしょうか?
この答えを「インポート文はノイジーになりがちで、プログラマにはわかりにくい」ということにすると、どのような解決策があるでしょうか。
提案: インタフェースに拡張関数を定義してみる
package foo
interface FooToast {
fun Context.toast(msg: String) {
// トースト表示
}
}
package baz
import foo.FooToast
class MainActivity: Activity(), FooToast {
override fun onResume() {
super.onResume()
toast("Hello")
}
}
FooToast
の名前には改良の余地がありますが、こんな具合です。
拡張関数のtoast
の名前空間としてインタフェースを噛ませて、ちょっと手間はかかるけど、わかりやすくなっているように感じます。
カリー化
ここで主張したいことはこうです:
- 拡張関数は既存の型に対してメソッドを生やす
-
operator
付きinvoke
メソッドを持ったオブジェクトは、関数呼び出しのようなシンタックスシュガーを持つ(obj.invoke()
はobj()
と同じ) - 関数にも型がある
これらを組み合わせるとDSLを開発するときに役立つでしょう。
DSLとは違いますが、例えば関数のカリーは次のコードのように表現できます。
operator fun <A, B, C> ((A, B) -> C).invoke(a: A): (B) -> C = { a(it) }
val minus = fun(a: Int, b: Int): Int = a - b
minus(5, 3) //=> 2
minus(5)(3) //=> 2
1行目では型(A, B) -> C
すなわち2引数関数に対してinvoke
という名前(そして引数はひとつ)のメソッドを生やしています。
これにより関数がデフォルトでカリー化されているように見えるわけです。
!!
を避ける (2016-01-03追記)
あるNullable値をNotNullに変換したい場合、たいていはif
によるチェックかlet
拡張関数を使えば事足ります。しかし、Nullable値がNotNullに仕様上必ず変換可能であるべきシーンがあります。そんなとき、Nullable→NotNullの強制変換演算子である!!
を使うことは避けるべきです。代わりにrequireNotNull
関数か、エルビス演算子 + 例外スローを使いましょう。NotNullに変換する理由をコードで明示的に表現するというわけです。
// !!を使う場合 ↓のようにコメントは欠かせない
// ここでuserは絶対にNotNullである
save(user!!)
// requireNotNullを使う場合
save(requireNotNull(user, {"userはNotNullであるべきなので何かがおかしい"}))
// エルビス演算子 + 例外スロー(実際の例外クラスは状況に応じて選択)
save(user ?: throw IllegalStateException("何かがおかしい"))
特定のコンテキスト内ではたらく拡張関数 (2016-02-02追記)
先日のAndroidでKotlin勉強会 @ Sansanで発表したスライドです↓
拡張関数って、当たり前ですがレシーバが1つだけなのが不便ですよね。
しかし2つの型に依存したような拡張関数を作れますよ、というのがこの発表の内容です。
アプローチとしては、インタフェース内で拡張関数を定義して、それをimplementsする感じです。
もう1つ別のアプローチを見つけました。
それは、「拡張関数を返す拡張プロパティ」を定義することです(double context extensionパターンと勝手に命名)。
詳細はこちらのエントリを参照。
おわりに
いかがでしたか?
なるほどと思えるものがあったら嬉しいです。
反論や改善案があったらぜひコメントしてください。
Kotlinに興味を持っていただけたら、来年1月にあるAndroidでKotlin勉強会 @ Sansanや2月にあるDroidKaigi 2016に参加してみてください!