プログラマに優しい現実指向JVM言語 Kotlin入門

第3回Kotlinを学ぶ

前回ではKotlinの開発環境構築について解説しました。今回はKotlinのプログラミング言語としての文法や機能をじっくり紹介していきます。

定番のHello World

まず紹介するのはHello Worldプログラムです。第1回でも紹介しましたが、もう一度確認しましょう。

リスト1は実行されると標準出力に「Hello,world!」と書き込んで終了するだけのプログラムです。main関数はKotlinプログラムのエントリポイントです。main関数はクラスに属さずパッケージ直下に置く必要があります。パッケージ名はJavaと同じようにドメイン名をひっくり返してピリオド.区切りのスタイルです。

リスト1 KotlinでHello World
package com.taroid.sample

fun main(args: Array<String>) {
  println("Hello, world!")
}

実際に動かすにはKotlinコンパイラやIntelliJ IDEA(+Kotlinプラグイン)などを使用します。ちょっと試してみたいときにはKotlin WebDemo[1]というWebブラウザ上でKotlinコードを編集、実行できる環境がお勧めです。

変数の使い方

今はただ世界に挨拶するだけのプログラムですが、挨拶する対象を変数として切り出してみましょうリスト2⁠。

リスト2 変数を使う
fun main(args: Array<String>) {
  val name: String = "Taro"
  println("Hello, ${name}!") // => Hello, Taro
}

nameという名前の変数に挨拶する相手の名前を代入しています(好きな名前を代入してください!⁠⁠。変数の宣言にはvalキーワードまたはvarキーワードが必要です。valを使うとその変数は再代入、すなわち変更が不可能な変数になります。varを使うと再代入可能な変数になります。基本的にはvalを使用し、varを使うのは最小限にとどめておきましょう。

nameの宣言でStringというがアノテートされています。nameString型であることを明示しているわけです。しかし右辺の"Taro"という文字列リテラルの存在によって、nameString=文字列)ということは明白であり、型のアノテートは冗長に思えます。実際、Kotlinコンパイラはこのような場合には型の明示的なアノテートなしにnameString型だと推論してくれます。たとえばval name = "Taro"のように変数定義ができます。このようなしくみを型推論と呼びますが、Kotlinでは変数定義以外のさまざまな場所でも型推論が働きます。

さて、挨拶する相手の名前をnameという変数に代入していることがわかりました。次に挨拶文を出力するコードです。"Hello, ${name}!"Stringテンプレートと呼ばれている機能です。式を埋め込むことが可能で、その計算結果が反映された文字列を生成します。今回の場合、nameの中身が"Taro"なので"Hello, Taro!"という文字列が得られます[2]⁠。

コマンドライン引数の使い方

挨拶する相手の名前を変数に定義しましたが、今は名前が固定で挨拶プログラムとしては実用的ではありません。プログラム利用者が名前を指定できると便利そうです。そこでプログラム実行時に渡されるコマンドライン引数を使います。Kotlinの場合、main関数の引数argsにコマンドライン引数が設定されていますリスト3⁠。

リスト3 コマンドライン引数に指定された名前を使う
fun main(args: Array<String>) {
  val name = args[0]
  println("Hello, ${name}!")
}

argsArray<String>からもわかるとおり文字列の配列です。0が配列の最初の要素のインデックスです。args[0]には複数渡され得るコマンドライン引数の最初の引数が代入されています。コマンドライン引数が指定されずに実行された場合、リスト3はargs[0]によりクラッシュします。

if式の使い方

そこで、argsが空の場合は、デフォルトの名前を使って挨拶するように変更します。状況によって処理を分岐する構文は、Javaでもお馴染みのif-elseですリスト4⁠。

リスト4 argsが空でないことを確認する
fun main(args: Array<String>) {
  if (args.isNotEmpty()) {
    println("Hello, ${args[0]}!")
  } else {
    println("Hello, 名無しさん!")
  }
}

isNotEmptyメソッドにより、配列が空でないかどうかを調べています。配列が空でない場合、すなわちisNotEmptytrueを返す場合はifの後に続くブロックを実行します。それ以外の場合はelseの後に続くブロックを実行します。Javaにおけるif-elseと同様に、リスト4のように分岐後のブロック内の文が1つの場合に限り波括弧{ }を省略して記述できます。

Kotlinのif-elseは式です。つまりif-elseは値を返します。それはちょうどJavaの条件演算子[3]と同じ動きをします。リスト4を書き直すとリスト5のようになります。

リスト5 if-elseは式である
fun main(args: Array<String>) {
  val name = if (args.isNotEmpty()) args[0] else "名無しさん"
  println("Hello, ${name}!")
}

forループの使い方

