前回 では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
という型 がアノテートされています。name
はString
型であることを明示しているわけです。しかし右辺の"Taro"
という文字列リテラルの存在によって、name
がString
(=
文字列)ということは明白であり、型のアノテートは冗長に思えます。実際、Kotlinコンパイラはこのような場合には型の明示的なアノテートなしにname
をString
型だと推論してくれます。たとえば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}!")
}
args
はArray<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
メソッドにより、配列が空でないかどうかを調べています。配列が空でない場合、すなわちisNotEmpty
がtrue
を返す場合は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
によりループを回しています。変数sum
はvar
により宣言されており、繰り返し新しい値が代入されています。次に再帰呼び出しのバージョンです(リスト16 ) 。
リスト16 再帰呼び出しによるループ
fun sum(ints: List<Int>): Int =
if da(ints.isEmpty()) 0
else ints.first() + sum(ints.drop(1))
for
も再代入もなくなりました。代わりにsum
関数の定義の中で自分自身を呼び出しています。
ちなみに、isEmpty
、first()
、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)
}
var
もwhile
ループも、そして一時変数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つの引数を取ります。Int
のList
である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プログラムを通じて、val
とvar
による変数宣言とStringテンプレート、コマンドライン引数を学びました。Kotlinのif-elseはJavaのそれとは異なり「式」であり、値を返します。for
ループは、Javaの拡張for文に似ており、ループカウンタを必要としません。
後半はKotlinの関数について学びました。関数の引数にはデフォルト値を設定しておくことができます。また関数呼び出し時に、引数を名前付きで渡せます。関数は第一級オブジェクトであり、関数型の変数に代入できます。その性質を利用した高階関数と、クロージャを学びました。Kotlinの関数はアノテーションを付けることで末尾呼び出し最適化やインライン関数化が有効になります。
次回はクラスについて解説します。
「インライン関数」
高階関数は強力なしくみですが、一般に呼び出しのコストが高い傾向にあります。関数オブジェクトの生成や呼び出しを伴うことがほとんどだからです。この問題を解消するためインライン関数というしくみが導入されています。インライン関数は、引数の関数リテラルがコンパイル時にインライン展開される関数のことです。通常の関数にinline
アノテーションを付加するだけでインライン関数になります。
リスト25をインライン関数にするには次のとおりです。
// inlineアノテーションを付けるだけ
inline 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
}
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)