5
3

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 3 years have passed since last update.

Kotlin の標準関数覚え書き(Scala との比較等)

Last updated at Posted at 2021-05-03

9ヶ月ほどサーバーサイド Kotlin を書いて過ごしていましたが、そろそろ Scala の仕事に戻れそうなので、備忘録として両者の書き味の違いを残して置こうと思います。
Collection とか Iterable 周りの関数とか書き方を中心に書きます。

実行環境など

Kotlin をラフに書いて試したい場合は、IntelliJ の Kotlin REPL を使うのが良さそうです。今回実行したバージョンはこちら。

Kotlin
Welcome to Kotlin version 1.4.21 (JRE 13.0.1+9)

Scala の場合はscalaコマンドかsbt consoleを使うと思います。今回実行したバージョンはこちら。

Scala
Welcome to Scala 2.13.5 (OpenJDK 64-Bit Server VM, Java 15.0.2).

Collections の関数

Scala には同等の関数がなさそうなやつ

Kotlin の関数を調べると、今のところ Scala には定義されていない便利そうな関数がたくさんあって面白かったので、Scala を使った場合の同等な処理と書き比べる形で紹介しようと思います。

Kotlin
val ints: List<Int> = (1..3).toList()
Scala
val ints: Seq[Int] = 1 to 3

コード例にはこちらの変数を使います。

associate(associateWith, associateBy)

Scala では配列からMapを作るときTuple2 + toMapをすると思います。(キーが被らない保証があるとき等)

Scala
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という関数が用意されています。

Kotlin
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を使って型のパターンマッチをして

Scala
Seq(List(1), Set(2), Map(3 -> 4)).collect { case xs: Set[Int] => xs }
// val res1: Seq[Set[Int]] = List(Set(2))

のように書くと思います。少しトリッキーな書き方になってしまいますよね。
Kotlin ならfilterIsInstanceという関数が用意されています。

Kotlin
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のような処理をしたい場合は、以下のように書く事ができます。

Kotlin
ints.groupingBy { it % 2 == 0 }.eachCount()
// res2: kotlin.collections.Map<kotlin.Boolean, kotlin.Int> = {false=2, true=1}

Scala で同等のことがしたい場合はgroupBy + mapValues, またはgroupMapReduce を使って

Scala
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を反転して

Scala
!ints.exists(_ % 2 == 0)
// val res4: Boolean = false

などとやると思います。
Kotlin の場合、noneという関数が用意されています。

Kotlin
ints.none { it % 2 == 0 }
// res4: kotlin.Boolean = false

union

Scala にも同名の関数がありますが、deprecated になっているのと、処理内容が違うので注意が必要です。Kotlin のunionは配列を合体させて一意にしたものが返されます。

Kotlin
ints.union(2..5)
// res5: kotlin.collections.Set<kotlin.Int> = [1, 2, 3, 4, 5]

同じ様な処理を Scala で書きたい場合、concat + toSetするとそれっぽくなると思います。

Scala
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する感じで

Scala
ints.reverse.takeWhile(1 < _).reverse
// val res6: Seq[Int] = Range 2 to 3

この様に書きますかね。(もう少しなんとかなりそうな気はするものの)
Kotlin には**LastWhileという関数が用意されています。

Kotlin
ints.takeLastWhile { 1 < it }
// res6: kotlin.collections.List<kotlin.Int> = [2, 3]

zipWithNext

Scala では配列の次の要素とzipしたい場合tail + zipで書きますよね。

Scala
ints.zip(ints.tail)
// val res7: Seq[(Int, Int)] = Vector((1,2), (2,3))

Kotlin にはzipWithNextという関数が用意されています。

Kotlin
ints.zipWithNext()
// res7: kotlin.collections.List<kotlin.Pair<kotlin.Int, kotlin.Int>> = [(1, 2), (2, 3)]

average

Scala では数値の平均を出すにはDoubleにして配列のサイズで割ると思います。

Scala
ints.sum.toDouble / ints.length
// val res8: Double = 2.0

