[毎日Kotlin] Day41. Builders: how it works

2018.03.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

毎日Kotlinシリーズです。

このシリーズを初めての方はこちらです。「毎日Kotlin」はじめました | Developers.IO

問題

Builders how it works | Try Kotlin

Look at the questions below and give your answers

  1. In the Kotlin code
tr {
    td {
        text("Product")
    }
    td {
        text("Popularity")
    }
}

'td' is:

a. special built-in syntactic construct

b. function declaration

c. function invocation

  1. In the Kotlin code
tr (color = "yellow") {
    td {
        text("Product")
    }
    td {
        text("Popularity")
    }
}

'color' is:

a. new variable declaration

b. argument name

c. argument value

  1. The block
{
    text("Product")
}

from the previous question is:

a. block inside built-in syntax construction td

b. function literal (or "lambda")

c. something mysterious

  1. For the code
tr (color = "yellow") {
    this.td {
        text("Product")
    }
    td {
        text("Popularity")
    }
}

which of the following is true:

a. this code doesn't compile

b. this refers to an instance of an outer class

c. this refers to a receiver parameter TR of the function literal:

tr (color = "yellow") {
    this@tr.td {
        text("Product")
    }
}
import Answer.*

enum class Answer { a, b, c }

val answers = mapOf<Int, Answer?>(
        1 to null, 2 to null, 3 to null, 4 to null
)

狙い

ここで考えて欲しい問題の意図はなんだろうか?

Kotlinの豊かなDSL。使いこなすと強力な武器。いままで解いてきた問題の集大成!

解答例

1 to c, 2 to b, 3 to b, 4 to c

mapOfをtoを記述できるようにしたものです。

toはただの拡張関数でPairを作ります。

public infix fun <A, B> A.to(that: B): kotlin.Pair<A, B>

本来なら

1.to(Answer.a)

なんですが、infixがついているので、スペース区切りで呼べます。

1 to c

種はこれだけなんですが、簡潔な記述に見えますね!

HtmlBuilderの補足

[毎日Kotlin] Day40. Html builderの解説が本日の問題説明文であります。

Html builders | Try Kotlinの定義はこちらを参照。

    return html {
        table {
            tr {
                td {
                    text("Product")
                }
                td {
                    text("Price")
                }
                td {
                    text("Popularity")
                }
            }
        }
    }.toString()

頭から見ていきましょう。

fun html(init: Html.() -> Unit): Html = Html().apply(init)

Html()をnewして、一時的な拡張関数initを引数でもらうことで、Htmlの初期化処理を外からできるようにしています。html {}の内部のthisはHtmlオブジェクトのことです。

tableの定義を見てみよう。

fun Html.table(init : Table.() -> Unit) = doInit(Table(), init)

class Table: Tag("table")

fun <T: Tag> Tag.doInit(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
}
open class Tag(val name: String) {
    val children = mutableListOf<Tag>()
    val attributes = mutableListOf<Attribute>()

    override fun toString(): String {
        return "<$name" +
            (if (attributes.isEmpty()) "" else attributes.joinToString(separator = " ", prefix = " ")) + ">" +
            (if (children.isEmpty()) "" else children.joinToString(separator = "")) +
            "</$name>"
    }
}

Htmlの拡張関数としてtableが定義されています。

Htmlと同じようにTable初期化処理を一時的な拡張関数initでもらっています。 それをHtmlの子要素に入れています。

tr,tdも同じように、自身を外部から初期化処理をもらい、親のchildernに自身を追加する流れになっています。

最後のtoString()のときに自分と子要素を結合していっています。

open class Tag(val name: String) {
    val children = mutableListOf<Tag>()
    val attributes = mutableListOf<Attribute>()

    override fun toString(): String {
        return "<$name" +
            (if (attributes.isEmpty()) "" else attributes.joinToString(separator = " ", prefix = " ")) + ">" +
            (if (children.isEmpty()) "" else children.joinToString(separator = "")) +
            "</$name>"
    }
}

やってることはそんなに難しいことをしていませんが、ラムダと拡張関数のオンパレードでややこしくみえます。その分、使用者は簡潔に、適切に記述できるようになります。

DSLでよく使えれる機能もチェック

いつも参考にさせていただいております。ありがとうございます!

あとがき

Day42.でまたお会いしましょう。 そして最終回。