■ はじめに
Kotlin でプログラムを書いていると、以下のようなコードを書くことがあるが、
list.forEach {
it.doSomething()
}
下記のように書けたら嬉しいと思うことがある。
list.forEach {
doSomething() // list の要素を lambda のスコープにしたい
}
標準ライブラリに欲しいと思いググってみたら、forEach with elements as the receivers という形で2018年に議論が開始され、未だ答えが出ていない。
■ ネットでの議論
ネットでの議論では、スコープ関数群との一貫性を保ちつつ妥当な命名を行うという方向で試行錯誤しているように見受けられる。
- forEachApply, onEach, withEach, runEach, eachRun, applyOnEach, runForEach, withEach, mapEach, asEach, toEach などの提案。
- 言語仕様レベルの変更案
※ onEach は Kotlin 1.1 にて別の機能が割り当てられた。https://kotlinlang.org/docs/whatsnew11.html#oneach
■ 既存のスコープ関数
スコープ関数に関しては、本家の Function selection に説明がある。
■ 新機能を視野に入れた情報整理
☆ スコープ関数
既存のスコープ関数のうち拡張関数のみに対して、以下の視点を追加してみた。
- 戻り値が Unit の場合
- レシーバーが lambda 内の 'this' になるかどうか。
- レシーバーが lambda の引数になるかどうか。
+----------------------------------------------------------+
| receiver is 'this' in lambda / receiver is arg of lambda |
+----------------------------------------------------------+
| yes / no | no / yes | yes / yes | no / no |
+-------------+----------------+------------------+----------+-----------+----------------+
|return value | context object | apply | also | *1 | *2 |
| | lambda result | run | let | *1 | *2 |
| | Unit | *3 | *3 | *3 | *2, *3 |
+-------------+----------------+------------------+----------+-----------+----------------+
*1 は、receiver が this となった状態で、closure のスコープの this を it で参照できるのはちょっとうれしいかも。以下例:
DataBindingUtil.setContentView<HogeBinding>(this, R.layout.hoge).apply {
lifecycleOwner = this@ArtistReviewToMeActivity // ← これがめんどくさい。
}
DataBindingUtil.setContentView<HogeBinding>(this, R.layout.hoge).hoge {
lifecycleOwner = it // ← これだとらくちん。
}
*2 は、スコープも変わらず引数も無しなので、スコープ関数にする必要がない。
*3 は、戻り値を使わなければいいだけなので、無くても実装上は困らない。(けど、戻り値を返さないという意思表示になるので保守性は上がるかも)
☆ Iterable に対する関数
Iterable に対する関数としては、下記のようになっていると思われる。
+----------------------------------------------------------+
| receiver is 'this' in lambda / receiver is arg of lambda |
+------------------+----------+-----------+----------------+
| yes / no | no / yes | yes / yes | no / no |
+-------------+----------------+------------------+----------+-----------+----------------+
|return value | context object | *6 | onEach | *5 | *6 |
| | lambda result | *7 | map | *5 | *6 |
| | Unit | *4 | forEach | *5 | *6 |
+-------------+----------------+------------------+----------+-----------+----------------+
*4 が今回欲しい『引数無しで、this が receiver で、Unit を返す関数』。
*5 は *1 と同様であったら便利なケースもある気がする。
■ 新機能をどう作ればいいのか考えてみる
- 既存のライブラリ関数はそのまま受け入れるしかない。
- 既存のライブラリ関数に対して命名の一貫性を保つべき。
- 『引数無しで、this が receiver で、Unit を返す関数』は、表の *4 の位置に配置されるので、言葉として apply や run を利用することはできない。
- 現状のスコープ関数に Unit を返すものがないので、Iterable に対する関数についても Unit を返すことにこだわらなくてもいいのでは?また、*4 は既存の知識から推測できないという点で微妙。
- だったら、*6 と *7 の位置に、applyEach と runEach を作ればいいんじゃね?
■ 実装例
inline fun <T> Iterable<T>.applyEach(action: T.() -> Unit): Iterable<T> =
onEach { it.action() }
inline fun <T, R> Iterable<T>.runEach(transform: T.() -> R): List<R> =
map { it.transform() }
■ おわりに
結局『引数無しで、this が receiver で、Unit を返す関数』は作らずに applyEach と runEach を作るという斜め上の展開となってしまいました。
forEach with elements as the receivers は、既存のライブラリの概念をそのまま使おうとするかぎりは結論が出ることはないと思う、、、。1
生きてればそういうこともあるよ。
おわり。
-
なぜなら既存のスコープ関数に
Unitを返すものがないのだから。 ↩

