KotlinのSecondary Constructorsでvalのプロパティを初期化したい

やりたかったこと

firstに依存しているsecondを保持するPairのようなモノが欲しかった。

data class Pair<F, S>(
    val first: F? = null,
    val second: S? = null
)

fun <F, S> newPair(first: F?, block: (F) -> S?) = Pair(
    first = first,
    second = first?.let(block) // firstがnullでなければblockを呼び出す
)

newPairは、以下のような使い方を想定している。

val pair = newPair(getFirst()) {first ->
    getSecond(first)
}

firstsecondともにNullableだがblockの実行はfirstがnullではない状態を保証したい。

別にこのままでも良いんだけど、ちょっとコンストラクタにしてみようと思いたった。このままでも良かったのにKotlin力が高くないので、アレコレ試行錯誤が必要だった。あとcompanion objectという方法もあったけど、まぁやってみたかったんだよ。

thisへの委譲では式が使える

Secondary Constructorsでは、Primary Constructorか他のSecondary Constructorへの委譲が必要になる。thisを使用して呼び出しを行うが、その際には式を利用することができる。なので、こういう呼び出し方ができる。

data class Pair<F, S>(
    val first: F? = null,
    val second: S? = null
) {
    // OK
    constructor(first: F?, block: (F) -> S?): this(first, first?.let(block))
}

このことがわかってなくてあれこれ試行錯誤した。

ダメな例

プロパティがvalであるということが問題になった。

data class Pair<F, S>(
    val first: F? = null,
    val second: S? = null
) {
    // NG: Val cannot be reassigned
    // secondはvalなので書き換えができない
    // コンストラクタなのになんで初期化できんのじゃーとかちょっと思ったりする
    constructor(first: F?, block: (F) -> S?): this(first) {
        second = first?.let(block)
    }

    // NG: Unresolved reference: sec
    // thisの方に変数を渡せるのでは???と思ったけど渡せなかった
    constructor(first: F?, block: (F) -> S?): this(first, sec) {
        val sec = first?.let(block)
    }

    // OK
    // ifも式なのでこれでも問題ない
    constructor(first: F?, block: (F) -> S?): this(first, if (first != null) block(first) else null)

    // OK
    constructor(first: F?, block: (F) -> S?): this(first, first?.let(block))
}

実行例

以下のように期待する動きをしている。

val p1 = Pair("aaa", "AAA")
val p2 = Pair("aaa", String::toUpperCase)
val p3 = Pair<String, String>(null, String::toUpperCase)

println(p1)
println(p2)
println(p3)

// output
// Pair(first=aaa, second=AAA)
// Pair(first=aaa, second=AAA)
// Pair(first=null, second=null)

継承のところで気づいた

リファレンスの継承の辺りを読んでいる時に、以下の記述を見つけて、これはPrimary Constructorの委譲でも使えるのではと思って試したら動いた。

class Derived(
    name: String,
    val lastName: String
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

...

リファレンスはCollectionの辺りまで読んだつもりだけど、どっか明確に記載されてたかなぁ…