情報と電子とおおかた遊び

プログラムの備忘録を綴りつつ、適当に遊びも記載するゆるいブログ

Kotlinのいろは3

関数

いよいよKotlinの関数についてやっていこうと思います。 できるだけ基本と、知っていれば便利程度のを乗っけていきます。 その後、必要に応じて、より高度な関数の使い方なども書いていこうかなと。 すべて網羅しようとすると結構大変なので(汗。

関数の定義

では基本の型です。

fun "関数名"("引数名" : "引数型"): "戻り値の型"{
    "処理"
}

ポイントは"fun"で始まるところですね。 スクリプト言語では結構、functionとかfuncで書き始めるというのは多いのですが、 "fun"ほど短いのは初めて見ます。思わず笑っちゃいそう(fun)ですね。

では、実際に何か書いてみましょう。

//引数二つを足し合わせる関数。
fun add(a : Int, b : Int) : Int {
    return a + b
}

個人的には、戻り値の型をパラメータの後に書くというのが、 いまいち慣れませんが、swiftなんかはこんな感じだっけかな?

デフォルト引数と名前付き引数

デフォルト引数というのは、あらかじめ関数定義時に定数の値を定義しておくことです。 そうすることで、関数の使用者が、その値をパラメーターとして渡さなくても、 関数はデフォルト値を利用して、普通に動作します。 関数にオプション機能などをつける際に使えますね。

//引数二つを加算か減算する
// optに何も指定されていないか、
// 0が指定された場合、加算
// 1 が指定されていた場合、減算する
fun addOrSubtract(a : Int, b : Int, opt : Int = 0) : Int {
    
    var result = -1;

    if(opt == 0){
        result = a + b
    }else if(opt == 1) {
        result = a - b
    }

    return result;
}

デフォルト値を決めるという機能とは別に、 どの引数名に対して値を渡しているかを、関数使用者は指定することができる。 上記の関数を使用することを前提に書いていく

//通常の使い方
println(addOrSubract(1,2,0)) // 3が出力される

// 値を指定するが順番をばらばらにする
println(addOrSubract(b = 1, a = 2, opt = 1)) // 1が出力される

可変長引数

引数の値を一つだけ、可変にすることが可能です。 関数側の処理では、可変長引数は配列と同じように扱います。 渡すほうは、単純にカンマ区切りで値を渡すか、配列を渡すことも一応できます。

// 引数にvarargとつける
fun sum(vararg ints : Int) : Int  {

    var result = 0;

    for(i in ints){
        result += i
    }
    return result
}

ではこの関数を使う際は、二通りの方法がある

// 任意の数の引数を入力
println(sum(1, 2, 3)) // 6 が出力

//配列を関数に渡す為、パラメータに渡す際に*をつける
val ints = intArrayOf(1, 2, 3) // intArrayOfにしないとタイプミスマッチが発生。 Array<Int> と引数で受け取れるIntArrayは別物らしい。
println(sum(*ints)) //6が出力

戻り値のない関数

どうやらKotlinには厳密な意味での、voidが戻り値の関数というのは存在しないらしい。 つまり必ず何かの値を返さないといけない。とはいえ常に、何かを返したいとも限らない。 そういう場合は、戻り値にUnitを指定する。

//パラメーターを出力する関数
fun printTest(string : String) : Unit{
    println(string)
}

さて、Unitと戻り値を指定した場合は、returnを書かなくてもエラーにならないで済みます。 ただし、厳密には戻り値が返ってくるようで、Unit型のオブジェクトがもらえます。 たいへんややこしい。この仕様・・・ちょっとひどくないか?

関数オブジェクト

関数と言ったり、関数オブジェクトと言ったりなんなのさ!と思われるかもしれません。 簡単に言うと、作った関数は、変数に代入することが可能です。 これを利用することで、関数のパーツ化等が容易になる場合があります。

val "変数名": ("引数の型") -> "戻り値の型"  = ::"関数名"

実際にはこのような形で代入できます。

fun test(str : String) : Int {
    return 1;
}

fun main(args: Array<String>) {
    val funcObject : (Strgin) -> Int = ::test
}

この機能を応用すると、変数のほかに関数の引数として、別の関数を渡すことなどもでき、 コールバック機能を持ったクラスや関数を作ることができます。

高階関数

直前に話した通り、関数に関数を渡すやり方です。 よりパーツ化されたクラスを作るには必須の知識となるでしょう。 また、Android開発などで、コールバック関数を設定してくださいとドキュメントに書かれていたら、 高階関数の使った実装がされたライブリリーなのではないかな?

// この関数は引数を二つ受け受け取り、戻り値にIntを返す関数を受け取る。
fun testHighOrderFunc( givenFunc : (Int, Int)->Int) : Int {
    var a = 1
    var b = 2
    return givenFunc(a, b)
}

//引数に何を受け取ったか出力し、引数同士を足し数を戻り値として返す。
fun sum(a : Int, b : Int) : Int{
    println("Given a = " + a + " b = " + b)
    return a+b
}

fun main(args: Array<String>) {
    var result = testHighOrderFunc(::sum)
    println(result) // 3が出力される
}

ラムダ式

これは書こうか迷ったんですが、昨今の言語は普通に導入し始めているので、 書くことにしました。直接、関数を生成するときなどに使える、方法の一つにラムダ式というのがります。 関数によっては、ローカルのスコープや、コールバックで呼ぶために一回ぽっきりしか使わないという場合があります。 その度に、再利用性の少ない関数を毎回作るのは正直無駄なので、そういう際はラムダ式が使えます。

Kotlinではラムダ式は以下のように書けます。

("引数型") -> "戻り値型" = { "引数名" : "引数型"
    "処理"
}

では実例を書いていきます。

//基本の書き方
val square : (Int) -> Int = { i : Int ->
    i * i
}

//型推論での書き方
val square = { i : Int ->
    i * i
}

//引数が一つだけの際に使用できる書き方(ITという変数が使えます)
val square : (Int) -> Int = {
    it * it
}

ラムダ式では、returnの考え方が変わるので、基本的には使えません。。 そのため、返される値は最後に評価される式の値で決まります。 はい、意味が分からないですよね。自分もそうです。 とりあえずこの場では、returnは使わなくても値が返る。その値は、最後の計算式で決まる、ということだけ覚えておいてください

じゃあどのケースならreturnが使えるのか? その説明をする前に一つ新しい概念を紹介します。

inline関数

名前だけ聞いてもよくわかりませんね。一行で書く関数のように聞こえます。

そもそも、関数を使用するというのは、システム的にはどういう手順が踏まれているのか? 簡単にいますと、関数の処理のための変数類のメモリの確保と、引数のコピーを作成がされます。

つまり、関数を呼ぶというプロセスは、システムリソース上決して安くはないということです。 もっと言えば、高階関数で、関数やら、ラムダ式を渡したりをすると、一時的とはいえ、 システムのリソースの使用量は増えます。

とはいえ、ぱああーつかの概念を考えると、できるだけ利用回数の多そうな処理は、関数化したいのが 真のプログラマです。じゃあどうすれば?

こうしましょう。

//インライン関数の宣言
inline fun sum(ints : Array<Int>) : Int  {

    var result = 0;

    for(i in ints){
        result += i
    }
    return result
}

さて、頭にinlineと就いただけですが、どうなるのか? もし、この関数が使われると、プログラムのコンパイル時(runボタンを押したとき)に、 コンパイラが、関数内部の処理を、関数の予備もとに直接処理を書き込んでくれます。

例えば下記のように先ほどのインライン関数を呼びます。

fun main(args: Array<String>) {
    var result = sum(arrayOf(1, 2, 3))
    println(result)
}

するとコンパイラが、バイトコードに直すときに、コードを書き直してくれます。 下記は多分こんな風になるというイメージです。

fun main(args: Array<String>) {
    var result = 0;
    var ints = arrayOf(1, 2, 3)

     for(i in ints){
        result += i
    }
    println(result)
}

そうすることで、関数を呼んでいるようで、処理を直接書き込んでいるだけという状況を作れるわけです。 メリットはリソースの削減(メモリ使用量)ができるため、短くて簡単だけど、汎用的な処理はインライン化をするといいでしょう。 デメリットとして、バイトコード自体が長くなるので、複雑な処理をインライン化すると、システムのストアレージを喰うことになるという点。

ここら辺のシステムのリソースの使用量のシビアさは、アプリの機能だったり、サーバーの使用者数の想定だったりで、 だいぶ変わってくるので、一概にインライン化すれば良いとか、悪いとかは言いにくいですけどね・・・

非ローカルへのリターンとラベルへのリターン

なんのこっちゃと思うタイトルですね。私も参考書読みながら、困惑しています(笑

先ほどの、ラムダ式でreturnが呼べるケース、呼べないケースに関わってきます。

非ローカルへのリターン

前提として、基本的にはKotlinのラムダ式には、returnは使わなくて大丈夫です。 なぜなら、呼ばなくても値が返るからです。値を返したかったら、最後に評価対象になる式を書いてください。 恐らくこれは、ラムダ式は関数ではなく、あくまでも一つの式だからという概念が根底にあるからです

では、いつどういう時ならreturnが使えるのか。 答えはインライン関数の中でです。 インライン関数に渡された、ラムダ式がreturnをしていると、呼び先の関数からではなく、 呼び元からreturnすることができます。これが、非ローカルへのリターンというわけです。 実際にコードを見てみましょう。

//インライン関数の文字列の文字単位でループを行い、関数に文字を渡す。
inline fun forEach(str : String, f: (Char) -> Unit){
    for(c in str){
        f(c)
    }
}

//文字列に数字があるか確認する関数
fun containDigit(str : String) : Boolean  {

    //forEachを呼び出す。
    //ここで定義しているラムダ式は、もし引数の文字が数字だったらtrueを返すというもの。
    //forEachがインライン関数なため、containDigitからreturnする。
    forEach(str, {char : Char ->
        if(char.isDigit())
            return true
    })
    return false
}

fun main(args: Array<String>) {
    println(containDigit("test1")); // trueが出力される。
}

因みに、下記のように書くとエラーになり、動きません。

//インライン関数の文字列の文字単位でループを行い、関数に文字を渡す。
inline fun forEach(str : String, f: (Char) -> Unit){
    for(c in str){
        f(c)
    }
}

//文字列に数字があるか確認する関数
fun containDigit(str : String) : Boolean  {

   //ラムダ式をあらかじめ定義する
   val test : (Char)->Unit = {
        if(it.isDigit())
            return true //エラーになります
   }
   
    forEach(str, test)
    return false
}

fun main(args: Array<String>) {
    println(containDigit("test1")); // trueが出力される。
}

上記でエラーになるのは、ラムダ式が、インライン関数で使われることが、このままでは保証されないため、 コンパイル段階でストップがかかります。

ラベルへのリターン

もう一つの方法で、ラベルへのリターンを使えば、インライン関数など関係なく、 returnをかますことができます。

方法としてはいつぞやのbreakとcontinueのラベルと同じです。 以下のコードを見てください。

//インライン関数の文字列の文字単位でループを行い、関数に文字を渡す。
fun forEach(str : String, f: (Char) -> Unit){
    for(c in str){
        f(c)
    }
}

//文字列に数字があるか確認する関数
fun containDigit(str : String) : Boolean  {

    //変数を初期化しておく
    var result = false
    
    //forEachを呼び出す。
    //returnするとhereの位置でreturnした扱いになるので、
    //forEachからのみreturnして、値のみが更新される。
    forEach(str, here@{char : Char ->
        if(char.isDigit()){
            result = true
            return@here
        }
    })

    //戻り値。ラムダ式内部の処理によってはtrueになっている
    return result
}

fun main(args: Array<String>) {
    println(containDigit("test1")); // trueが出力される。
}

汎用的な面では、ラベルを使うのだけど、ここまでしてreturnしたいのであれば、 無名関数というものがあるので、それを使うほうが、可読性に優れていると思わいます。 ベストプラクティスがないので、何とも言えませんが。

参照

多くの情報は、この書籍を参考に記載しております。

Kotlinスタートブック -新しいAndroidプログラミング

Kotlinスタートブック -新しいAndroidプログラミング