Kotlin にはaverageという関数が用意されています。

Kotlin
ints.average()
// res8: kotlin.Double = 2.0

mapIndexed(filterIndexed, foldIndexed...)

Scala では index 値を使って配列を操作したい場合zipWithIndexと組み合わせて書くと思います。

Scala
ints.zipWithIndex.map { case (index, i) => s"$i-$index" }
// val res9: Seq[String] = Vector(0-1, 1-2, 2-3)

Kotlin には**Indexedという合成済みの関数が用意されています。

Kotlin
ints.mapIndexed { index, i -> "$i-$index" }
// res9: kotlin.collections.List<kotlin.String> = [1-0, 2-1, 3-2]

sortedDescending

Scala では逆順にソートする場合sorted.reverseもしくはパフォーマンスが気になる場合は

Scala
ints.sorted(Ordering[Int].reverse)
// val res10: Seq[Int] = Vector(3, 2, 1)

このように書いたりすると思います。
Kotlin にはsortedDescendingという関数が用意されています。

Kotlin
ints.sortedDescending()
// res10: kotlin.collections.List<kotlin.Int> = [3, 2, 1]

maxOf(minOf, sumOf...)

Scala ではmapした結果のmaxをとる処理を書く場合、文字通り以下のように書きます。

Scala
ints.map(i => i * i % 5).max
// val res11: Int = 4

Kotlin には**Ofという関数が用意されています。

Kotlin
ints.maxOf { it * it % 5 }
// res11: kotlin.Int = 4

runningReduce

Scala で「初期値を与えないscan」みたいなことをしたいとき、僕だったら配列が空か判断してから以下のようにやるかなと思います。

Scala
ints.tail.scan(ints.head)(_ + _)
// val res12: Seq[Int] = Vector(1, 3, 6)

Kotlin では以下のように書けます。

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もこの関数と同じ括り。

Kotlin
ints.all { it % 2 == 0 }
// res0: kotlin.Boolean = false

ints.any { it % 2 == 0 }
// res1: kotlin.Boolean = true
Scala
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になるようなので、元の情報が消えてしまうことがある気がします。

Kotlin
ints.intersect(2..4)
// res2: kotlin.collections.Set<kotlin.Int> = [2, 3]

ints.subtract(2..4)
// res3: kotlin.collections.Set<kotlin.Int> = [1]
Scala
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です。(返り値の型とか、groupedslidningの一般化した関数ではないとか色々あると思いますが、やれることは一緒)

配列をいい感じに分割して(二次元配列にして)返す処理が書けます。

Scala
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))
Kotlin
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: Booleanfalseにすると、size に足りない要素は消してくれる機能付きです。(デフォルトがfalseなので注意が必要です)

Kotlin
ints.windowed(2, 2)
// res6: kotlin.collections.List<kotlin.collections.List<kotlin.Int>> = [[1, 2]]

runningFold

これはscanそのものです。Kotlin にもscan関数が用意されているんですが、それもrunningFoldを呼び出す形で定義されています。

_Collections.kt
public inline fun <T, R> Iterable<T>.scan(initial: R, operation: (acc: R, T) -> R): List<R> {
    return runningFold(initial, operation)
}
Kotlin
ints.runningFold(10) { acc, i -> acc + i }
// res7: kotlin.collections.List<kotlin.Int> = [10, 11, 13, 16]
Scala
ints.scan(10)(_ + _)
// val res7: Seq[Int] = Vector(10, 11, 13, 16)

in, !in

こちらは関数名ではなくcontainsの糖衣構文ですが Scala では出来ない書き方なので紹介します。

Kotlin
2 in ints
// res8: kotlin.Boolean = true

2 !in ints
// res9: kotlin.Boolean = false
Scala
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などで型が変わらない方が普通、みたいな感覚があったので全然意識外でした。試してみると、

Scala
Set(1, 2, 3).map(_ % 2)
// val res0: scala.collection.immutable.Set[Int] = Set(1, 0)
Kotlin
setOf(1, 2, 3).map { it % 2 }
// res0: kotlin.collections.List<kotlin.Int> = [1, 0, 1]

