関数とラムダ
Kotlin のコードの大部分は、関数の宣言と呼び出しでできています。この章の前半では、関数の宣言と引数の渡し方 (デフォルト引数と名前付き引数、可変長引数、infix 関数) を扱います。続いて、関数を値として扱う関数型とラムダを見て、ラムダの中の return と関数参照、fun interface と typealias に進みます。後半では、既存の型に関数を足す拡張関数と、let や apply などのスコープ関数を扱います。最後に、ラムダの中の return の動きを決めている inline 関数の仕組みを説明します。
読み終えると、送料の計算や在庫の確認のような処理を、ラムダを受け取る関数として書けるようになります。標準ライブラリのスコープ関数も、目的に合わせて選べるようになります。
関数を宣言する
引数と戻り値
関数は fun で宣言します。引数は 名前: 型 の形で並べ、戻り値の型は引数のリストの後ろに : を付けて書きます。
fun lineTotal(unitPrice: Int, quantity: Int): Int {
return unitPrice * quantity
}
fun printLine(productName: String, quantity: Int) {
println("$productName × $quantity")
}
fun main() {
println(lineTotal(500, 3))
printLine("ノート", 3)
}
1500
ノート × 3
lineTotal は return で値を返します。printLine は戻り値の型を書いていません。波括弧の本体を持つ関数で戻り値の型を省くと、その関数は Unit を返し、return も要りません (Unit は Kotlin の基本 で扱いました)。
引数は val と同じく読み取り専用で、関数の中で再代入できません。
単一式関数
本体が 1 つの式だけの関数は、波括弧と return を省き、= の後ろに式を書けます。この形を 単一式関数 と呼びます。
fun lineTotal(unitPrice: Int, quantity: Int): Int = unitPrice * quantity
fun shippingFee(subtotal: Int) = if (subtotal >= 5000) 0 else 500
fun main() {
println(lineTotal(500, 3))
println(shippingFee(1500))
println(shippingFee(6000))
}
1500
500
0
if は値を返す式なので、shippingFee のような分岐も 1 つの式で書けます。shippingFee は戻り値の型も省いていて、型は = の右の式から推論されます。ほかのファイルから呼ぶ関数では、戻り値の型を書いておくと、本体を書き換えたときに戻り値の型が気づかないうちに変わることを防げます。
デフォルト引数と名前付き引数
引数の宣言に = 値 を付けると、呼び出し側がその引数を省いたときに使う値になります。これを デフォルト引数 と呼びます。呼び出し側では、引数名 = 値 の形で、引数を名前で指定できます (名前付き引数)。
fun describeLine(
productName: String,
quantity: Int = 1,
giftWrap: Boolean = false,
): String {
val wrap = if (giftWrap) "あり" else "なし"
return "$productName × $quantity (ギフト包装: $wrap)"
}
fun main() {
println(describeLine("ノート"))
println(describeLine("ノート", 3))
println(describeLine("ノート", giftWrap = true))
println(describeLine(quantity = 2, productName = "ペン"))
}
ノート × 1 (ギフト包装: なし)
ノート × 3 (ギフト包装: なし)
ノート × 1 (ギフト包装: あり)
ペン × 2 (ギフト包装: なし)
2 回目の呼び出しは前から順に渡しているので、2 つ目の値が quantity に入ります。3 回目は quantity を飛ばして giftWrap だけを渡したいので、名前で指定しています。名前を付けずに describeLine("ノート", true) と書くと、true が 2 番目の引数の quantity に渡され、型が合わずにコンパイルエラーになります。4 回目のように、名前付き引数は宣言と違う順番でも並べられます。
名前付き引数は、省く引数があるとき以外にも役に立ちます。describeLine("ノート", 1, true) の true は、呼び出しだけを読んでも何を指定したのか分かりません。giftWrap = true と書けば、宣言を見に行かなくても意味が読めます。
describeLine の宣言にある最後の引数の後ろの , は、省いてもかまいません。引数を 1 行に 1 つずつ書くときに付けておくと、引数を足したときの差分が 1 行で済みます。
可変長引数
vararg を付けた引数は、0 個以上の値をまとめて受け取ります。
fun totalQuantity(vararg quantities: Int): Int {
var total = 0
for (quantity in quantities) {
total += quantity
}
return total
}
fun main() {
println(totalQuantity())
println(totalQuantity(2, 3))
val fromCart = intArrayOf(1, 4, 5)
println(totalQuantity(*fromCart))
println(totalQuantity(10, *fromCart))
}
0
5
10
20
関数の中で、vararg quantities: Int の quantities は IntArray 型の配列になります。配列は コレクションとジェネリクス で扱います。ここでは for で順に取り出せることだけ分かれば足ります。
手元にある配列を渡すときは、前に * を付けて要素に展開します。この * を スプレッド演算子 と呼びます。最後の呼び出しのように、個別の値と展開した配列を混ぜて渡すこともできます。
infix 関数
関数に infix を付けると、. と括弧を省いて a 関数名 b の形で呼べます。
data class Inventory(val productName: String, val available: Int) {
infix fun canFulfill(quantity: Int): Boolean = available >= quantity
}
fun main() {
val note = Inventory("ノート", 5)
println(note canFulfill 3)
println(note.canFulfill(3))
println(note canFulfill 2 + 4)
println("ノート" to 3)
}
true
true
false
(ノート, 3)
Inventory は data class として宣言しています。クラスの宣言は クラスとオブジェクト で扱うので、この章では「値をまとめて持ち、println で中身を表示できる型」と考えてください。canFulfill は Inventory の中で宣言した関数 (メンバー関数) です。
実行結果の 1 行目と 2 行目は、同じ呼び出しを書き方だけ変えたものです。3 行目は、infix の呼び出しより算術演算子が先に結合することを示しています。note canFulfill 2 + 4 は note.canFulfill(2 + 4) として評価されます。
infix を付けられるのは、メンバー関数か拡張関数 (後の節で扱います) で、引数がちょうど 1 つのものだけです。その引数には vararg もデフォルト値も付けられません。標準ライブラリの to も infix 関数で、2 つの値から Pair を作ります。mapOf("ノート" to 500) のように、マップを作るときによく使います。
関数型とラムダ
関数を値として扱う
Kotlin では、関数を値として変数に入れたり、引数に渡したりできます。関数の値の型を 関数型 と呼び、(引数の型) -> 戻り値の型 と書きます。関数の値を作るいちばん短い書き方が ラムダ式 (以下、ラムダ) で、{ 引数 -> 本体 } と書きます。
val addTax: (Int) -> Int = { price -> price * 110 / 100 }
val describe: (String, Int) -> String = { name, quantity -> "$name × $quantity" }
val quantityOnly: (String, Int) -> Int = { _, quantity -> quantity }
val thanks: () -> Unit = { println("ご注文ありがとうございます") }
println(addTax(1000))
println(describe("ノート", 3))
println(quantityOnly("ノート", 3))
thanks()
1100
ノート × 3
3
ご注文ありがとうございます
addTax の型 (Int) -> Int は「Int を 1 つ受け取って Int を返す関数」を表します。ラムダの本体の最後の式がそのまま戻り値になるので、return は書きません。引数が 2 つなら (String, Int) -> String のように並べ、ラムダの中で使わない引数は名前の代わりに _ と書きます。引数を取らず、返す値も無い関数の型は () -> Unit です。関数型の値は、普通の関数と同じく括弧を付けて呼び出します。
型の推論と it
ラムダの引数の型は、多くの場合に省けます。
val withTax = { price: Int -> price * 110 / 100 }
val isFreeShipping: (Int) -> Boolean = { it >= 5000 }
val shippingLabel: (Int) -> String = { subtotal ->
val fee = if (subtotal >= 5000) 0 else 500
"小計 $subtotal 円、送料 $fee 円"
}
println(withTax(2000))
println(isFreeShipping(4999))
println(shippingLabel(3000))
2200
false
小計 3000 円、送料 500 円
withTax は変数の型を書いていないので、ラムダの引数に price: Int と型を書いています。変数の型は、ラムダの引数と本体から (Int) -> Int と推論されます。逆に isFreeShipping のように変数の型を書けば、ラムダの側の型は省けます。
引数が 1 つのラムダは、引数の宣言と -> を省き、暗黙の名前 it で引数を参照できます。isFreeShipping の it は、呼び出しで渡した Int の値です。
ラムダの本体は複数行にでき、最後の式の値が戻り値になります。shippingLabel は、途中で fee を求めてから、最後の行の文字列を返しています。
高階関数
関数を引数に取る関数と、関数を返す関数を、まとめて 高階関数 と呼びます。次の applyDiscount は、値引きの計算を引数 rule として受け取ります。
fun applyDiscount(price: Int, rule: (Int) -> Int): Int {
val discounted = rule(price)
return if (discounted < 0) 0 else discounted
}
fun main() {
println(applyDiscount(1000, { price -> price - 300 }))
println(applyDiscount(1000) { it * 90 / 100 })
println(applyDiscount(200) { it - 300 })
}
700
900
0
applyDiscount は、値引きの計算方法を知りません。知っているのは「計算した結果が負なら 0 にする」という決まりだけで、計算そのものは呼び出し側がラムダで渡します。
最後の引数が関数型なら、ラムダを括弧の外に出して書けます。この書き方を トレイリングラムダ と呼びます。1 回目と 2 回目の呼び出しの違いは、ラムダを括弧の中に書くか外に書くかだけです。Kotlin のコーディング規約は、外に書ける場合は外に書く形を勧めています。引数がラムダだけなら、quantities.forEach { ... } のように括弧そのものを省けます。
関数を返す関数も書けます。
fun discountOf(percent: Int): (Int) -> Int = { price -> price * (100 - percent) / 100 }
fun main() {
val tenPercentOff = discountOf(10)
val halfPrice = discountOf(50)
println(tenPercentOff(1000))
println(halfPrice(1000))
}
900
500
discountOf の戻り値の型 (Int) -> Int は関数型です。返したラムダは、discountOf に渡した percent を覚えたまま、後から呼び出されます。
外側の変数を使うラムダ
ラムダは、宣言した場所から見える変数を読み書きできます。
var placedCount = 0
val place: (String) -> Unit = { orderId ->
placedCount++
println("$orderId を受け付けました")
}
place("A-1")
place("A-2")
println(placedCount)
A-1 を受け付けました
A-2 を受け付けました
2
place のラムダは、外側で宣言した var placedCount を書き換えています。ラムダを呼ぶたびに同じ変数が増えるので、実行結果の最後の行は place を呼んだ回数になります。外側の変数を取り込んだラムダを クロージャ と呼びます。前の discountOf が返したラムダも、引数の percent を取り込んだクロージャです。
null を許す関数型
関数型にも ? を付けて、null を許す型にできます。省いてもよいコールバックを受け取るときに使います。
fun placeOrder(orderId: String, onPlaced: ((String) -> Unit)? = null) {
println("$orderId を登録しました")
onPlaced?.invoke(orderId)
}
fun main() {
placeOrder("A-1")
placeOrder("A-2") { println("$it の確認メールを送ります") }
}
A-1 を登録しました
A-2 を登録しました
A-2 の確認メールを送ります
null を許す関数型は、((String) -> Unit)? のように関数型全体を括弧で囲んでから ? を付けます。括弧の無い (String) -> Unit? は「Unit? を返す関数」という別の型です。
invoke は関数型の値を呼び出すメソッドで、f(x) と f.invoke(x) は同じ意味です。null かもしれない関数型の値は f(x) の形では呼べないので、?.invoke(x) と書きます。onPlaced が null なら、何も呼ばれません。
レシーバ付きの関数型
関数型には、StringBuilder.() -> Unit のように、型と . を前に付けた形もあります。これを レシーバ付きの関数型 と呼びます。このラムダの中では、前に付けた型の値 (レシーバ) が this になり、そのメンバーを this. なしで呼べます。
fun buildLabel(build: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.build()
return builder.toString()
}
fun main() {
val label = buildLabel {
append("注文 A-1")
append("、")
append("明細 3 件")
}
println(label)
}
注文 A-1、明細 3 件
StringBuilder は文字列を少しずつ組み立てるための標準ライブラリのクラスで、append は末尾に文字列を足すメソッドです。buildLabel が builder.build() と呼ぶことで、builder がラムダの中の this になります。そのため、ラムダの中では append を builder. なしで書けます。後の節で扱うスコープ関数のうち run、with、apply の 3 つは、この仕組みで対象の値を this にしています。
ラムダの中の return
ラベルの無い return
ラムダの中の return は、どの関数から抜けるかに注意が要ります。まず、次のコードはコンパイルできません。
fun eachQuantity(quantities: List<Int>, action: (Int) -> Unit) {
for (quantity in quantities) {
action(quantity)
}
}
fun hasZero(quantities: List<Int>): Boolean {
eachQuantity(quantities) { quantity ->
if (quantity == 0) return true
}
return false
}
'return' is prohibited here
ラベルの無い return は、fun で宣言した関数から抜けます。ここで抜けようとしているのは hasZero です。ところが、ラムダは値として eachQuantity に渡されるので、eachQuantity がそのラムダをいつ呼ぶのかは hasZero の側からは分かりません。eachQuantity がラムダを保存しておき、hasZero が戻った後で呼ぶこともありえます。そのときには抜ける先の hasZero がもう終わっているので、コンパイラはラムダの中のラベルの無い return を認めません。
同じ処理を標準ライブラリの forEach で書くと、コンパイルが通ります。forEach は、リストの要素ごとにラムダを呼ぶ関数です。
fun firstShortage(availableQuantities: List<Int>, required: Int): Int? {
availableQuantities.forEach { available ->
if (available < required) return available
}
return null
}
fun main() {
println(firstShortage(listOf(10, 2, 5), 3))
println(firstShortage(listOf(10, 8), 3))
}
2
null
forEach に渡したラムダの中の return は、外側の firstShortage から抜けます。足りない在庫が見つかった時点で関数が終わるので、実行結果の 1 行目は最初に見つかった値で、その後ろの要素は調べていません。ラムダの中から外側の関数を抜ける return を 非ローカル return と呼びます。
forEach で認められ、eachQuantity で認められない理由は、forEach が inline を付けた関数であることにあります。仕組みは章の最後の inline 関数 で説明します。
ラベル付きの return
外側の関数でなくラムダだけを抜けたいときは、return@ラベル と書きます。ラムダを受け取った関数の名前が、そのままラベルとして使えます。
val quantities = listOf(3, 0, 2)
quantities.forEach { quantity ->
if (quantity == 0) return@forEach
println("数量 $quantity")
}
println("確認を終えました")
数量 3
数量 2
確認を終えました
return@forEach は、ラムダの今回の呼び出しだけを終わらせます。forEach は次の要素に進むので、数量が 0 の要素だけが飛ばされ、forEach の後ろの println まで実行されます。
ラベルには自分で名前を付けることもできます。Kotlin の基本 でループに付けたラベルと同じく、名前@ をラムダの前に置きます。もう 1 つの方法は、ラムダの代わりに 無名関数 を渡すことです。
val quantities = listOf(3, 0, 2)
quantities.forEach check@{ quantity ->
if (quantity == 0) return@check
println("ラベル: $quantity")
}
quantities.forEach(fun(quantity) {
if (quantity == 0) return
println("無名関数: $quantity")
})
ラベル: 3
ラベル: 2
無名関数: 3
無名関数: 2
check@{ ... } は、ラムダに check というラベルを付けています。無名関数は fun(引数) { 本体 } の形で書く名前の無い関数で、引数の型は渡す先から推論できれば省けます。無名関数の中の return は、ラベルが無くても無名関数そのものから抜けます。どちらの書き方でも、数量が 0 の要素だけが飛ばされます。
関数参照
宣言済みの関数は、::関数名 と書くと関数型の値として扱えます。これを 関数参照 と呼びます。
data class Product(val name: String, val price: Int)
fun isAffordable(product: Product): Boolean = product.price <= 1000
fun main() {
val products = listOf(Product("ノート", 500), Product("万年筆", 12000), Product("ペン", 200))
val check: (Product) -> Boolean = ::isAffordable
println(check(products[1]))
println(products.filter(::isAffordable))
println(products.map(Product::name))
val note = products[0]
val priceOfNote: () -> Int = note::price
println(priceOfNote())
val create: (String, Int) -> Product = ::Product
println(create("消しゴム", 100))
}
false
[Product(name=ノート, price=500), Product(name=ペン, price=200)]
[ノート, 万年筆, ペン]
500
Product(name=消しゴム, price=100)
この例には 4 種類の参照があります。
::isAffordableは、トップレベルの関数の参照です。型は(Product) -> Booleanで、ラムダと同じく関数を受け取る関数にそのまま渡せますProduct::nameは、プロパティの参照です。Productを受け取ってnameを返す関数として使えますnote::priceは、特定の値noteに結び付けた参照です。引数を取らずにnote.priceを返します::Productは、コンストラクタの参照です。(String, Int) -> Productの関数として使えます
filter は条件を満たす要素だけを残したリストを返し、map は要素ごとに変換した結果のリストを返します。どちらも コレクションとジェネリクス で扱います。products.filter { isAffordable(it) } と書いても結果は同じです。すでに名前の付いた関数があるなら、関数参照で渡すと、引数を受け渡すだけのラムダを書かずに済みます。
fun interface と SAM 変換
抽象メソッドを 1 つだけ持つ interface に fun を付けて宣言すると、ラムダからその interface の実装を作れます。ラムダを interface の実装に変えることを SAM 変換 と呼びます (SAM は Single Abstract Method の略です)。
fun interface DiscountPolicy {
fun discount(price: Int): Int
}
fun checkout(price: Int, policy: DiscountPolicy): Int = policy.discount(price)
fun main() {
val memberDiscount = DiscountPolicy { price -> price * 95 / 100 }
println(checkout(1000, memberDiscount))
println(checkout(1000) { it - 100 })
}
950
900
DiscountPolicy { price -> ... } は、ラムダを discount の実装にした DiscountPolicy の値を作ります。2 回目の呼び出しのように、引数の型が fun interface なら、ラムダをそのまま渡すこともできます。interface の宣言は クラスとオブジェクト で扱います。
同じことは、引数を関数型 (Int) -> Int にしても書けます。fun interface を選ぶのは、型に名前を付けて引数の役割を示したいときと、ラムダのほかにクラスでも実装させたいときです。fun interface には、抽象でないメソッドを足すこともできます。
Java で書かれた interface は、fun が無くても、抽象メソッドが 1 つなら SAM 変換できます。第 2 部で使う Spring のライブラリは Java で書かれているので、この形でラムダを渡す場面があります。
val notice = Runnable { println("在庫の引き当てを始めます") }
notice.run()
在庫の引き当てを始めます
Runnable は Java の標準ライブラリの interface で、引数も戻り値も無い抽象メソッド run を 1 つだけ持ちます。import は要りません。
typealias
typealias は、既存の型に別名を付けます。長い関数型や型引数の付いた型に、用途を表す名前を付けるときに使います。
typealias PriceRule = (Int) -> Int
typealias PriceTable = Map<String, Int>
fun priceOf(table: PriceTable, productName: String, rule: PriceRule): Int? {
val base = table[productName] ?: return null
return rule(base)
}
fun main() {
val table: PriceTable = mapOf("ノート" to 500, "ペン" to 200)
val halfPrice: PriceRule = { it / 2 }
println(priceOf(table, "ノート", halfPrice))
println(priceOf(table, "消しゴム", halfPrice))
val plain: (Int) -> Int = halfPrice
println(plain(300))
}
250
null
150
table[productName] は、マップにキーが無ければ null を返します。その場合は ?: の右側の return null で関数を抜けます。
別名は新しい型を作りません。PriceRule と (Int) -> Int はコンパイラにとって同じ型なので、最後の 2 行のように、PriceRule の値を (Int) -> Int の変数にそのまま代入できます。同じ Int でも単価と数量を取り違えないよう型で区別したいなら、別名でなく value class を使います。value class は クラスとオブジェクト で扱います。
拡張関数と拡張プロパティ
拡張関数
fun 型.関数名() と宣言すると、既存の型に関数を足したように、値.関数名() の形で呼べます。これを 拡張関数 と呼びます。
fun Int.withTax(): Int = this * 110 / 100
fun String.toOrderLabel(): String = "注文 $this"
fun main() {
println(1000.withTax())
println("A-1".toOrderLabel())
}
1100
注文 A-1
拡張関数の中では、. の前の値 (レシーバ) が this になります。Int や String のように、自分では書き換えられない型にも関数を足せます。
拡張関数は、型そのものを書き換えるわけではありません。コンパイラは 1000.withTax() を、1000 を引数に渡す普通の関数の呼び出しとして扱います。そのため、拡張関数から使えるのは型の外から見えるメンバーだけで、private のメンバーには触れません。宣言したパッケージの外で使うときは、関数と同じく import が要ります。
拡張プロパティ
プロパティも同じ形で足せます。
data class OrderLine(val productName: String, val unitPrice: Int, val quantity: Int)
val OrderLine.subtotal: Int
get() = unitPrice * quantity
fun main() {
val line = OrderLine("ノート", 500, 3)
println(line.subtotal)
}
1500
拡張プロパティでは、get() に値の求め方を書きます。get() の書き方は クラスとオブジェクト のカスタムアクセサで扱います。拡張プロパティは型に値の置き場所 (バッキングフィールド) を足せないので、初期値を書くとコンパイルエラーになります。
val OrderLine.discountRate: Int = 0
Extension property cannot be initialized because it has no backing field
メンバーとの優先と null のレシーバ
data class Product(val name: String, val price: Int) {
fun describe(): String = "メンバー: $name"
}
fun Product.describe(): String = "拡張: $name"
fun Product?.nameOrDefault(): String = this?.name ?: "商品なし"
fun findProduct(name: String): Product? =
if (name == "ノート") Product("ノート", 500) else null
fun main() {
val note = Product("ノート", 500)
println(note.describe())
println(findProduct("ノート").nameOrDefault())
println(findProduct("消しゴム").nameOrDefault())
}
メンバー: ノート
ノート
商品なし
名前と引数が同じメンバー関数と拡張関数があると、メンバー関数が呼ばれます。実行結果の 1 行目のとおり、既存のメンバーを拡張関数で置き換えることはできません。コンパイラも、メンバーに隠れる拡張関数の宣言に警告を出します。
レシーバの型を Product? にすると、null に対しても呼べる拡張関数になります。中では this が null かもしれないので、?. や ?: で扱います。呼び出し側は、findProduct が null を返す場合でも ?. を付けずに nameOrDefault() を呼べます。
スコープ関数
標準ライブラリの let、run、with、apply、also は、ある値を対象にしてラムダを 1 つ実行する関数です。まとめて スコープ関数 と呼びます。5 つの違いは、ラムダの中で対象を何と呼ぶかと、何を返すかの 2 点です。
| 関数 | 呼び方 | ラムダの中の対象 | 返す値 |
|---|---|---|---|
let | x.let { ... } | it | ラムダの結果 |
run | x.run { ... } | this | ラムダの結果 |
with | with(x) { ... } | this | ラムダの結果 |
apply | x.apply { ... } | this | x そのもの |
also | x.also { ... } | it | x そのもの |
対象が this になる 3 つは、レシーバ付きの関数型 でラムダを受け取ります。
let
let は、null かもしれない値が null でないときだけ処理をして、その結果を使う場面でよく使います。
fun couponDiscount(couponCode: String?): Int =
couponCode?.let { code -> if (code.startsWith("SPRING")) 300 else 100 } ?: 0
fun main() {
println(couponDiscount("SPRING2026"))
println(couponDiscount("WELCOME"))
println(couponDiscount(null))
}
300
100
0
couponCode?.let { ... } は、couponCode が null なら let を呼ばずに式全体が null になり、?: の右側の値が使われます。null でなければ、ラムダの引数 code は null を許さない String 型になり、ラムダの結果が let の戻り値になります。
run と with
data class OrderLine(val productName: String, val unitPrice: Int, val quantity: Int)
fun main() {
val line = OrderLine("ノート", 500, 3)
val summary = with(line) {
"$productName × $quantity、小計 ${unitPrice * quantity} 円"
}
println(summary)
val subtotal = line.run { unitPrice * quantity }
println(subtotal)
val total = run {
val shippingFee = 500
subtotal + shippingFee
}
println(total)
}
ノート × 3、小計 1500 円
1500
2000
with(line) { ... } と line.run { ... } は、どちらも line を this にしてラムダを実行し、ラムダの結果を返します。ラムダの中では、productName や quantity を line. なしで書けます。2 つの違いは呼び方で、run は x?.run { ... } のように null の場合と組み合わせられます。
対象を取らない run { ... } もあります。複数の文を 1 つの式にまとめ、途中で使う変数 (shippingFee) をその中に閉じ込めます。
apply と also
data class OrderDraft(var deliveryNote: String = "", var giftWrap: Boolean = false)
fun main() {
val draft = OrderDraft().apply {
deliveryNote = "置き配"
giftWrap = true
}
println(draft)
val confirmed = draft.also { println("確定する下書き: ${it.deliveryNote}") }
println(confirmed === draft)
}
OrderDraft(deliveryNote=置き配, giftWrap=true)
確定する下書き: 置き配
true
apply は対象を this にしてラムダを実行し、対象そのものを返します。作ったばかりのオブジェクトのプロパティを続けて設定するときに使います。
also は対象を it で受け取り、apply と同じく対象そのものを返します。値を変えずに、ログの出力のような処理を途中に挟むときに使います。実行結果の最後の行のとおり、also が返した confirmed は draft と同じオブジェクトです。=== は Kotlin の基本 で扱った、同じオブジェクトかどうかを比べる演算子です。
スコープ関数の選び方
どれを使っても書ける場面は多いので、目的で選びます。
- null でないときだけ処理をして、その結果を使うなら
?.letを使います - オブジェクトを作って設定し、そのオブジェクトを使い続けるなら
applyを使います - 値はそのままにして、途中で別の処理を挟むなら
alsoを使います - 1 つの値のメンバーを続けて使って結果を求めるなら、
withかrunを使います
スコープ関数を入れ子にすると、this や it がどの値を指すのかを読み取りにくくなります。入れ子にしたくなったら、ローカル変数に分けるか、it の代わりに引数に名前を付けてください。
inline 関数
ラムダを呼び出し側に展開する
関数に inline を付けると、コンパイラは呼び出しの場所に、その関数の本体と、渡したラムダの本体を書き写します。ラムダを値として作らずに済むので、ラムダを受け取る小さな関数を何度も呼ぶ場合の負担が減ります。
inline fun step(label: String, block: () -> Unit) {
println("開始: $label")
block()
println("終了: $label")
}
fun reserve(quantities: List<Int>): Boolean {
step("在庫の引き当て") {
for (quantity in quantities) {
if (quantity <= 0) return false
}
}
return true
}
fun main() {
println(reserve(listOf(1, 2)))
println(reserve(listOf(1, 0)))
}
開始: 在庫の引き当て
終了: 在庫の引き当て
true
開始: 在庫の引き当て
false
reserve の中の return false は、step に渡したラムダの中にあります。それでもこの return は reserve から抜けます。実行結果の後半のとおり、2 回目の呼び出しでは step の最後の println が実行されないまま、reserve が戻っています。
非ローカル return が書けるのは、展開した後の形を考えると分かります。コンパイルした後の reserve は、step の本体とラムダの本体を自分の中に書き写した形になります。ラムダの中にあった return false も reserve の本体の一部になるので、reserve から抜けられます。ラベルの無い return の節の forEach も inline 関数なので、同じ理由で外側の関数を抜けられました。inline でない関数に渡したラムダは値として作られ、受け取った関数がいつ呼ぶか分からないので、外側の関数を抜ける return を書けません。
inline が意味を持つのは、主に関数型の引数を持つ関数です。もう 1 つ、型パラメータに reified を付ける関数は、inline にしないと書けません。こちらは コレクションとジェネリクス で扱います。関数型の引数も reified の型パラメータも持たない関数に付けると、コンパイラは効果が見込めないという警告を出します。
crossinline
inline 関数の中で、受け取ったラムダを、さらに別のラムダの中から呼ぶことがあります。
inline fun afterCommit(crossinline action: () -> Unit): () -> Unit {
return {
println("コミットしました")
action()
}
}
fun main() {
val notify = afterCommit { println("確認メールを送ります") }
println("登録を続けます")
notify()
}
登録を続けます
コミットしました
確認メールを送ります
afterCommit は、action を呼ぶ新しいラムダを作って返します。実行結果の 1 行目のとおり、afterCommit を呼んだ時点では action は動かず、返したラムダを notify() で呼んだときに動きます。action の本体が展開される先は、main の本体ではなく、この新しいラムダの本体です。そこに main から抜ける非ローカル return があっても、新しいラムダの中から main を抜けることはできません。返したラムダは、main が終わった後で呼ばれることもありえます。
crossinline は、その引数のラムダに非ローカル return を書かせないという指定です。crossinline を付けずに action を別のラムダの中で呼ぶと、コンパイルエラーになります。
noinline
inline 関数が受け取ったラムダは、展開されるので値としては存在しません。そのため、そのままではリストに入れることも、inline でない関数に渡すこともできません。値として扱いたい引数には noinline を付けます。
inline fun placeOrder(
orderId: String,
tasks: MutableList<() -> Unit>,
check: () -> Boolean,
noinline onPlaced: () -> Unit,
) {
if (!check()) {
println("$orderId は登録できません")
return
}
println("$orderId を登録しました")
tasks.add(onPlaced)
}
fun main() {
val tasks = mutableListOf<() -> Unit>()
placeOrder("A-1", tasks, { true }) { println("A-1 の確認メールを送ります") }
placeOrder("A-2", tasks, { false }) { println("A-2 の確認メールを送ります") }
println("登録の後で、残した処理を実行します")
for (task in tasks) {
task()
}
}
A-1 を登録しました
A-2 は登録できません
登録の後で、残した処理を実行します
A-1 の確認メールを送ります
onPlaced は後で実行するために tasks に入れるので、値として作られている必要があります。noinline を付けた onPlaced だけが普通のラムダとして渡され、check はこれまでどおり展開されます。noinline の引数は値なので、そのラムダの中には非ローカル return を書けません。MutableList は要素を足せるリストで、コレクションとジェネリクス で扱います。
3 つの指定をまとめると、次の表になります。
| 引数の指定 | ラムダの扱い | ラムダの中の非ローカル return |
|---|---|---|
| (指定なし) | 呼び出し側に展開する | 書ける |
crossinline | 展開するが、別のラムダの中からも呼べる | 書けない |
noinline | 展開せず、値として渡す | 書けない |
迷ったら指定なしで書き始めてください。ラムダを別のラムダの中で呼ぶとコンパイラが crossinline を求め、値として使うと noinline を求めるので、その指示に従えば足ります。
まとめ
- 関数は
funで宣言します。本体が 1 つの式なら=で書く単一式関数にできます - デフォルト引数で省ける引数を作り、名前付き引数で何を渡したかを呼び出し側に書きます。
varargは 0 個以上の値を配列として受け取り、*で配列を展開して渡せます - 関数型は
(引数の型) -> 戻り値の型と書き、ラムダで値を作ります。最後の引数のラムダは括弧の外に書けます - ラムダの中のラベルの無い
returnは外側の関数から抜けようとするので、inline 関数に渡したラムダの中でしか書けません。ラムダだけを抜けるときはreturn@ラベルを使います ::関数名で関数参照を作れます。fun interfaceと、抽象メソッドが 1 つの Java の interface は、ラムダから実装を作れますtypealiasは型に別名を付けるだけで、新しい型は作りません- 拡張関数と拡張プロパティは既存の型に関数を足したように見せますが、
privateのメンバーには触れず、バッキングフィールドも持てません - スコープ関数は、対象を
thisとitのどちらで呼ぶかと、何を返すかで選びます inlineは関数の本体とラムダを呼び出し側に展開します。別のラムダの中で呼ぶ引数にはcrossinline、値として使う引数にはnoinlineを付けます
次に読む
次章 クラスとオブジェクト では、この章で最小限の宣言だけ使った data class と interface を含め、クラスの宣言を扱います。拡張プロパティで使った get() の書き方も、そこで扱います。前の章は Kotlin の基本 です。
練習問題
数量が 0 の明細を飛ばして残りを表示するつもりで、次の関数を書きました。実行結果が意図と違う理由を説明し、直してください
fun printLines(quantities: List<Int>) {
quantities.forEach { quantity ->
if (quantity == 0) return
println("数量 $quantity")
}
println("表示を終えました")
}
fun main() {
printLines(listOf(2, 0, 1))
}
数量 2
解答例
fun printLines(quantities: List<Int>) {
quantities.forEach { quantity ->
if (quantity == 0) return@forEach
println("数量 $quantity")
}
println("表示を終えました")
}
fun main() {
printLines(listOf(2, 0, 1))
}
数量 2
数量 1
表示を終えました
元のコードの return にはラベルが無いので、ラムダでなく外側の printLines から抜けます。forEach は inline 関数なので、この非ローカル return はコンパイルを通ります。数量が 0 の要素に来た時点で printLines が終わるため、元の実行結果は 1 行で止まり、最後の println も実行されていません。
ラムダの今回の呼び出しだけを終わらせるには、return@forEach と書きます。forEach は次の要素へ進み、すべての要素を見終えてから最後の println を実行します。
注文明細 OrderLine の小計を拡張プロパティ subtotal で求め、明細のリストの合計に送料を足す関数 orderTotal を書いてください。送料はデフォルト引数で 500 にし、呼び出し側で変えられるようにします
解答例
data class OrderLine(val productName: String, val unitPrice: Int, val quantity: Int)
val OrderLine.subtotal: Int
get() = unitPrice * quantity
fun orderTotal(lines: List<OrderLine>, shippingFee: Int = 500): Int {
var total = 0
for (line in lines) {
total += line.subtotal
}
return total + shippingFee
}
fun main() {
val lines = listOf(OrderLine("ノート", 500, 3), OrderLine("ペン", 200, 2))
println(orderTotal(lines))
println(orderTotal(lines, shippingFee = 0))
}
2400
1900
subtotal は、値を持たずに get() で毎回計算する拡張プロパティです。初期値を書くとバッキングフィールドが要るのでコンパイルエラーになる、という本文の規則に沿っています。
shippingFee はデフォルト引数なので、1 回目の呼び出しのように省けます。送料を無料にするときは、orderTotal(lines, 0) と書いても動きますが、shippingFee = 0 と名前を付けると、0 が何の値かを呼び出しだけで読めます。
合計を for で足し上げている部分は、コレクションとジェネリクス で扱う sumOf を使うと lines.sumOf { it.subtotal } の 1 行で書けます。