スクリーンショット_2019-07-27_23

Kotlin/Native + Coroutine

7月27日土曜日、曇り

明け方にぱらりぱらりと降っていた雨──それをもたらしていただろう台風6号はあえなく熱帯低気圧に変わったらしく、起き出した昼過ぎには暴風の気配さえもなかった。(けれども湿度は高く、それゆえ不快感も高い)

IntelliJ IDEA で Kotlin/Native が簡単に書けることを、まとめておこうと思ったので、以下、そのお話。(macOS のを使っているけれど、ほかの OS でもほとんど同じに使えるはず)

* * *

プロジェクトの作成

IDEA を起動して表示されるスプラッシュスクリーンで「Create New Project」を選ぶ。

つづいて左の形式から「Kotlin」を選び、右の詳細で「Native | Gradle」を選ぶ。(Kotlin/Native を Gradle でビルドする、ということだ)

Gradle を動かす設定では「ビルドスクリプトの変更にあわせてこのプロジェクトを自動的に取り込む」という設定をオンにしてみたけれど、これがどういう効果を持つかはわからない。

プロジェクトの名前をつける。いわゆる「Hello World」がサンプルで生成されるので、それにあわせて「native-hello」とした。

これでウィザードがプロジェクトをテンプレートから生成してくれる。

プロジェクトの確認

さて。規定で生成されるプロジェクトのソースはこれ。

至極単純なサンプルソース。

package sample

fun hello(): String = "Hello, Kotlin/Native!"

fun main() {
  println(hello())
}

sample パッケージに hello と main という名前の関数を定義。
C 言語と同じで、 Kotlin/Native では main 関数がエントリーポイントになる。(ただし戻り値はない)

戻り値がないということは、プログラムは「正常に終了」することが期待されていて、異常が発生したら「例外を送出して終了」するというポリシーなのだろう。(その場合の終了コードはゼロ以外になるのだろう。 255 かな?)

Kotlin では「型」を後置する。
"fun hello(): String" からは以下の情報が読み取れる:
・関数("fun")、
・その名前は hello、
・呼び出す際の引数は不要("()";空っぽの引数リスト)、そして
・文字列を返す(": String";この関数の型は String)

hello 関数は、その本体が「= "Hello, Kotlin/Native!"」と定義されている。
旧来の言語なら次のように書くところだ。

fun hello(): String {
  return "Hello, Kotlin/Native!"
}

もちろんこのように書いても正しくてコンパイル可能。等号による定義は、このような「式」であることを簡潔に明示できて、かつ「代入による状態の変化」がないことがわかりやすいというメリットがある。

main 関数には型の定義がない。関数の型を省略した場合、なにも戻さない「Unit」型を指定したことになる。関数本体のブロックには、だから return 文がない。
println は、与えられた引数をコンソールに出力(print)して改行する(ln;line  の略。たぶん)命令。

つまりこのプログラムを実行すると、 hello 関数を呼び出して "Hello, Kotlin/Native!" という文字列を得て、これをコンソールに出力して改行する、という具合に動く。

右の Gradle パネルを開き、定義ずみタスクから実行(run)の中にある「リリース実行形式を実行(runReleaseExecutableMacos)」をダブルクリックすると下画面が「実行結果」表示に変わり、しばらくビルドタスクが走ってから実行結果が表示される。

> Task :runReleaseExecutableMacos
Hello, Kotlin/Native!

Coroutine を使う

さて。 Kotlin の目玉機能のひとつ、 Coroutine を使ってみよう。

Gradle 設定ファイルに Coroutine サポートを利用する旨を追記して Gradle 同期を実行すると、プロジェクトで Kotlin Coroutine が使えるようになる。
変更するファイルは ~/build.gradle と ~/settings.gradle のふたつ。

build.gradle では、 sourceSets に commonMain を加え、その依存関係に 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.2.2' を追加する。

sourceSets は、プロジェクトでビルドするターゲットを管理するもので、 macOS でプロジェクトを生成すると、規定で macosMain と macosTest のふたつが定義されている。つまり macOS 用の実行バイナリー(main)とテスト用バイナリー(test)のふたつ。

ひとつのプロジェクトで Window や Linxu、 Android や iOS 向けのバイナリーも管理できるようで、その場合 sourceSets にそれぞれの定義を追加していくことになる。

