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' 修飾子はこの関数には適用できません。メンバーか拡張関数ではないからです。

関数スコープ[編集]

[TODO]

可変長引数[編集]

Kotlin で可変長引数( variable-length arguments )を持つ関数を定義するには、キーワード vararg とスプレッド演算子( spread operator[4] )を使います。

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

fun myVaPrint(vararg values: String) {
    for (s in values)
        println(s)
}
実行結果
abc
def 
xyz

[TODO:スプレッド演算子]

高階関数[編集]

引数あるいは戻値あるいは両方が関数の関数を高階関数()と呼びます[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[編集]

[TODO: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