Scala の場合はSetのままだけど、Kotlin の場合はListになっています。

_Collections.kt
fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>

定義を見るとそりゃそうですがListを返すようになっていますね。他の配列を返す標準関数もだいたいListを返す様に出来ているので、注意が必要そうです。注意が必要そうなことの一例でいうと、ものすごく巨大な配列に対して操作したあとに結果としてSetが欲しい場合など、

Kotlin
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はその逆ですね。

Kotlin
2.takeIf { it % 2 == 0 }
// res0: kotlin.Int? = 2

2.takeUnless { it % 2 == 0 }
// res1: kotlin.Int? = null

Scala と比べるのは不毛ですが、近い関数は Option::when, Option::unlessになると思います。

Scala
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を返してくれる関数です。

Kotlin
ints.firstOrNull()
// res2: kotlin.Int? = 1

ints.singleOrNull()
// res3: kotlin.Int? = null

ints.elementAtOrNull(100)
// res4: kotlin.Int? = null

これも Scala と比べるのは不毛ですが近い処理を書くと、

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 追記ここから]

がくぞさんに、お教えいただいたので追記します。

Scala
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を除外してくれる合成済みの関数という感じです。

Kotlin
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になってくれるというものです。便利ですね。

Kotlin
10000000000L as? Int
// res8: kotlin.Int? = null

Scala で同じ様な処理書くとしたら、汎用的には書けなそうな気がしますがどうなんでしょうか。上記と似た様な処理は、一応こんな感じで書けると思います。

Scala
Option.when(10000000000L.isValidInt)(10000000000L.toInt)
// val res8: Option[Int] = None

Range の違い

範囲を表すRangeは Kotlin では以下のように書きます。

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型については注意が必要です。

Kotlin
// 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は定義できなそうですが、

Kotlin
(1..3).map { it / 10.0 }
// res7: kotlin.collections.List<kotlin.Double> = [0.1, 0.2, 0.3]

必要な場合は、上記のようにmapすればいいから特に困らなそうですね。

一方 Scala では以下の様に書きます。

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自体は実現できます。

Scala
// 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の生成は以下のとおりです。

Kotlin
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ですが、以下のようにインスタンスを生成します。

Scala
(1, "user1")
// res0: (Int, String) = (1,user1)

1 -> "user1"
// res1: (Int, String) = (1,user1)

Scala では、->Tuple2を生成するときに使う糖衣構文ですが、Kotlin ではラムダやパターンマッチのときに使う記号です。Scala ではラムダやパターンマッチをするときに使うのは=>なので、混乱しがちです。

関数

Kotlin のPairには(Tripleもですが)、toListメソッドが用意されています。

Triple.kt
public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second)

first, second が同じ型の場合に使えるものなので、以下のように使えます。

Kotlin
Pair(1, 2).toList()
// res3: kotlin.collections.List<kotlin.Int> = [1, 2]

Scala のTuple2には、toListメソッドは用意されていないので、同等の処理を書こうとすると、

Scala
(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 にはなさそうでした。

Scala
ints.collect{ case i if i % 2 == 0 => s"$i is even" }
// val res0: Seq[String] = Vector(2 is even)

なので Kotlin の場合、filter + mapするしかないんでしょう。おそらく。

Kotlin
ints.filter { it % 2 == 0 }.map { "$it is even" }
// res0: kotlin.collections.List<kotlin.String> = [2 is even]

transpose

Scala には、transposeという、二次元配列を「縦で持ち直す」便利な関数があるんですが、Kotlin にはなさそうでした。

Scala
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する処理を書くしかなさそう。

Kotlin
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がない
  • パターンマッチ弱い

みたいなこと(もっとあると思うけど)で実装方針が全然変わってくるので、それぞれのコーディング手法の良し悪しの理解が進んだら、そちらについても書き残しておきたいなと思っています。

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?