48
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

エムスリーAdvent Calendar 2015

Day 20

QOLうなぎ登りなKotlin Tips提案

Last updated at Posted at 2015-12-20

プログラミング言語Kotlinは、今年特にAndroiderの間で注目を集めました。
実際今月だけでも多くの方がKotlinに関する記事を書かれています(すごい!)。

Kotlinについて詳しく知りたい方は公式サイトを参照してください。
雰囲気を掴みたい方は「Android開発を受注したからKotlinをガッツリ使ってみたら最高だった」を、最新動向や込み入ったトピックに興味のある方はKotlin Advent Calendar 2015を見てください。
日本語による書籍ですとSoftware Designで一時期連載がありました。単行本は目下執筆中です。

さて、本題です。
このようにKotlinユーザは増えてきているようですが、Kotlinはまだ若いことと、(Javaと比較して)機能が多いだけあってイディオムやパターンめいたものが定着していないように感じます。
ごく基本的なイディオムについては公式サイトで示されているので、ここでは別視点というかもっと攻めたコードを紹介したいと思います。
これが正解だっ!というつもりはなく、あくまで提案なので意見があればどんどんコメントください!(もしくはSlackのKotlin日本語部屋で議論するのも面白いでしょう。)

nullはifでチェックすればいいんだよ

まずは簡単なトピックから始めましょう。
結論から言うと、ifnullチェックすることは悪いことではない、ということです。

少し長くなりますが、背景を。
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は、ifnullでないことを確認した後は、その変数を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という拡張関数をパッケージfoobarでそれぞれ定義したとき、パッケージ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に参加してみてください!

48
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?