そしてこれらが共通で参照する定義も加えられ、その名前が commonMain。ここに Maven リポジトリーから jetbrain(Kotlin の開発元であり開発環境 IDEA の開発元でもある)が作成している「kotlinx-coroutines-core-native」を加えた、というのが先の変更。

* * *

もうひとつ。 settings.gradle には、以下一行を加える。

enableFeaturePreview('GRADLE_METADATA')

build.gradle への変更で触れたとおり、 Kotlin の Gradle プロジェクトではひとつのソースファイルから複数のターゲット OS 向けのバイナリーを生成できる。

Gradle プラグイン「org.jetbrains.kotlin.multiplatform」が複数ターゲットのバイナリー生成を支えている。そしてダウンロードしたライブラリーに含まれるメタデータを見て、ターゲット OS 向けの場合分けができるよう設定を変更するのが先の追加行の役割。

Hello, Coroutine

設定も終わったので、 Coroutine を使ってみよう。

先に貼り付けた Gist の diff に含まれているけれど、 coroutine 関連の定義を import して、 main を次のように書き換える。

fun main() = runBlocking {
   hello()
       .map {
           async {
               delay(1000)
               it
           }
       }
       .awaitAll()
       .joinToString("")
       .let {
           println(it)
       }
}

新しい main は runBlocking で定義される式全体に等しい。

脱線するけれど coroutine は複数の処理を「非同期」──いいかえると同時に実行する仕組み。(スレッドという仕組みもあるしプロセスという処理単位もあって、順にコンピューターに与える「負荷」が高くなる)

runBlocking は、この中では coroutine による並行処理が走るけれど、このブロックを抜けるときにはすべて完了して呼び出し元(ここでは main なので呼び出し元は OS)に制御を戻します、という意味合い。(正確には呼び出し元をブロックして(=動作を止めて)実行(run)する)

その中では……

hello() を呼び出して "Hello, Kotlin/Native!" を得て、
その文字列を構成する文字一つひとつにたいしてある操作を適用(map)、
そのそれぞれの実行すべての完了を待ち合わせて(awaitAll)、
文字列にまとめあげて(joinToString;それぞれを空文字列 "" で連結)、
コンソールに出力(println(it))。

* * *

map している「ある操作」とは、これ:

{
  async {
    delay(1000)
    it
  }
}

Kotlin の糖衣構文で、「その場で定義する関数(無名関数;ラムダ)の引数がただ一つの場合、その記述を省略できる(省略した名前は it になる)」というものがあり、これを利用している。

省略しなければ以下のようになる。

{ it: Char ->
  async {
    delay(1000)
    it
  }
}

async は非同期に値を生成する Deferred オブジェクトを生成する関数で、その引数に suspend 関数(引数なし、任意の型を返す)を取る。(ここでは { delay(1000); it } が、その suspend 関数)

delay も suspend 関数で、指定した時間(ミリ秒単位)だけ呼び出し元の suspend 関数を停止する。

map した関数を全体でながめると、こういうこと。
任意の文字を受け取って Deferred オブジェクトに変える。その Deferred オブジェクトは「1秒待って、受け取った文字を返す」。

* * *

元のプログラム全体に戻って眺めなおすと、こうなる。

"Hello, Kotlin/Native!" という文字列を構成する文字それぞれを「1秒後にその文字を返す」非同期実行オブジェクトに変える。すべての完了を待ち合わせ、ふたたび元の文字を得て "" を挟んで連結。つまり元の文字列 "Hello, Kotlin/Native!" に戻して、これをコンソールに出力。

実行結果に注目。

1秒で実行完了している。
※先にビルドしている。

"Hello, Kotlin/Native!" という文字列は21文字ある。だから1秒ずつ待ち合わせると最低でも21秒かかる。
けれどそれぞれを Deferred オブジェクトに変えて「同時に」走らせているため、全体で1秒で処理が完了している。

* * *

蛇足だけれど、 async/await をなくした以下のコードなら、しっかり21秒かかります。

    hello()
       .map {
           delay(1000)
           it
       }
       .joinToString("")
       .let {
           println(it)
       }

また、以下にソース一式コミットしてあります。


この記事が気に入ったらサポートをしてみませんか?