複数指定されたコマンドライン引数に対し、すべてにHelloと言いたい場合にはforループを使いますリスト6⁠。

リスト6 argsの各要素に繰り返しHelloする
fun main(args: Array<String>) {
  for (name in args) {
    println("Hello, ${name}!")
  }
}

このように、Kotlinのforは、Javaにおける「拡張for文」に似ています。ループカウンタを増やして行くスタイルのループを、Kotlinのforはサポートしていません。

Kotlinの関数の定義の方法

まずは簡単な関数を定義する例を示しますリスト7⁠。

リスト7 hello関数
fun hello(name: String) {
  println("Hello, ${name}!")
}

リスト7にhelloという名前の関数を定義しました。関数を定義するにはfunキーワード、関数名、引数リストの順に記述します。引数には型を明示する必要があります。波括弧で関数本体を表します。

関数定義と関数の呼び出し例をリスト8に示します。"Kotlin"を引数に、helloを呼び出しています。

リスト8 関数呼び出し例
fun hello(name: String) {
  println("Hello, ${name}!")
}

fun main(args: Array<String>) {
  hello("Kotlin") // => Hello, Kotlin!
}

値を返す関数

引数を取って、なんらかの計算を施し、その結果を返すような関数を定義してみましょう。シンプルな足し算を行うだけの関数plusリスト9に定義しました。

リスト9 足し算関数を定義
fun plus(a: Int, b: Int): Int {
  return a + b
}

fun main(args: Array<String>) {
  println("2 + 5 = ${plus(2, 5)}")
// => 2 + 5 = 7
}

先ほどのhello関数と違うのは、戻り値の型を指定しているところと、returnキーワードにより値を返しているところです。戻り値の型は、関数の引数リストの直後にコロン:を挟んで記述します。plus関数の戻り値の型は2つのInt型の足し算なのでIntです。returnキーワードは関数の値を返すためのキーワードです。

plus関数は、return文のみにより構成されているので、この場合に限りリスト10のように=を使ったシンプルな記述が可能になります。

リスト10 単一式関数
fun plus(a: Int, b: Int): Int = a + b

関数の基本的な使い方は以上です。リスト11のように面白い関数を作って遊んでみましょう![4]

リスト11 関数で遊ぼう
// 掛け算
fun times(a: Int, b: Int) = a * b

// 平方
fun square(n: Int): Int = times(n, n)

// 大きい方を返す
fun max(a: Int, b: Int): Int = if (a < b) b else a

// 小さい方を返す
fun min(a: Int, b: Int): Int = if (a <= b) a else b

// 最大公約数を返す
fun gcd(a: Int, b: Int): Int {
  var x = max(a, b)
  var y = min(a, b)
  while(y != 0) {
    val w = y
    y = x % y
    x = w
  }
  return x
}

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

関数の引数にはデフォルト値を設定しておくことができますリスト12⁠。

リスト12 デフォルト引数
fun hello(name: String, exclamation: Boolean = false) {
  val suffix = if (exclamation) "!" else ""
  println("Hello, ${name}${suffix}")
}

リスト12のhello関数は、Boolean型のexclamationという引数を持っていますが、デフォルト値を設定しています。デフォルト値を持った引数(デフォルト引数)は、呼び出しの際に値の指定を省略できます。省略した場合にはデフォルト値が使われるというわけですリスト13⁠。

リスト13 デフォルト引数の関数の使用例
// 第2引数を省略
hello("Kotlin")       // => Hello, Kotlin

// 第2引数を指定
hello("Kotlin", true) // => Hello, Kotlin!

また、関数呼び出しの際に引数へ渡す値を名前指定で渡せますリスト14⁠。

リスト14 名前付き引数
hello(name = "Foo")

// 引数リストの順番に従う必要はない
hello(exclamation = false, name = "Baz")

再帰呼び出し

関数が、自分自身を呼び出すことを再帰呼び出しと言います。再帰呼び出しにより、ループを宣言的に記述できるようになります。たとえば、引数のリストの合計値を返す関数を考えましょう。まずは通常バージョンですリスト15⁠。

リスト15 forによるループ
fun sum(ints: List<Int>): Int {
  var sum = 0
  for (e in ints) {
    sum += e
  }
  return sum
}

forによりループを回しています。変数sumvarにより宣言されており、繰り返し新しい値が代入されています。次に再帰呼び出しのバージョンですリスト16⁠。

リスト16 再帰呼び出しによるループ
fun sum(ints: List<Int>): Int =
    if da(ints.isEmpty()) 0
    else ints.first() + sum(ints.drop(1))

forも再代入もなくなりました。代わりにsum関数の定義の中で自分自身を呼び出しています。

