とにかくやるブログ

とにかくやるブログ

プログラムの備忘録とその他雑記を適当にやるブログ

【kotlin】関数についてのお話

表題の通り、まず「関数」周りの章をやっていてのメモ書き。



Javaの参考書とかに比べて関数・ラムダについての機能やらが手厚い印象ですね。







・単一式関数

コード量を減らすテクニック的なもの
自分で使うことはあまりないかもだけど
他人のコードを読んだ時に慌てないようにメモ

fun myFunction(int : Int): String{
        val test = if (int == 0){
            "it is ZERO"
        }else{
            "it is not ZERO"
        }
        return test
    }


上のコードは以下の形で書き換えることができる。

fun myFunction(int : Int) =
        if (int == 0) {
            "it is ZERO"
        }else{
            "it is not ZERO"
        }


今までの自分の感覚だと
「関数は何かを返すためのもの」
というイメージだったのが、このコードを見ると


なんというか、関数を変数とかと同じように定義している?みたいな?
うまく説明できないけど関数に対する新しいイメージをつかめました。







・Nothing型

Unit型の説明に付随して説明されました。
なかなかにふーんと思ったのでメモ。

Nothing型の関数は
コンパイラに関数が決して正常に完了しないことを保証する」関数です。


一体こんなもん何に使うのかというと
interfaceとかをクラスに実装した時に出てくるTODO()で使っています。


/**
 * Always throws [NotImplementedError] stating that operation is not implemented.
 */

@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

豆知識みたいなもんですが、面白かったので。







・無名関数

書には「kotlinの本質的な部分」とあります。
それほど大事なことなのです。


この無名関数でできることの一つは
「標準ライブラリ関数にルールを追加することで、その振る舞いをカスタマイズできる」


どういうことなのか見ていきます。

val test = "kokinwakashu".count ({letter -> 
            letter == 'k'
        })

ここのcount関数を呼び出すのに使っているドットは「ドット構文」(そのまんま)という。
ドット構文は型の一部として含まれている関数を呼び出すのに使う。


このコードでは変数testに"kokinwakashu"というStringに含まれている"k"の数を代入している。




ここでcount関数のコードを見てみる。

/**
 * Returns the number of characters matching the given [predicate].
 */
public inline fun CharSequence.count(predicate: (Char) -> Boolean): Int {
    var count = 0
    for (element in this) if (predicate(element)) ++count
    return count
}

ここで注目して欲しいのは、count関数の引数の型。
この"predicate: (Char) -> Boolean"はブーリアン型を返す無名関数を意味する。
つまり、このメソッドは引数としてboolean型を返す関数を要求している。

この無名関数は前述のコードで言うところの"letter == 'k'"にあたる。



無名関数についてのもっとわかりやすい例は以下

println({
     val day = "0707"
     "hello!! today is $day"
}())

{}で括られた箇所が関数の定義

これまで関数は名前をつけて定義した上で使っていたが
その作業を省略して、直接使っているようなイメージ。


この無名関数も実はIntやStringのような型を持っており
その型の名前を"関数型"という。



他の型と同様に関数型も変数を定義することが可能で
その方法で先ほどのコードを書き直すと以下になる。

val sayHelloWithDate: () -> String = {
    val day = "0707"
    "hello!! today is $day"
}
println(sayHelloWithDate())


sayHelloWithDate()メソッドでは
String型を返しているが、無名関数の場合は"return"キーワードを使わない。

無名関数においては暗黙的に関数定義の最後の行を返す
そのため、基本的には"return"キーワードは許容されない。







・無名関数に引数を渡す

先ほどの関数の引数を渡してみる。

val sayHelloWithDate: (String) -> String = {name ->
    val day = "0707"
    "hello, $name!! today is $day"
}
println(sayHelloWithDate("taro"))

引数としてString型を受け取れるようにした。

まず型定義で一つのStringを受け取ることを指定。
そのパラメータを関数内で扱う際の名前を、開き波カッコの直後に定義。
名前の後に矢印を書いて、以降に関数処理を書いていく。



また、引数が一つだけの場合は"it"キーワードを使うことで
引数の名前の定義を簡略化することができる。

val sayHelloWithDate: (String) -> String = {
    val day = "0707"
    "hello, $it today is $day"
}
println(sayHelloWithDate("taro"))

"it"は暗黙的にパラメータを指す。


コード量を減らす上では有効だが
多用するとコードが読みにくくなるので
使いどころには注意が必要。




複数の引数を受け取る場合は以下となる。

val sayHelloWithDate: (String, Int) -> String = { name, year ->
    val day = "0707"
    "hello, $name today is $year $day"
}
println(sayHelloWithDate("taro", 2017))

引数が複数ある場合は"it"は使えない。








型推論を使う

型推論を使うことで無名関数の返り値を明示せずに使うこともできる。


型推論なし

val sayHelloWithDate: () -> String = {
    val day = "0707"
    "hello!! today is $day"
}


型推論あり

val sayHelloWithDate = {
    val day = "0707"
    "hello!! today is $day"
}

引数がある場合は書き方がちょっと変わるので注意。

val sayHelloWithDate = { name: String, year: Int ->
    val day = "0707"
    "hello, $name today is $year $day"
}

・関数を受け取る関数の定義

    val sayHelloWithDate: (String, Int) -> String = { name, year ->
        val day = "0707"
        "hello, $name today is $year $day"
    }
    
    fun aisatsu(name: String, sayHello: (String, Int) -> String ){
        val year = 1999
        println(sayHello(name,year))
    }
    
    fun test(){
        aisatsu("emi", sayHelloWithDate)
    }

sayHelloWithDate が無名関数
aisatsuメソッドがString型と「第一引数にStringを第二引数にIntをとる無名関数」を引数にとる。
実際に使う際はtestメソッドの中のような感じで。








・インライン化

無名関数(ラムダ)は便利だが利用するのにはコストがかかる。
ラムダは一つ定義すると、JVM上で1個のインスタンスオブジェクトとして表現される。
また、ラムダが利用できる全ての変数に対してメモリ割り当てを実行するので、メモリのコストが発生する。
その結果、ラムダによるメモリのオーバーヘッドが加わり、性能に影響が及ぶ可能性もある。


このリスクを回避するために用いるのが"インライン化"である。

インライン化する方法は簡単で、ラムダを受け取る関数にinlineキーワードをつけるだけだ。

  inline fun aisatsu(name: String, sayHello: (String, Int) -> String ){
        val year = 1999
        println(sayHello(name,year))
    }

こうすることでオーバーヘッドのリスクを回避することができる。

しかし、このインライン化は使用できない場合もある。
例えばラムダを受け取るのが再帰関数の場合などはインライン化を行うことはできない。








・関数リファレンス

名前付き関数を引数として用いたい場合は関数リファレンスを使う。

  val sayHelloWithDate: (String, Int) -> String = { name, year ->
        val day = "0707"
        "hello, $name today is $year $day"
    }

    fun aisatsu(name: String, testPrintMethod: (String) -> Unit ,sayHello: (String, Int) -> String ){
        val year = 1999
        testPrintMethod("")
        println(sayHello(name,year))
    }

    fun testMethod(test: String){
        println(test)
    }

    fun test(){
        aisatsu("taro", ::testMethod, sayHelloWithDate )
    }

名前付き関数を引数として使いたい場合の引数の書き方は
aisatsuメソッドの第二引数。
aisatsuメソッドを実際に使う場合の書き方はtestメソッドにて。












今回はここまで。
関数型はkotlinを使う上では重要だが
書き方がちょっと特殊な感じがするので
慣れるまでに時間がかかりそうだなーと。。。