雑に描いた餅

餅は餅屋、わたしは技術屋、と言いたかった。

KotlinのjoinToString関数に関する小ネタ

今でもたまにやらかすので供養として。

Kotlinにはいくつか便利な関数がありますが、その中にjoinToStringがあります。

joinToString - Kotlin Programming Language

上に貼ったリンク先でも読めますが、Iterableをレシーバーとして、要素を連結して文字列に変換する関数です。


ただ、便利なあまりたまに紛らわしい挙動をすることがあります。

例えばこんなふうに。

fun main() {
    val list = listOf("a", "b", "c")
    // 1
    println(list.joinToString(","))
    // 2
    println(list.joinToString { "," })
    kotlin.system.exitProcess(0)
}

joinToString 関数を使った処理を2つ書いてみました。遠目に見ると同じ処理に見えなくもないです。

いやいや、全然違うじゃん、という人も、出力結果を予想しながら続きを読んでみてください。

fun main() {
    val list = listOf("a", "b", "c")
    // 1
    println(list.joinToString(",")) // a,b,c
    // 2
    println(list.joinToString { "," }) // ,, ,, ,
    kotlin.system.exitProcess(0)
}

全く違う結果になりました。不思議ですね。

これは、Kotlinのコーディング規約と、joinToStringの引数の合わせ技によって起こっています。ここから先は気になった人だけ読んでみてください。

Kotlinのコーディング規約

Kotlinのコーディング規約の中には以下のようなものがあります。

According to Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses: Higher-order functions and lambdas | Kotlin Documentation

つまり、関数の最後の引数が関数であった場合は、その部分だけラムダ式としてカッコの外側に書き出すことができる、ということなのです。

実際に以下のようなコードを見たことがある人もいるのではないでしょうか。

val product = items.fold(1) { acc, e -> acc * e }

上記はfold関数の最後の引数は、operation: (acc: R, T) -> Rという、各要素に対する操作を表す関数です。よって、関数部分に当たるラムダ式がカッコの外側に書き出されている、というわけです。

今回のjoinToString関数も、transform: ((T) -> CharSequence)? = null という関数になっており、{ "," }というラムダ式で、常に","が返ってくる関数になってしまっている状態というわけです。

ちなみに、以下のページにて、Kotlinの開発元である、Jetbrainsがコーディング規約をまとめています。気になった人は覗いてみてください。

Coding conventions | Kotlin Documentation

joinToStringの引数

上でも少し触れましたが、joinToStringの最後の引数は関数を受け取ります。

では、最初の引数は何か、というと、separator、つまり区切り文字を表します。

ちなみにこのseparatorには、デフォルト値として", "が与えられています。

つまり、以下のコードは「", "を区切り文字として」「要素を常に","に変化させる関数を適用する」ことによって生まれた出力ということになります。

    println(list.joinToString { "," }) // ,, ,, ,

おわりに

joinToStringがなんだかまぎらわしい関数のように思えてきた人もいるかもしれませんが、他にもprefix/postfixを指定できたり、いろいろな機能があります。

val numbers = listOf(1, 2, 3)
println(numbers.joinToString()) // 1, 2, 3
println(numbers.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3]
println(numbers.joinToString(prefix = "<", postfix = ">", separator = "+")) // <1+2+3>

気になった人は以下のPlaygroundで実際に触って確かめてみてください。

Kotlin Playground: Edit, Run, Share Kotlin Code Online