ちなみに、isEmptyfirst()drop(1)は整数のListであるintsのメソッドです。それぞれ、リストが空かどうか、リストの先頭要素、先頭から1つ分要素を除いた新しいリストを返します。

リスト11で定義した最大公約数を求めるgcd関数を再帰呼び出しを使って実装してみましょうリスト17⁠。

リスト17 gcdを再帰関数にする
fun gcd(a: Int, b: Int): Int {
  val x = max(a, b)
  val y = min(a, b)
  return if (y == 0) x
         else gcd(y, x % y)
}

varwhileループも、そして一時変数wも消せました! このように再帰呼び出しを使うとコードがすっきりして読みやすくなることが多いです。

再帰呼び出しの欠点は、関数を何回も呼び出し続けることによるスタックの消費です。何回も(環境によりますが非常に多くの回数)関数を呼び続けるとスタックオーバーフローを起こし、プログラムがクラッシュします。これを回避するためにKotlinには末尾呼び出し最適化(tail call optimization)と呼ばれるしくみが備わっています。

末尾呼び出しとは、再帰呼び出しが末尾にあるような呼び出しです。たとえばリスト17のgcd関数は末尾呼び出しを行っています。このような再帰関数にtailRecursive[5]というアノテーションを付けると末尾呼び出し最適化が施され、スタックを食いつぶさないようなコードに展開されますリスト18⁠。

リスト18 末尾呼び出し最適化が効くgcd関数
tailRecursive fun gcd(a: Int, b: Int): Int {
  val x = max(a, b)
  val y = min(a, b)
  return if (y == 0) x
         else gcd(y, x % y)
}

最適化が有効になるのはあくまで末尾呼び出しのみなので、関数によっては再帰の仕方を工夫する必要があります。

関数オブジェクトと関数型

Kotlinでは関数をほかの値と同じように変数に代入したり、引数として関数に渡したり、戻り値として受け取ったりできます。このようにほかの値と同様の形になった関数を、便宜的に関数オブジェクトと呼ぶことにします。実際に関数を変数に代入する例をリスト19に示します。

リスト19 関数オブジェクト
fun succ(n: Int) = n + 1
val hoge = ::succ

定義されたsucc関数を変数hogeに代入しました。ポイントは、関数名の直前に::と記述することです。::を置くことで関数オブジェクトを得ることができるのです。

関数オブジェクトの関数としての機能を呼び出すには、関数オブジェクトのinvokeメソッドを使いますリスト20⁠。

リスト20 invokeメソッド
val r = hoge.invoke(5)
println(r) // => 6

この機能はよく使うので構文糖衣が提供されています。リスト21のように普通の関数呼び出しに似ています。

リスト21 invoke呼び出しの構文糖衣
val r = hoge(5)
println(r) // => 6

ところで、変数hogeの型を明示していませんでしたが、関数オブジェクトの型はどのようになるのでしょうか。hogeの宣言を型推論に頼らないで記述するとリスト22のようになります。

リスト22 関数オブジェクト
val hoge: (Int) -> Int = ::succ

(Int) -> Intの部分が関数の型です。->を挟んで左が引数の型リスト、右が戻り値の型を表現しています。2つの引数を取る関数の型は、たとえば(Char, Int) -> Stringのようになります。

高階関数

関数オブジェクトと関数型についてわかったので、関数オブジェクトを引数に取る関数について解説します。関数を引数に取ったり、関数を返すような関数のことを高階関数(こうかいかんすう:higher-order function)と呼びます。簡単な例をリスト23に示します。

リスト23 高階関数の例
fun apply(n: Int, f: (Int) -> Int): Int {
  println("開始")
  val r = f(n)
  println("終了")
  return r
}

apply関数は引数を2つ取ります。Int型のnと、(Int) -> Int(すなわち関数型)fです。apply関数の動きとしては、nを引数にfを適用した結果を返すだけです。では、このapply関数を使ってみましょうリスト24⁠。

リスト24 apply関数を使う
val got = apply(5, ::succ)
println(got) // => 6

iとして5を、fとしてリスト19で定義したsucc関数の関数オブジェクトを渡しています。このコードを実行すると「開始」⁠終了」⁠6」と各行に表示されます。

もう少し複雑で役に立ちそうな例を見てみましょう。リスト25で定義したmap関数は、リストの各要素を変換して新しいリストを得る関数です。

リスト25 リストの各要素を変換して新しいリストを得るmap関数
fun map(ints: List<Int>, f: (Int) -> Int): List<Int> {
  val newList = java.util.ArrayList<Int>()
  for (e in ints) {
    newList.add(f(e))
  }
  return newList
}

