9ヶ月ほどサーバーサイド Kotlin を書いて過ごしていましたが、そろそろ Scala の仕事に戻れそうなので、備忘録として両者の書き味の違いを残して置こうと思います。
Collection とか Iterable 周りの関数とか書き方を中心に書きます。
実行環境など
Kotlin をラフに書いて試したい場合は、IntelliJ の Kotlin REPL を使うのが良さそうです。今回実行したバージョンはこちら。
Welcome to Kotlin version 1.4.21 (JRE 13.0.1+9)
Scala の場合はscala
コマンドかsbt console
を使うと思います。今回実行したバージョンはこちら。
Welcome to Scala 2.13.5 (OpenJDK 64-Bit Server VM, Java 15.0.2).
Collections の関数
Scala には同等の関数がなさそうなやつ
Kotlin の関数を調べると、今のところ Scala には定義されていない便利そうな関数がたくさんあって面白かったので、Scala を使った場合の同等な処理と書き比べる形で紹介しようと思います。
val ints: List<Int> = (1..3).toList()
val ints: Seq[Int] = 1 to 3
コード例にはこちらの変数を使います。
associate(associateWith, associateBy)
Scala では配列からMap
を作るときTuple2
+ toMap
をすると思います。(キーが被らない保証があるとき等)
ints.map(i => i -> s"user$i").toMap
// val res0: scala.collection.immutable.Map[Int,String] = Map(1 -> user1, 2 -> user2, 3 -> user3)
Kotlin ならassociate
という関数が用意されています。
ints.associate { it to "user$it" }
// res0: kotlin.collections.Map<kotlin.Int, kotlin.String> = {1=user1, 2=user2, 3=user3}
なおassociateWith
, associateBy
とかを使うとシンプルに書けることがあります。
filterIsInstance
Scala では型でフィルタリングする際は、collect
を使って型のパターンマッチをして
Seq(List(1), Set(2), Map(3 -> 4)).collect { case xs: Set[Int] => xs }
// val res1: Seq[Set[Int]] = List(Set(2))
のように書くと思います。少しトリッキーな書き方になってしまいますよね。
Kotlin ならfilterIsInstance
という関数が用意されています。
listOf(listOf(1), setOf(2), mapOf(3 to 4)).filterIsInstance<Set<Int>>()
// res1: kotlin.collections.List<kotlin.collections.Set<kotlin.Int>> = [[2]]
groupingBy
Kotlin には Scala と同じ様にgroupBy
関数も用意されていますが、似たような名前のgroupingBy
という関数も存在します。この関数の返り値はGrouping
型という集計処理を行うための中間型になっていて、例えばgroupby
+ count
のような処理をしたい場合は、以下のように書く事ができます。
ints.groupingBy { it % 2 == 0 }.eachCount()
// res2: kotlin.collections.Map<kotlin.Boolean, kotlin.Int> = {false=2, true=1}
Scala で同等のことがしたい場合はgroupBy
+ mapValues
, またはgroupMapReduce
を使って
ints.groupBy(_ % 2 == 0).view.mapValues(_.size).toMap
// val res2: scala.collection.immutable.Map[Boolean,Int] = Map(false -> 2, true -> 1)
ints.groupMapReduce(_ % 2 == 0)(_ => 1)(_ + _)
// val res3: scala.collection.immutable.Map[Boolean,Int] = Map(false -> 2, true -> 1)
という風に書く必要があります。少し冗長になるのと、やってることがなんか少し分かりづらそうですよね。(あと遅そう)
Grouping
型を使いこなせたらスマートに書ける処理がたくさんありそうですね。
none
Scala では「条件に合致するものが一個もない」と判定したい場合exists
を反転して
!ints.exists(_ % 2 == 0)
// val res4: Boolean = false
などとやると思います。
Kotlin の場合、none
という関数が用意されています。
ints.none { it % 2 == 0 }
// res4: kotlin.Boolean = false
union
Scala にも同名の関数がありますが、deprecated になっているのと、処理内容が違うので注意が必要です。Kotlin のunion
は配列を合体させて一意にしたものが返されます。
ints.union(2..5)
// res5: kotlin.collections.Set<kotlin.Int> = [1, 2, 3, 4, 5]
同じ様な処理を Scala で書きたい場合、concat
+ toSet
するとそれっぽくなると思います。
ints.concat(2 to 5).toSet
// val res5: scala.collection.immutable.Set[Int] = HashSet(5, 1, 2, 3, 4)
takeLastWhile(dropLastWhile)
Scala の場合、配列の逆から takeWhile
するような関数がないので、やろうとすると少しブサイクですが、reverse
してreverse
する感じで
ints.reverse.takeWhile(1 < _).reverse
// val res6: Seq[Int] = Range 2 to 3
この様に書きますかね。(もう少しなんとかなりそうな気はするものの)
Kotlin には**LastWhile
という関数が用意されています。
ints.takeLastWhile { 1 < it }
// res6: kotlin.collections.List<kotlin.Int> = [2, 3]
zipWithNext
Scala では配列の次の要素とzip
したい場合tail
+ zip
で書きますよね。
ints.zip(ints.tail)
// val res7: Seq[(Int, Int)] = Vector((1,2), (2,3))
Kotlin にはzipWithNext
という関数が用意されています。
ints.zipWithNext()
// res7: kotlin.collections.List<kotlin.Pair<kotlin.Int, kotlin.Int>> = [(1, 2), (2, 3)]
average
Scala では数値の平均を出すにはDouble
にして配列のサイズで割ると思います。
ints.sum.toDouble / ints.length
// val res8: Double = 2.0
Kotlin にはaverage
という関数が用意されています。
ints.average()
// res8: kotlin.Double = 2.0
mapIndexed(filterIndexed, foldIndexed...)
Scala では index 値を使って配列を操作したい場合zipWithIndex
と組み合わせて書くと思います。
ints.zipWithIndex.map { case (index, i) => s"$i-$index" }
// val res9: Seq[String] = Vector(0-1, 1-2, 2-3)
Kotlin には**Indexed
という合成済みの関数が用意されています。
ints.mapIndexed { index, i -> "$i-$index" }
// res9: kotlin.collections.List<kotlin.String> = [1-0, 2-1, 3-2]
sortedDescending
Scala では逆順にソートする場合sorted.reverse
もしくはパフォーマンスが気になる場合は
ints.sorted(Ordering[Int].reverse)
// val res10: Seq[Int] = Vector(3, 2, 1)
このように書いたりすると思います。
Kotlin にはsortedDescending
という関数が用意されています。
ints.sortedDescending()
// res10: kotlin.collections.List<kotlin.Int> = [3, 2, 1]
maxOf(minOf, sumOf...)
Scala ではmap
した結果のmax
をとる処理を書く場合、文字通り以下のように書きます。
ints.map(i => i * i % 5).max
// val res11: Int = 4
Kotlin には**Of
という関数が用意されています。
ints.maxOf { it * it % 5 }
// res11: kotlin.Int = 4
runningReduce
Scala で「初期値を与えないscan
」みたいなことをしたいとき、僕だったら配列が空か判断してから以下のようにやるかなと思います。
ints.tail.scan(ints.head)(_ + _)
// val res12: Seq[Int] = Vector(1, 3, 6)
Kotlin では以下のように書けます。
ints.runningReduce { acc, i -> acc + i }
// res12: kotlin.collections.List<kotlin.Int> = [1, 3, 6]
scan
を使ってて「累積和の最初の 0 いらないなぁ」ってtail
したことある気がするので、そんなときこれを使うと便利かもしれないですね。
Scala と同等な関数があるけど別名のやつ
他の言語間でもそうだと思いますが、同じ処理なのに関数名が違うものが色々ありますので紹介します。
all, any
配列の要素に全部に対して判定をする様な関数で、Scala では、forall
, exists
に対応する関数が、all
, any
になります。上の方で紹介したnone
もこの関数と同じ括り。
ints.all { it % 2 == 0 }
// res0: kotlin.Boolean = false
ints.any { it % 2 == 0 }
// res1: kotlin.Boolean = true
ints.forall(_ % 2 == 0)
// val res0: Boolean = false
ints.exists(_ % 2 == 0)
// val res1: Boolean = true
intersect, subtract
文字通り、積集合、差集合を作ることのできる関数で、Scala では、intersect
, diff
が対応します。intersect
は同じなのに、差集合は違う名前なんですね。上で紹介したunion
もこの関数と同じ括り。
Kotlin の方は返り値の型がSet
になるようなので、元の情報が消えてしまうことがある気がします。
ints.intersect(2..4)
// res2: kotlin.collections.Set<kotlin.Int> = [2, 3]
ints.subtract(2..4)
// res3: kotlin.collections.Set<kotlin.Int> = [1]
ints.intersect(2 to 5)
// val res2: Seq[Int] = Vector(2, 3)
ints.diff(2 to 5)
// val res3: Seq[Int] = Vector(1)
windowed, chunked
Scala ではsliding
, grouped
という関数がありますが、それに対応する関数が Kotlin では windowed
, chunked
です。(返り値の型とか、grouped
はslidning
の一般化した関数ではないとか色々あると思いますが、やれることは一緒)
配列をいい感じに分割して(二次元配列にして)返す処理が書けます。
ints.grouped(2).toList
// val res4: List[Seq[Int]] = List(Range 1 to 2, Range 3 to 3)
ints.sliding(2, 2).toList
// val res5: List[Seq[Int]] = List(ArraySeq(1, 2), ArraySeq(3))
ints.chunked(2)
// res4: kotlin.collections.List<kotlin.collections.List<kotlin.Int>> = [[1, 2], [3]]
ints.windowed(2, 2, true)
// res5: kotlin.collections.List<kotlin.collections.List<kotlin.Int>> = [[1, 2], [3]]
windowed
は、第三引数のpartialWindows: Boolean
をfalse
にすると、size に足りない要素は消してくれる機能付きです。(デフォルトがfalse
なので注意が必要です)
ints.windowed(2, 2)
// res6: kotlin.collections.List<kotlin.collections.List<kotlin.Int>> = [[1, 2]]
runningFold
これはscan
そのものです。Kotlin にもscan
関数が用意されているんですが、それもrunningFold
を呼び出す形で定義されています。
public inline fun <T, R> Iterable<T>.scan(initial: R, operation: (acc: R, T) -> R): List<R> {
return runningFold(initial, operation)
}
ints.runningFold(10) { acc, i -> acc + i }
// res7: kotlin.collections.List<kotlin.Int> = [10, 11, 13, 16]
ints.scan(10)(_ + _)
// val res7: Seq[Int] = Vector(10, 11, 13, 16)
in, !in
こちらは関数名ではなくcontains
の糖衣構文ですが Scala では出来ない書き方なので紹介します。
2 in ints
// res8: kotlin.Boolean = true
2 !in ints
// res9: kotlin.Boolean = false
ints.contains(2)
// val res8: Boolean = true
!ints.contains(2)
// val res9: Boolean = false
[2021/05/04追記ここから]
Scala と同名だけど注意が必要なやつ
map, filter, flatMap...
がくぞさんにお教えいただいたので、追記します。
KotlinはSet等でmap/filterしてもListになりますが(なので別途mapToやfilterToがあります)
灯台下暗しという感じでした。。map
などで型が変わらない方が普通、みたいな感覚があったので全然意識外でした。試してみると、
Set(1, 2, 3).map(_ % 2)
// val res0: scala.collection.immutable.Set[Int] = Set(1, 0)
setOf(1, 2, 3).map { it % 2 }
// res0: kotlin.collections.List<kotlin.Int> = [1, 0, 1]
Scala の場合はSet
のままだけど、Kotlin の場合はList
になっています。
fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>
定義を見るとそりゃそうですがList
を返すようになっていますね。他の配列を返す標準関数もだいたいList
を返す様に出来ているので、注意が必要そうです。注意が必要そうなことの一例でいうと、ものすごく巨大な配列に対して操作したあとに結果としてSet
が欲しい場合など、
hugeSet.map { it % 2 }.toSet()
とやると、List
からSet
へのコンバート処理にすごいコストが掛かって、めちゃ遅いということが起きると思います。
ですのでそういうときは mutable にまわせるようにしたいというニーズに答えられるように**To
のような、第一引数にMutableCollection
を受け取る別バージョンも用意している、ということかと思いました。
[2021/05/04追記ここまで]
null 系関数
Scala と Kotlin ではnull
の扱いが違っており Scala ではOption
型として表現されるのに対して、Kotlin では各型に対して?
というnull
を許容すること(nullability
)を表す記号を書きます。
そのため Kotlin には Scala には存在しない「null
のことを考えた関数」がたくさん用意されています。
takeIf, takeUnless
takeIf
は受け取った関数の判定が真の場合、自身を返して(型には?
がつく)、偽の場合は、null
を返すというものです。takeUnless
はその逆ですね。
2.takeIf { it % 2 == 0 }
// res0: kotlin.Int? = 2
2.takeUnless { it % 2 == 0 }
// res1: kotlin.Int? = null
Scala と比べるのは不毛ですが、近い関数は Option::when
, Option::unless
になると思います。
val x = 2
// val x: Int = 2
Option.when(x % 2 == 0)(x)
// val res0: Option[Int] = Some(2)
Option.unless(x % 2 == 0)(x)
// val res1: Option[Int] = None
firstOrNull, singleOrNull, elementAtOrNull
**OrNull
系の関数は、指定した要素が見つからなかった(ではなかった)場合にnull
を返してくれる関数です。
ints.firstOrNull()
// res2: kotlin.Int? = 1
ints.singleOrNull()
// res3: kotlin.Int? = null
ints.elementAtOrNull(100)
// res4: kotlin.Int? = null
これも Scala と比べるのは不毛ですが近い処理を書くと、
ints.headOption
// val res2: Option[Int] = Some(1)
Option.when(ints.lengthIs == 1)(ints.head)
// val res3: Option[Int] = None
Option.when(ints.isDefinedAt(100))(ints(100))
// val res4: Option[Int] = None
とかになりますでしょうか。
あんまりスマートに書けないですね。なにかいい書き方あればお教えいただきたい・・・。
[2021/05/04 追記ここから]
がくぞさんに、お教えいただいたので追記します。
ints.lift(1)
// val res0: Option[Int] = Some(2)
ints.lift(100)
// val res1: Option[Int] = None
PartialFunction
に属する関数なんですね。全然見たことなかったので勉強になりました!
[2021/05/04 追記ここまで]
mapNotNull, filterNotNull, listOfNotNull
**NotNull
系はその名の通り、null
を除外してくれる合成済みの関数という感じです。
ints.mapNotNull { it.takeIf { it % 2 == 0 } }
// res5: kotlin.collections.List<kotlin.Int> = [2]
listOf(1, 2, null, 3, 4).filterNotNull()
// res6: kotlin.collections.List<kotlin.Int> = [1, 2, 3, 4]
listOfNotNull(1, 2, null, 3, 4)
// res7: kotlin.collections.List<kotlin.Int> = [1, 2, 3, 4]
Scala の場合は、flatten
, flatMap
とかでOption
を潰す処理と同等になるかと思います。
as?
as
は型変換をする際に使うものですが、as?
とすると、変換に失敗したときは例外ではなくnull
になってくれるというものです。便利ですね。
10000000000L as? Int
// res8: kotlin.Int? = null
Scala で同じ様な処理書くとしたら、汎用的には書けなそうな気がしますがどうなんでしょうか。上記と似た様な処理は、一応こんな感じで書けると思います。
Option.when(10000000000L.isValidInt)(10000000000L.toInt)
// val res8: Option[Int] = None
Range の違い
範囲を表すRange
は Kotlin では以下のように書きます。
// 1 ~ 10 の配列を作る場合
1..10
// res0: kotlin.ranges.IntRange = 1..10
// 1 ~ 9 (10 を含まない)の配列を作る場合
1 until 10
// res1: kotlin.ranges.IntRange = 1..9
// 10 ~ 1 の配列を作る場合
10 downTo 1
// res2: kotlin.ranges.IntProgression = 10 downTo 1 step 1
downTo
を使った場合は、IntRange
の親クラスであるIntProgression
として作られます。またIntRange
という型からわかるように、他の型のRange
も個別で用意されています。
Long や Double の Range
Long
型やDouble
型の場合も同じ様に書くことができますが、Double
型については注意が必要です。
// Long 型の場合
1L..10L
// res3: kotlin.ranges.LongRange = 1..10
// Double 型の場合
1.1..1.9
// res4: kotlin.ranges.ClosedFloatingPointRange<kotlin.Double> = 1.1..1.9
res4.contains(1.7)
// res5: kotlin.Boolean = true
res4.contains(2.0)
// res6: kotlin.Boolean = false
Long
の場合、LongRange
になるのは分かりやすいんですが、Double
の場合のClosedFloatingPointRange
とはなんでしょうか。
調べてみるとイテレータとしての機能はないけど、contains
, isEmpty
などのメソッドは持っている型のようです。
Kotlin では小数に対してイテレートするRange
は定義できなそうですが、
(1..3).map { it / 10.0 }
// res7: kotlin.collections.List<kotlin.Double> = [0.1, 0.2, 0.3]
必要な場合は、上記のようにmap
すればいいから特に困らなそうですね。
一方 Scala では以下の様に書きます。
// 1 ~ 10 の配列を作る場合
1 to 10
// res0: scala.collection.immutable.Range.Inclusive = Range 1 to 10
// 1 ~ 9 (10 を含まない)の配列を作る場合
1 until 10
// res1: scala.collection.immutable.Range = Range 1 until 10
// 10 ~ 1 の配列を作る場合
10 to 1 by -1
// res2: scala.collection.immutable.Range = Range 10 to 1 by -1
こちらは、引数が一個のto
を使った場合だけ、Range
の子クラスであるRange.Inclusive
として作られます。Kotlin のdownTo
の様な関数はないので、10 to 1 by -1
とする必要があります。
Long や Double の Range
Scala の場合Long
型は書けますが、Double
型については書くことができません。代替案としてBigDecimal
型で step を小数にすることで小数のRange
自体は実現できます。
// Long 型の場合
1L to 10L
// res3: scala.collection.immutable.NumericRange.Inclusive[Long] = NumericRange 1 to 10
// Double 型の場合
9.474 to 49.474 by 1.0
scala> 9.474 to 49.474 by 1.0
^
error: value to is not a member of Double
// BigDecimal 型 の場合
BigDecimal(9.474) to BigDecimal(49.474) by 0.1
// res5: scala.collection.immutable.NumericRange.Inclusive[scala.math.BigDecimal] = NumericRange 9.474 to 49.474 by 0.1
なお Scala では1 to 10
という書き方は、Range
を生成するときに使いますが、Kotlin ではPair
を生成するときに使うので注意が必要です。until
は Scala, Kotlin で書き方一緒なので、余計にややこしいですね。
Pair の違い
Collection
でもIterable
でもないですが、よく使うデータ構造であるPair
を見ていきます。
オブジェクト生成
Kotlin の場合、Pair
の生成は以下のとおりです。
Pair(1, "user1")
// res0: kotlin.Pair<kotlin.Int, kotlin.String> = (1, user1)
1 to "user1"
// res1: kotlin.Pair<kotlin.Int, kotlin.String> = (1, user1)
Scala の場合は、Pair
ではなくTuple2
ですが、以下のようにインスタンスを生成します。
(1, "user1")
// res0: (Int, String) = (1,user1)
1 -> "user1"
// res1: (Int, String) = (1,user1)
Scala では、->
はTuple2
を生成するときに使う糖衣構文ですが、Kotlin ではラムダやパターンマッチのときに使う記号です。Scala ではラムダやパターンマッチをするときに使うのは=>
なので、混乱しがちです。
関数
Kotlin のPair
には(Triple
もですが)、toList
メソッドが用意されています。
public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second)
first, second が同じ型の場合に使えるものなので、以下のように使えます。
Pair(1, 2).toList()
// res3: kotlin.collections.List<kotlin.Int> = [1, 2]
Scala のTuple2
には、toList
メソッドは用意されていないので、同等の処理を書こうとすると、
(1, 2).productIterator.collect { case i: Int => i }.toList
// val res3: List[Int] = List(1, 2)
の様になります。標準でtoList
欲しい。
なお Scala 3 だとTuple
周りがすごく書きやすくなりそうなので、とても楽しみにしています。(toList
もできるようになるみたいです。)
Scala にあるけど Kotlin にはなさそうな関数
たくさんあると思いますが「なんでないんだ。不便だな。」と思った関数だけ上げていきます。
これまでとは逆に Kotlin だったらどう書くかも併せて書いていこうと思います。
collect
Scala だとcollect
というfilter
+ map
の役割を持つ関数があるんですが Kotlin にはなさそうでした。
ints.collect{ case i if i % 2 == 0 => s"$i is even" }
// val res0: Seq[String] = Vector(2 is even)
なので Kotlin の場合、filter
+ map
するしかないんでしょう。おそらく。
ints.filter { it % 2 == 0 }.map { "$it is even" }
// res0: kotlin.collections.List<kotlin.String> = [2 is even]
transpose
Scala には、transpose
という、二次元配列を「縦で持ち直す」便利な関数があるんですが、Kotlin にはなさそうでした。
Seq(Seq(1, 2, 3), Seq(11, 12, 13), Seq(21, 22, 23)).transpose
// val res1: Seq[Seq[Int]] = List(List(1, 11, 21), List(2, 12, 22), List(3, 13, 23))
Kotlin で書く場合、indices
を使って自身をmap
する処理を書くしかなさそう。
val list = arrayOf(arrayOf(1, 2, 3), arrayOf(11, 12, 13), arrayOf(21, 22, 23))
list.first().indices.map { i -> list.map { it[i] } }
// res1: kotlin.collections.List<kotlin.collections.List<kotlin.Int>> = [[1, 11, 21], [2, 12, 22], [3, 13, 23]]
他には、splitAt
, span
, inits
, tails
などの関数も Kotlin にはないので自力で実装しなきゃいけなくて、欲しくなる場面があるだろうなと思いました。
終わりに
Kotlin, Scala それぞれ個性があって面白かったです。ちょっとしたロジックを書く分にはこのくらい覚えておいて、また使うときにちょっとアップデートできればいいかなと思っています。
今回はほとんどコレクション周りの標準関数の書き比べしか書いていませんが、Scala から Kotlin を見ると
-
Option
がない -
Either
がない - パターンマッチ弱い
みたいなこと(もっとあると思うけど)で実装方針が全然変わってくるので、それぞれのコーディング手法の良し悪しの理解が進んだら、そちらについても書き残しておきたいなと思っています。