スコープ関数の使いどころ(Kotlin)

## はじめに

Kotlinには「スコープ関数（Scope-Function）」という関数があります。
いくつか種類がありますが、どの関数も似ていて使い分けや使いどころがわからなかったのでまとめました。

## 注意

私は業務でKotlinを使っておらず、iOSアプリ開発でSwift（スコープ関数がない言語）を使っています。
そのため、Kotlinをメインで使っている人とは考え方が異なると思います。

スコープ関数を使い慣れていないため、考え方が変わることも大いにありえます。

## 前提条件

本記事を読むための前提条件です。

- Kotlinのスコープ関数を理解している  
理解していない場合、[公式ドキュメント](https://kotlinlang.org/docs/reference/scope-functions.html)をご参照ください

## 環境

- Kotlin：1.3.61

## 結論

まず私の結論を述べます。

- `with` ・ `run` ・ `apply` は使わない
- nullableな変数を処理したい
    - 戻り値を使う → `let` を使う
    - 戻り値を使わない
      - スマートキャストできる（= 変数が不変） → `if` を使う
      - スマートキャストできない（= 変数が可変） → `let` を使う
- 変数のプロパティを変更したい・メソッドを実行したい
    - 戻り値を使う → `also` を使う
    - 戻り値を使わない → スコープ関数を使わない

ざっくりいうと、「できる限り可読性を損なわず、スコープ関数のメリットである「前の処理からメソッドチェーンで繋げられる（with以外）」「戻り値がある（メソッドチェーンで後ろに処理を繋げられる）」を活かせる場面で使う」ということです。

## `with` ・ `run` ・ `apply` を使わない理由

`with` ・ `run` ・ `apply` を使わない理由を説明します。

### `with` は他のスコープ関数と統一性がないので使わない

`with` でできることはすべて `run` でもできます。（後述しますが、私は `run` も使いません）
逆に、 `with` は拡張関数ではないので `run` のようにメソッドチェーンすることができません。

```kotlin:×withはメソッドチェーンできない
var holderView = getHolderView()
with(holderView) {
    tag = item
    setOnClickListener(mOnClickListener)
}
```

```kotlin:○runはメソッドチェーンできる
getHolderView().run {
    tag = item
    setOnClickListener(mOnClickListener)
}
```

他のスコープ関数と使い方を統一するためにも、拡張関数ではない `with` は使わないようにします。

### `run` ・ `apply` は `this.` の省略がわかりづらいので使わない

私がKotlinに慣れていないのもありますが、 `this.` が省略されていると処理が理解しづらいです。

```kotlin:×runはthisを省略できる
holder.view.run {
    tag = item // `tag` って何？
    setOnClickListener(mOnClickListener)
}
```

```kotlin:○letはitを省略できない
holder.view.let {
    it.tag = item
    it.setOnClickListener(mOnClickListener)
}
```

`this.` を省略した場合、同名の変数が追加されると意図しない動作に変わるので、不具合の温床にしないためにも `run` ・ `apply` は使わないようにします。

```kotlin:×thisの省略は不具合の温床になる
var tag = 1 // 変数を追加
holder.view.run {
    tag = item // 変数で定義した `tag` に代入される
    setOnClickListener(mOnClickListener)
}
```

`this` と異なり、 `it` だと別名を付けられるのもメリットです。

```kotlin:○itは別名を付けられる
getMock(type)?.let { mock ->
    clearer.clear(arrayOf(mock), options)
}
```

## `let` ・ `also` の使いどころ

消去法で残った `let` ・ `also` の使いどころを紹介します。

### `let` はnullableな変数に使う

~~Kotlinには三項演算子がなく、nullかどうかで戻り値を切り替える場合には `let` が有用です。
エルビス演算子（ `?:` ）と併用することで、スマートに記述できます。~~
[Twitterで教えていただいた](https://twitter.com/Inb_J/status/1250639913973776384?s=20)のですが、 `let` は最後に評価した値が戻り値になるため、エルビス演算子と併用すべきではなさそうです。
おとなしくif-elseを使うのが無難です。

```kotlin:○if-elseによるスマートキャストでも悪くはない
var foo: Int? = 3
val bar = if(foo != null) foo * 2 else 0 // 6
```

```kotlin:△letを使うとスマートに書けるが、ラムダ結果がnullになるとエルビス演算子側で処理されてしまう
var foo: Int? = 3
val bar = foo?.let { it * 2 } ?: 0 // 6

// `it.getInt()` が `null` の場合、 `0` が返ってしまう
hoge?.let { it.getInt() } ?: 0
```

ifと異なりメソッドチェーンが使えるので、処理をより短く記述できることがあります。

```kotlin:△if文だと変数に代入する必要がある
override fun clear(type: KClass<*>, options: MockKGateway.ClearOptions) {
    val mock = getMock(type)
    if (mock != null) {
        clearer.clear(arrayOf(mock), options)
    }
}
```

```kotlin:○letを使うと短く書ける
override fun clear(type: KClass<*>, options: MockKGateway.ClearOptions) {
    getMock(type)?.let {
        clearer.clear(arrayOf(it), options)
    }
}
```

引用：https://github.com/mockk/mockk/blob/0dd309232fc75906dc52e75c3f0dc89d9f2cc63e/mockk/jvm/src/main/kotlin/io/mockk/impl/instantiation/JvmConstructorMockFactory.kt#L185-L189

ただ、こちらの例はスコープ関数の戻り値を使っていないため、個人的には冗長でも変数に代入してif文で記述したいです。
どうしても戻り値を使わずにスコープ関数を使いたい場合、個人的には自身を返す `also` を使いたいですが、 __`let` の方がなんとなくnullableの変数を扱う感がある__ ので、このままとします。

変数が可変である（= `var` で定義されている）場合、スマートキャストが使えません。
その場合、戻り値を使わなくても `let` を使うのが有用です。

```kotlin:×変数が可変だとスマートキャストできずにビルドエラーとなる
// `arguments` が可変のため、以下のビルドエラーが発生する
// Smart cast to 'Bundle' is impossible, because 'arguments' is a mutable property that could have been changed by this time
if (arguments != null) {
    columnCount = arguments.getInt(ARG_COLUMN_COUNT)
}
```

```kotlin:○letを使うとビルドエラーにならない
arguments?.let {
    columnCount = it.getInt(ARG_COLUMN_COUNT)
}
```

ただし、変数が不変（= `val` で定義されている）場合はスマートキャストを使いましょう。
そうすることで、戻り値なしで `let` が使われていると、「この変数は可変なんだな」と推測できるようになります。

### `also` はプロパティの変更やメソッドの実行に使う

`also` はメソッドチェーンによるプロパティの変更やメソッドの実行に使います。

```kotlin:△スコープ関数を使わないと冗長になる
val ret = obj
    .javaClass
    .getDeclaredField("ret")
ret.isAccessible = true
ret.get(obj)
```

```kotlin:○alsoを使うとメソッドチェーンで書ける
obj
    .javaClass
    .getDeclaredField("ret")
    .also { it.isAccessible = true }
    .get(obj)
```

引用：https://github.com/mockk/mockk/blob/b0fe0ad2284ab3677a8eb477a68b8c6f28a3af32/mockk/jvm/src/test/kotlin/io/mockk/gh/Issue149Test.kt#L38-L42

`let` でもいいましたが、戻り値を使わない場合、個人的にはスコープ関数を使いたくありません。
スコープ関数に慣れていない人が少しでも読みやすいようにしたいです。

```kotlin:△ある変数に対する処理がまとまっていて悪くはない
holder.view.also {
    it.tag = item
    it.setOnClickListener(mOnClickListener)
}
```

```kotlin:○でも戻り値を使わないなら他の言語と同じ書き方で十分だと思う
holder.view.tag = item
holder.view.setOnClickListener(mOnClickListener)
```

ある変数に対する処理をまとめる場合、変数の初期化から始めると戻り値が使われ、処理がまとまって可読性も上がると思います。

```kotlin:○変数の初期化から処理をまとめると読みやすい
val button = Button(this).also {
    it.text = "Foo"
    it.setOnClickListener { 
        startActivity(Intent(this, NextActivity::class.java))
    }
}
```

参考：https://qiita.com/SYABU555/items/c6d828b4c29c545a58f3#tips

SwiftのInitialization Closureみたいで読みやすいです。

## おまけ：Swiftにスコープ関数があったらどうか？

私が有用だと考えている `let` と `also` について、Swiftにもあったら便利でしょうか。
`let` はif-letや三項演算子があるので不要そうですが、 `also` は有用だと感じます。

[Twitterで教えていただいた](https://twitter.com/takasek/status/1250655013858238471?s=20)のですが、[Then](https://github.com/devxoul/Then)というライブラリを使えば、Swiftでも `also` と同様のことを実現できます。

```swift:InitializationClosureを使う
let label: UILabel = {
  let label = UILabel()
  label.textAlignment = .center
  label.textColor = .black
  label.text = "Hello, World!"
  return label
}()
```

```swift:Thenを使うとalsoと同じように書ける
let label = UILabel().then {
  $0.textAlignment = .center
  $0.textColor = .black
  $0.text = "Hello, World!"
}
```

Builderパターンと似ていますが、インスタンスの初期化時以外にも使える点が異なります。

## おわりに

おそらくかなり偏った考えだと思います。
みなさんのスコープ関数の使いどころもぜひ教えてください！

## 参考リンク

- [Scope Functions - Kotlin Programming Language](https://kotlinlang.org/docs/reference/scope-functions.html)
- https://twitter.com/kotlin/status/1425088549075001354?s=20
- [Kotlin スコープ関数 用途まとめ - Qiita](https://qiita.com/ngsw_taro/items/d29e3080d9fc8a38691e)
- [Kotlinのスコープ関数の定義を読み解く - Qiita](https://qiita.com/wiroha/items/1768ed13c4ab2a322959)
- [Kotlin スコープ関数の使い分け - Qiita](https://qiita.com/yasuX/items/20b6354d42b19879a55e)
- [Kotlinのif let - Qiita](https://qiita.com/mattak/items/f817fd4c83d18454cd9e)
- [Kotlinアンチパターン](https://www.slideshare.net/RecruitLifestyle/kotlin-87339759/23)  
p.23-31
- [ウホーイさんはTwitterを使っています 「スコープ関数のメリットって以下の2つが大きいと思う ・メソッドチェーンできる（with以外） ・戻り値がある 実は↓は上2つのメリットのおまけに過ぎないかもしれない ・複数の処理を実行できる」 / Twitter](https://twitter.com/the_uhooi/status/1250666199488139264?s=20)