関数シグネチャを注意深く見てみましょう。map関数は2つの引数を取ります。IntListであるintsが第1引数で、元となるリストです。(Int) -> Int型のfは、リストの要素に適用される変換ロジックです。そして、戻り値の型はList<Int>で、これが変換後のリストとなるわけです。

次に関数本体です。新しいリストが欲しいので、リスト(ここではjava.util.ArrayListのインスタンスを生成します。生成したリストにnewListという名前を付けておきます。ここでforループが登場し、intsの各要素に対してループします。各要素はfの引数となり、その適用の結果得られた値がnewListに追加されていきます。こうしてnewListは元のリストの各要素が変換された値で構成されるリストとなり、mapの戻り値となります。

実際にmapを使ってみましょうリスト26⁠。

リスト26 map関数を使う
// [2, 3, 4]のリストを作る
val src = listOf(2, 3, 4)

// 平方を得る関数を各要素に適用
fun square(n: Int): Int = n * n
println(map(src, ::square)) // => [4, 9, 16]

// 階乗を得る関数を各要素に適用
fun factorial(n: Int): Int =
    if (n == 1) 1
    else n * factorial(n - 1)
println(map(src, ::factorial)) // => [2, 6, 24]

このように、高階関数の利点は抽象的な部品であるということです。例に示したmap関数はリストの各要素を別の値に変換する処理ですが、変換の詳細には触れていません。変換の詳細は、引数fに任されているのです。部品が抽象的であるということは、コードの再利用をより容易にすることを意味します。

クロージャ

定義済みの関数を::により、関数オブジェクトに変換する一連の流れを介さずに関数オブジェクトを直接生成することもできます。リスト27の①と②と③では同じ結果を得られます。

リスト27 関数オブジェクトのリテラル表現
// ①
fun succ(n: Int): Int = n + 1
map(list, ::succ)

// ②
map(list, fun(n: Int): Int { return n + 1 })

// ③
map(list, {n: Int -> n + 1})

②や③のように関数オブジェクトを直接生成するような記法を関数リテラルと呼びます。ほかの言語ではラムダ式や無名関数と呼ばれるものです。③のような関数リテラルの書式を一般化すると次のようになります。

{引数リスト -> 関数本体}

関数リテラルには波括弧が必須であることに注意してください。また、文脈によっては省略した記述が可能になりますリスト28⁠。

リスト28 関数リテラル記法
// 関数リテラル内で型推論が働く
val foo: (Int) -> Int = {
  n -> n + 1
}

// 引数が1つの場合は暗黙の変数itが使える
val bar: (Int) -> Int = {
  it + 1
}

// 複数の文を持つ関数リテラル
val baz: (Int) -> Int = {
  var sum = 0
  for (e in 1..it) {
    sum += e
  }
  sum
}

// 高階関数に渡す特殊な記法
map(listOf(1, 2, 3)) {
  it + 1
}

ところで、Kotlinの関数リテラルはクロージャ(closure)です。つまり、引数に与えられた変数以外の変数を、コードを記述したときのスコープで解決できます。日本語で説明してもわかりづらいと思うので、コード例を示します。

今、リスト29に定義したような関数counterがあります。

リスト29 クロージャを返す関数counter
fun counter(): ()->Int {
  var count = 0
  return {
    count++
  }
}

この関数はIntを返す関数」を返します。Intを返す関数」を関数Aと呼ぶことにします。関数Aは{ count++ }のことです。変数countは関数Aの外で宣言されていますが、関数Aが定義されている場所でこれを参照、更新することができます。関数Aを呼び出すとcountの値を返して、countをインクリメントします。

関数counterの使用例がリスト30です。

リスト30 呼び出すたびにカウントアップする
val counter1 = counter()
println(counter1()) // => 0
println(counter1()) // => 1
println(counter1()) // => 2

val counter2 = counter()
println(counter2()) // => 0
println(counter2()) // => 1

counter()により関数Aを取得しています。関数A(ここではcounter1などと名前を付けています)が呼び出されるたびに、返される値が増えているのがわかります。

まとめ

Hello Worldプログラムを通じて、valvarによる変数宣言とStringテンプレート、コマンドライン引数を学びました。Kotlinのif-elseはJavaのそれとは異なり「式」であり、値を返します。forループは、Javaの拡張for文に似ており、ループカウンタを必要としません。

後半はKotlinの関数について学びました。関数の引数にはデフォルト値を設定しておくことができます。また関数呼び出し時に、引数を名前付きで渡せます。関数は第一級オブジェクトであり、関数型の変数に代入できます。その性質を利用した高階関数と、クロージャを学びました。Kotlinの関数はアノテーションを付けることで末尾呼び出し最適化やインライン関数化が有効になります。

次回はクラスについて解説します。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