コンテンツにスキップ

Kotlin/関数

出典: フリー教科書『ウィキブックス(Wikibooks)』

このページは、親コンテンツである「Kotlin」の中から {{:Kotlin/関数}} の形式で展開されることを意図して書かれています。

この手法は

  1. ページ分割すると、[[#inline|inline]] のようなページ内リンクが大量に切れる。
  2. ページ分割すると、<ref name=foobar /> のような名前のついた参照引用情報が大量に切れる。
  3. スマートフォンやタブレットではページ遷移は好まれない。
  4. MediaWikiは、圧縮転送に対応しているので1ページのサイズが大きくなるのはトラフィック的には問題が少なく、ページ分割によりセッションが多くなる弊害が大きい。
  5. 編集はより小さなサブパート(このページ)で行える。

という技術的背景があります。

Kotlinのサブページ

関数

[編集]

関数は、キーワード fun を使って定義します[1]

関数定義

[編集]
関数定義と呼出しの例
fun ipow(base: Int, times: UInt) : Int {
    var result = 1
    var i = 0U
    while (i < times) {
        result *= base
        i++
    }
    return result
}

fun main() {
    println("ipow(2, 3) = ${ipow(2, 3U)}")
    println("ipow(10, 4) = ${ipow(10, 4U)}")
}
実行結果
ipow(2, 3) = 8 
ipow(10, 4) = 10000
関数 ipow 定義の冒頭
fun ipow(base: Int, times: UInt) : Int {
Intの仮引数 base と、UIntの仮引数 times を受取り Int の戻値を返すと読めます。
UIntに下のは、マイナスの指数を整数の累乗で扱いたくなかったため、3U や 4U のような符号なし整数リテラルの例にもなってます。
main() の中で ipow(2, 3U)ipow(10, 4U)の様に呼出しています。
仮引数と実引数の型の一致は、符号まで求められます。
関数定義の構文(1)
fun 関数名(仮引数リスト) : 戻値型 {
    // 文 …
    return 戻値式
}
さて、main関数はこの構文から逸脱しています。: 戻値型がありませんし、return 戻値式も見当たりません。
return 戻値式を省略したときの関数の戻値型は Unit になります。
また、: Unitは省略可能です。ということで main の定義はさっぱりしたものになります。

ボディが単一の式からなる関数定義

[編集]

関数のボディが単一の式からなる場合、{ return 式 } を、= 式と書くことができます[2]

関数定義の構文(2)
fun 関数名(仮引数リスト) : 戻値型 = 式
ボディが単一の式からなる関数定義の例
fun add2(n: Int) : Int = 2 + n 

fun main() {
    println("add2(3) = ${add2(3)}")
}
実行結果
add2(3) = 5

初見だと驚きますが、関数型プログラミング風の書き方が簡素にできます。 特に、ifやwhenが値を返すことができる式であることが効いてきます。

戻値型の省略

[編集]

ボディが単一の式からなる関数定義では、戻値式の型が推論できる場合が多いので、戻値型を省略できる場合があります。

戻値型の省略
fun add2(n: Int) = 2 + n 

fun main() {
    println("add2(3) = ${add2(3)}")
}

再帰関数は戻値型を省略できない

[編集]

再帰関数の戻値型を省略しようとすると、自分自身が型不明な項になりコンパイルできません。

戻値型の省略
fun power(n: Int, i: Int) = when {
    i < 0 -> throw Exception("Negative powers of integers cannot be obtained.")
    i == 0 -> 1
    i == 1 -> n
    else -> n * power(n, i - 1)
}

fun main() {
    (0 .. 7).forEach{
        println("power(2, $it) => ${power(2, it)}")
    }
}
コンパイル結果
Main.kt:5:17: error: type checking has run into a recursive problem. Easiest workaround: specify types of your declarations explicitly
    else -> n * power(n, i - 1) 
                ^
意訳
【エラー】型チェックで再帰問題が発生しました。最も簡単な回避策は、宣言の型を明示的に指定することです。
戻値型を明示
fun power(n: Int, i: Int) : Int = when {
    i < 0 -> throw Exception("Negative powers of integers cannot be obtained.")
    i == 0 -> 1
    i == 1 -> n
    else -> n * power(n, i - 1)
}

fun main() {
    (0 .. 7).forEach{
        println("power(2, $it) => ${power(2, it)}")
    }
}
実行結果
power(2, 0) => 1
power(2, 1) => 2
power(2, 2) => 4
power(2, 3) => 8
power(2, 4) => 16
power(2, 5) => 32
power(2, 6) => 64 
power(2, 7) => 128

引数はイミュータブル

[編集]

関数の引数はイミュータブルです。 これは Zig も同じで、新興言語は不用意な書換えによる古参言語で度々アクシデントのもととなった引数の破壊を永久になくしたいようです。

引数のディフォルト値

[編集]

関数には、引数にディフォルト値を設定できます。これにより、呼び出し側は引数を指定しなくても関数を呼び出すことができます。

たとえば、次の関数は、名前と年齢の2つの引数を取ります。名前には「John Doe」というデフォルト値が設定されています。

fun sayHello(name: String = "John Doe", age: Int) {
    println("Hello, $name! You are $age years old.")
}

この関数を呼び出すには、名前を指定するか、デフォルト値を使用できます。

sayHello() // Hello, John Doe! You are 0 years old.
sayHello("Jane Doe") // Hello, Jane Doe! You are 0 years old.

関数の引数にデフォルト値を設定すると、呼び出し側がすべての引数を指定する手間を省くことができます。また、関数の使い方を覚えやすくすることもできます。

関数呼出し

[編集]

関数指向の構文

[編集]
関数名(実引数リスト)

次のコードは、関数指向の構文を使用して関数を呼び出す方法を示しています。

fun main(args: Array<String>) {
     val sum = add(1, 2)
     println(sum) // 3
 }
 
 fun add(x: Int, y: Int): Int {
     return x + y
 }

このコードは、add 関数を呼び出して、2つの引数 12 を渡します。add 関数は、これらの引数を受け取った後、それらを加算して結果を返します。関数 main は、結果をコンソールに出力します。

メソッド指向の構文

[編集]
インスタンス.関数名(実引数リスト)
メソッドあるいは拡張関数の呼出しはドット記法になります。

次のコードは、メソッド指向の構文を使用して関数を呼び出す方法を示しています。

fun main(args: Array<String>) {
    val person = Person("John Doe", 30)
    println(person.greet()) // Hello, John Doe!
}
 
class Person(val name: String, val age: Int) {
    fun greet() = "Hello, $name!"
}

このコードは、Person クラスのインスタンスを作成し、name プロパティに John Doe という値、age プロパティに 30 という値を設定します。次に、greet メソッドを呼び出して、インスタンスの名前を出力します。

関数引数のある構文

[編集]
関数呼出しの構文(2)
関数名(実引数リスト) 関数型実引数
関数呼出しの構文(2’)
インスタンス.関数名(実引数リスト) 関数型実引数
メソッドあるいは拡張関数の呼出しはドット記法になります。
関数にブロック(に擬態したラムダ関数)を渡す
fun Int.times(action: (Int) -> Unit) = (0 ..< this).forEach(action)

fun main() {
    5.times{
        println("Hello -- $it")
    }
}
実行結果
Hello -- 0
Hello -- 1
Hello -- 2
Hello -- 3
Hello -- 4
RubyInteger#timesをKotlinに移植してみました。
this の数だけ関数仮引数 action を実行します。
呼出は 42.times{ /* Action */ } の形式になります。
この機能や infix 関数修飾子・ラムダ関数拡張関数のおかげで Kotrin は、あたかも「構文を後からプログラマーが拡張することができる言語」のように振舞います。

仮引数の名前を使った実引数の指定

[編集]

Kotlin は関数を呼出す時、引数を名前で指定する事ができます。

キーワード引数
fun main() {
    val ary = Array(3){it}
    println(ary)
    println(ary.toString())
    println(ary.joinToString())
    println(ary.joinToString("A", "B", "C"))
    println(ary.joinToString(prefix="🌞", separator="⭐", postfix="🌛"))
}
[Ljava.lang.Integer;@5ca881b5
[Ljava.lang.Integer;@5ca881b5
0, 1, 2
B0A1A2C 
🌞0⭐1⭐2🌛
Arrayクラスのインスタンスを println() に渡すとワヤクチャな文字列を表示します。
これは、Any.toString() をオーバーライドした Array.toString() が暗黙に呼出された結果です。
Array.joinString() を使うと、0, 1, 2 と表示されます
Array.joinString() は、先頭・区切り・末尾を引数で指定できます。
…区切り・先頭・末尾の順だったようです。
このように、引数の順序と意味を正確に覚えておくのは面倒なので、prefix= の様に関数定義の仮引数の名前で実引数を指定することができます。

infix

[編集]

関数修飾子 infix を使うと、中置表現の関数呼出しを行うことができるようになります。外観は文法を拡張したかのような印象をうけます。

実引数1 関数名 実引数2
infix 関数修飾子で修飾された関数は、中置表現での呼出しができます。
infix な関数の呼出し例
fun main() {
    val r = 4 downTo 0
    println("r => $r")
    println("r::class.simpleName => ${r::class.simpleName}")
    r.forEach{
        println("Hello -- $it")
    }

    val q = 0.downTo(-4)
    q.forEach{
        println("Goodbye -- $it")
    }
}
実行結果
r => 4 downTo 0 step 1
r::class.simpleName => IntProgression
r => 4 downTo 0 step 1
r::class.simpleName => IntProgression
Hello -- 4
Hello -- 3
Hello -- 2
Hello -- 1
Hello -- 0
Goodbye -- 0
Goodbye -- -1
Goodbye -- -2
Goodbye -- -3 
Goodbye -- -4
downTo は、二項演算子に擬態していますが infix fun Int.downTo(to: Byte): IntProgression と宣言された拡張関数です[3]
4 downTo 04.downTo(0) と同じ意味です。
infix 修飾できるのは、メソッドあるいは拡張関数です。
一般の関数に infix を適用しようとすると
infix fun mult(n: Int, m: Int) : Int = n * m

fun main() {
    println("12 mult 2 => ${12 mult 2}")
}
コンパイル結果
Main.kt:1:1: error: 'infix' modifier is inapplicable on this function: must be a member or an extension function
infix fun mult(n: Int, m: Int) : Int = n * m
^
Main.kt:4:32: error: unresolved reference: mult
    println("12 mult 2 => ${12 mult 2}") 
^
意訳
’infix' 修飾子はこの関数には適用できません。メンバーか拡張関数ではないからです。

関数スコープ

[編集]

関数スコープは、変数や関数が定義され、利用可能な範囲を指します。Kotlinでは、関数スコープ内で定義された変数や関数はそのスコープ内でのみアクセス可能で、外部のスコープからは見えません。

以下に、関数スコープの基本的な特徴と例を示します。

関数スコープの特徴

[編集]
  1. 変数の有効範囲: 関数スコープ内で宣言された変数は、その関数内でのみ有効です。関数外からはアクセスできません。
  2. 関数の有効範囲: 同様に、関数スコープ内で宣言された関数もそのスコープ内でのみ呼び出すことができます。
  3. 変数のシャドーイング: 関数スコープ内で同名の変数を再宣言すると、外部の変数は一時的に「シャドーイング」され、関数内の変数が優先されます。

関数スコープの例

[編集]
fun main() {
    // 外部のスコープ
    val outerVariable = "I am outside!"

    println(outerVariable) // 外部の変数にアクセス

    myFunction()

    // 関数外からは関数スコープ内の変数や関数にアクセスできない
    // println(innerVariable) // コンパイルエラー
    // innerFunction() // コンパイルエラー
}

fun myFunction() {
    // 関数スコープ
    val innerVariable = "I am inside!"

    println(innerVariable) // 関数内から外部の変数にアクセス

    fun innerFunction() {
        println("I am an inner function!")
    }

    innerFunction() // 関数内から関数を呼び出し
}

この例では、main 関数が外部のスコープで、myFunction が関数スコープ内で宣言されています。main 関数内では outerVariable にアクセスでき、myFunction 内では innerVariableinnerFunction にアクセスできます。ただし、逆は成り立ちません。

関数スコープは、変数や関数の可視性を制御し、プログラムの構造を整理する上で重要な役割を果たします。関数ごとにスコープが分離されるため、変数や関数の名前の衝突を防ぎ、コードの保守性を向上させます。

可変長引数

[編集]

可変長引数( Variable-Length Arguments )は、関数が異なる数の引数を受け入れることを可能にする Kotlin の機能です。これは vararg キーワードとスプレッド演算子( spread operator[4] )を使用して実現されます。

可変長引数のコード例
fun main() {
    myVaPrint("abc", "def", "xyz")
}

fun myVaPrint(vararg values: String) {
    for (s in values)
        println(s)
}
上記の例では、myVaPrint 関数が可変長引数 values を受け入れるように定義されています。この関数は、与えられた引数を文字列として順番に出力します。
実行結果
abc
def 
xyz

このように、関数を呼び出す際に異なる数の引数を渡すことができます。可変長引数は、同じ型の引数が複数個ある場面で便利です。

スプレッド演算子

[編集]

スプレッド演算子( Spread Operator )は、リストや配列などの要素を展開して、可変長引数として渡すための演算子です。

fun main() {
    val values = arrayOf("abc", "def", "xyz")
    myVaPrint(*values)
}

この例では、arrayOf で作成した配列の要素をスプレッド演算子 * を使って myVaPrint 関数に渡しています。これにより、配列の各要素が可変長引数として関数に渡されます。

可変長引数とスプレッド演算子の組み合わせにより、異なる数の引数を柔軟に扱うことができ、関数の再利用性を向上させます。

高階関数

[編集]

引数あるいは戻値あるいは両方が関数の関数を高階関数()と呼びます[5]

関数にブロック(に擬態したラムダ関数)を渡す
fun Int.times(action: (Int) -> Unit) = (0 ..< this).forEach(action)

fun main() {
    5.times{
        println("Hello -- $it")
    }
}
実行結果
パラメーター action が関数です。
型は(Int) -> Unit のように (引数型リスト) -> 戻値型
関数 times 本体の、(0 ..< this).forEach(action)整数範囲のメソッド forEach に関数 action を渡しているので、これも高階関数です。
forループで書くと
fun Int.times(action: (Int) -> Unit) {
    for (0 ..< this)
        action(it)
}
:: 長い!
このコードは関数呼出しからの再録です。

ラムダ

[編集]

ラムダ式( lambda expressions )では、波括弧の周囲と、パラメータと本体を分ける矢印の周囲に空白を使用する必要があります。ラムダを1つだけ指定する場合は、可能な限り括弧で囲んでください[6]

また、ラムダのラベルを指定する場合、ラベルと中括弧の間にスペースを入れてはいけません。

ラムダ式の例
fun main() {
	val pow = { x: Int -> x * x };
	println("pow(42) => ${pow(42)}");
}

無名関数

[編集]

上記のラムダ式構文には、関数の戻値の型を指定する機能がひとつだけ欠けています。ほとんどの場合、戻値の型は自動的に推測されるため、この指定は不要です。しかし、明示的に指定する必要がある場合は、別の構文として無名関数( Anonymous functions )を使用することができます[7]

無名関数の例
fun(a: Int, b: Int): Int = a * b
// あるいは
fun(a: Int, b: Int): Int {
  return a * b;
}
JavaScriptの関数リテラルと似ていますが、JSには戻値型はないので動機が違います(JSではラムダがthisを持てないので関数リテラルの出番があります)。

クロージャー

[編集]
Wikipedia
Wikipedia
ウィキペディアクロージャーの記事があります。

ラムダ式や無名関数(ローカル関数やオブジェクト式も同様)は、外部スコープで宣言された変数を含むクロージャー( Closures )にアクセスすることができます。クロージャーに取り込まれた変数は、ラムダ式で変更することができます[8]

クロージャーの例
fun main() {
    var sum = 0
    IntArray(10){2 * it - 10}.filter{ it > 0 }.forEach {
        sum += it
    }
    print(sum)
}

inline

[編集]

高階関数を使用すると、ある種の実行時ペナルティーが課せられます。各関数はオブジェクトであり、クロージャーを捕捉します。クロージャー( closure )とは、関数本体でアクセス可能な変数のスコープです。メモリー確保(関数オブジェクトとクラスの両方)と仮想呼出しは、実行時オーバーヘッドを発生させます[9]

しかし、多くの場合、ラムダ式をインライン化することで、この種のオーバーヘッドをなくすことができます。

関数呼出しのコードにinlineを前置
inline fun Int.times(action: (Int) -> Unit) = (0 ..< this).forEach(action)
この例は、拡張関数のインライン化です。

再帰的呼出し

[編集]

Kotlin は、特に修飾辞なしに関数を再帰呼び出しできます。

フィボナッチ
fun main() {
    var i = 1
    while (i < 30)  {
        println("$i! == ${fib(i)}");
        i++
    }
}

fun fib(n: Int) : Int = if (n < 2) n else fib(n - 1) + fib(n - 2)
実行結果
1! == 1
2! == 1
3! == 2
4! == 3
5! == 5
6! == 8
7! == 13
8! == 21
9! == 34
10! == 55
11! == 89
12! == 144
13! == 233
14! == 377
15! == 610
16! == 987
17! == 1597
18! == 2584
19! == 4181
20! == 6765
21! == 10946
22! == 17711
23! == 28657
24! == 46368
25! == 75025
26! == 121393
27! == 196418
28! == 317811 
29! == 514229
フィボナッチ数自体が再帰的な式なので、関数定義の式構文が使えました。

tailrec(末尾再帰最適化)

[編集]

tailrec は、Kotlinのキーワードで、末尾再帰最適化(Tail Recursion Optimization)を実現するために使用されます。末尾再帰最適化は、再帰関数が最後の操作として再帰呼び出しを行う場合に、スタックの消費を減少させる最適化手法です。これにより、スタックオーバーフローを避けることができます。

末尾再帰関数の例
fun main() {
    val result = factorial(5)
    println("Factorial: $result")
}

tailrec fun factorial(n: Int, accumulator: Long = 1): Long {
    return if (n == 0) {
        accumulator
    } else {
        factorial(n - 1, n * accumulator)
    }
}

上記の例では、factorial 関数が末尾再帰関数として宣言されています。再帰呼び出しは末尾で行われており、コンパイラが最適化を行うことが期待されます。

tailrec の制約

[編集]

tailrec を使用するためにはいくつかの制約があります。

  • 関数は再帰呼び出しの最後に自分自身を呼び出さなければなりません。
  • 再帰呼び出しの後に他の処理(例: 演算、代入)があってはいけません。
制約を満たさない例
// コンパイルエラー: "This call is not allowed here"
tailrec fun invalidFactorial(n: Int): Int {
    return if (n == 0) {
        1
    } else {
        n * invalidFactorial(n - 1) // 制約を満たしていない
    }
}
制約を満たす例
tailrec fun validFactorial(n: Int, accumulator: Int = 1): Int {
    return if (n == 0) {
        accumulator
    } else {
        validFactorial(n - 1, n * accumulator) // 制約を満たしている
    }
}

tailrec を使うと、再帰関数の性能を向上させることができます。ただし、制約を理解し、満たすことが重要です。tailrec が適用される場合、コンパイラはループに変換し、スタックの使用を最小限に抑えます。

  1. ^ JavaScriptのfunctionに相当し、C言語やJavaには関数定義用のキーワードはなく文脈から判断されます。
  2. ^ Single-expression functions
  3. ^ downTo
  4. ^ JavaScriptでは、スプレッド演算子と呼ばずスプレッド構文と呼ぶようになりました。
  5. ^ Higher-order functions
  6. ^ lambdas
  7. ^ Anonymous functions
  8. ^ Closures
  9. ^ Inline functions