ツアー副読本:関数
関数(ツアー) - Kotlin 日本語リファレンスを読んでいく時の補助教材です。
最初に読む時
初めてツアーをやる時は、ラムダ式の手前まで読んだら次のクラスに進む方がいいと思います。 クラスが終わってからラムダ式に戻ってきましょう。
単一式関数までは特に補足無しで読むだけで分かるでしょう。
以下は、クラスが終わってから戻ってきてください。
ラムダ式
ラムダ式の所は他の言語でラムダ式や関数オブジェクトを使った事が無い人には難解な説明かもしれません。
ラムダ式とは何か、という事について以下に動画を作りました。
filterがなんか中括弧なんだけど?
「他の関数に渡す」の所で、例が以下のようなコードになっています。
val numbers = listOf(1, -2, 3, -4, 5, -6)
val positives = numbers.filter { x -> x > 0 }
val negatives = numbers.filter { x -> x < 0 }
println(positives)
// [1, 3, 5]
println(negatives)
// [-2, -4, -6]
filterの所、なんかfilter()
といいつつ呼ぶ時はfilter {}
になっててどういう事?
という感じがしますが、
これは後に出てくる「トレーリングラムダ」という省略記法です。
現時点では、このコードは以下と同じなので、以下が理解出来れば良いです。
これはnumbersのfilterというメソッドにラムダ式を渡しています。
ラムダ式は { x -> x > 0 }
と { x -> x < 0 }
の部分です。
この後に囲み記事で書いてある「もしラムダ式が唯一の関数のパラメータの場合〜」というのは、 この事を言っています。
次のmapの例も同じです。
「関数の型」のあたりがなんだかわからん
「関数の型」のあたりは解説がやや難しいかもしれない。
変数には型をつける事も出来る、という話がありました。 つまり、以下の2つのコードは同じ意味です。
val s = "ほげ"
val s : String = "ほげ"
型の指定は多くの場合で必要無いけれど、以下の2つの場所では必要な事があった。
- varで変数を作って後で入れる場合
- 関数の引数
前者はあまり出番が無いので主に後者が問題となる。 例えば以下のような関数を考える時、
fun printS(s: String) {
println(s)
}
このsには型の指定、つまり: String
の部分が必要だった。
これは、通常は
val s = "ほげ"
のようにイコールの右側から型が推測出来るのでつける必要が無いけれど、 関数は作る時点ではどの値が入るか分からないので推測に使えるデータが無いからだ。
理屈はおいといて、関数の引数では型を指定する必要がある。
引数に関数を渡す場合の指定
さて、ここで少しややこしい、「関数に関数を渡す」という事を考える。どういう事か?
先程のラムダ式の動画を見れば分かるように、 関数というのは関数オブジェクトというのが作られて、 それを「呼ぶ」事で実行するという話だった。
だから「呼ぶ前」の関数オブジェクトを、他の関数に引数で渡す事が出来る。
例えば以下の「??ここはどうする??」の部分を除いたコードを考える。
fun callF(s: String, myFunc: ??ここをどうする??) {
myFunc(s)
}
callFという関数に、myFuncという関数を渡すようなコードだ。
渡された関数を myFunc(s)
という風に呼び出している。
このように、関数を他の関数の引数にわたす、という事が出来る。
使う側は例えば以下のようになる。
callF("いかいか", {str:String -> println("ほげほげ: ${str}")})
さて、先程の関数の定義に戻ろう。
fun callF(s: String, myFunc: ??ここをどうする??) {
... 省略 ...
}
「??ここをどうする??」の所には何を書いたらいいだろうか?
ここには{str:String -> println("ほげほげ: ${str}")}
の「型」が入る。
ラムダ式の型だが、ラムダ式は関数の一種だったのでこれは関数の型になる。
より具体的には、以下のように書いた時、
val f = {str:String -> println("ほげほげ: ${str}")}
このfの型はなんだろうか?というのがこの「関数の型」という所で解説している所だ。
ちなみに答えは(String)->Unit
になる。
試してみよう。
なんだかややこしいが、(String)->Unit
が、これまでのString
とかInt
とかと同じように「型」である、
という事に注意すると解読出来ると思う。
なおこのcallFはかなりややこしいので、頑張って解読して理解して欲しい。 なんでこんな事をするのかはもうちょっと後で解説するので、 この時点ではcallFのコードを解読出来ればOK。
ちなみに、以下のコードは
val f = {str:String -> println("ほげほげ: ${str}")}
以下と同じ意味となる。
val f :(String)->Unit = {str:String -> println("ほげほげ: ${str}")}
「単独で呼び出す」が意味わからんのだが?
「単独で呼び出す」に、以下のようなコードがある。
これがすんなり分かるかは人によるだろう。
という事ですんなりわからん人向けに簡単に解説を書く。 以下のコードは、
println({ string: String -> string.uppercase() }("hello"))
printlnの中で一気に2つの事をやっている
- ラムダ式で関数を作る
- その関数を呼び出す
これはややこしいので、この2つを間に変数をつかって分けて書くと以下のようになる。
val f = { string: String -> string.uppercase() }
println(f("hello"))
ラムダ式を作って、それを"hello"
を引数に呼び出している訳だ。
これでも良く分からないなら、ラムダ式じゃない普通のfunで書き直してみても良い。 書き直してみると以下のようになる。
つまり、
println({ string: String -> string.uppercase() }("hello"))
も
val f = { string: String -> string.uppercase() }
println(f("hello"))
も、
fun f(string: String) : String {
return string.uppercase()
}
println(f("hello"))
も、同じ意味となる。 ラムダを直接呼ぶのは慣れるまでは読みにくいので、 良く分からない時は一旦変数fに入れるコードに書き直してみると良い。
トレーリングラムダのあたりが良くわからんのだが?
唐突にfoldとか出てきて、何言ってるのおまえ?という気分になる人もいるかもしれないので、 もうちょっと簡単な例でトレーリングラムダの解説を書いておく。
以下のコードがあった。
この呼び出す側、つまり以下の行が、
callF("いかいか", {str:String -> println("ほげほげ: ${str}")})
最後の引数がラムダ式になっている。 この場合、kotlinにはトレーリングラムダというルールがあって、 以下のように書いても良い、という風になっている。
callF("いかいか") {str:String -> println("ほげほげ: ${str}")}
最後の引数を、カッコの外に出しても良い。 なんでこうなっているかは後回しにして、とりあえずトレーリングラムダとはこれの事だ、 という事をここでは理解して欲しい。
実際に試してみよう。
さて、これがトレーリングラムダだが、 もう一つルールがある。
それは、「ラムダを外に出した時に、他に引数が残っていなければ()
を省略出来る」というもの。
前の方でfilterの例があった。
val numbers = listOf(1, -2, 3, -4, 5, -6)
val positives = numbers.filter( { x -> x > 0 } )
この二行目、以下の行は、
val positives = numbers.filter( { x -> x > 0 } )
トレーリングラムダを使うと以下のように書ける。
val positives = numbers.filter() { x -> x > 0 }
すると、filterの引数が何も残らないので、このfilter()
の()
は省略出来て、以下のように書ける。
val positives = numbers.filter { x -> x > 0 }
これがトレーリングラムダの2つ目のルール、「ラムダを外に出した時に、他に引数が残っていなければ()
を省略出来る」だ。
逆に言えば、以下のコードは
val positives = numbers.filter { x -> x > 0 }
いつも、以下のコードと同じ意味になる。
val positives = numbers.filter( { x -> x > 0 } )
慣れるまではこれに戻しても良い。
いろいろ書いてみよう
ここまでの説明だと、だいたいが「これ何に使うの?」という事ばかりだったと思う。 という事で具体的に使う例をいろいろ書いてみよう。
なお、この例はどれもかなり難しいので、出来なかったら出来なかったでもOK。
リストの要素に対して順番に渡された関数を実行する、myForeachを書く
リストの要素に対して、順番に処理をする、myForeachを考えよう。 型はいろいろ決めてしまう。
例えば以下のようなリストがあった時に、
val list = listOf("ほげ", "いか", "ふが")
これを順番にprintlnするには以下のようにすれば良い。
for(a in list) {
println(a)
}
では、前に「むえぇ〜:」をつけてprintlnしたい場合はどうだろう?
それは以下のように書けば良いだろう。
for(a in list) {
println("むえぇ〜:" + a)
}
この両者は、prntlnの所以外は同じだ。 同じ所は関数にして再利用したい、というのは良くある。
だから、以下の部分を再利用したい
for(a in list) {
// 「この中は使う人が決める」
}
こういう時に、「この中は使う人が決める」の部分を関数として外から渡すようにするのが、 良くあるやり方である。
言葉でごちゃごちゃ言われてもわからん、という人も居ると思うので、 先に答えを見てみよう。
以下のようになる。
fun myForeach(list: List<String>, f: (String)->Unit) {
for(a in list) {
f(a)
}
}
このうち、以下の部分を見ると、
for(a in list) {
f(a)
}
「この中は使う人が決める」の所が、f(a)
になっている。
以上を実際に使ってみよう。
なお、ラムダ式を外に出すトレーリングラムダを使っている。 以下と同じ意味です。
myForeach(list, { s-> println(s) })
myForeach(list, { s-> println("むえぇ〜:" + s) })
課題1: myForeachを使って、文字列を全部つなげるコードを完成させよ
「ほげいかふが」と出力するように、連結するコードを書け。
解答例
fun myForeach(list: List<String>, f: (String)->Unit) {
for(a in list) {
f(a)
}
}
fun main() {
val list = listOf("ほげ", "いか", "ふが")
var s = ""
// TODO: 以下でmyForeachとラムダ式で、sにlistの中身を連結せよ
myForeach(list) { s1-> s+=s1 }
// 以下はいじらない
println(s == "ほげいかふが")
}
課題2: myForeachを自分で書け
こういうのは、自分で書いてみないと分からないものなので、 先程のmyForeachを自分で書いてみよう。
先程の例に答えがあるので見ながら書いてもいいけれど、最終的には見ないで書けるように何度か書いてみよう。
課題3: 全部の要素を二倍して渡すmyTwiceForeachを作ろう
myForeachは文字列のリストを渡しましたが、今度はIntのリストを渡して、それを二倍して関数を呼び出すmyTwiceForeachを作りましょう。
良く意味が分からない人はmainの中を見て使い道を予想してください。
ヒント
リストがIntなので、関数の型は(Int)->Unit
となる。
ヒント2
やりたい事をまずfor文で考えてみると、
for(a in list) {
val a2 = 2*a
// 「ここにa2を使った何かを実行する」
}
のようになる。「ここにa2を使った何かを実行する」の所が関数になる。 回答ではわざわざa2を作らなくてもいいが、作っても良い。
解答例
// TODO: ここにmyTwiceForeachを書く
fun myTwiceForeach(list: List<Int>, f: (Int)->Unit) {
for(a in list) {
f(2*a)
}
}
// 以下はいじらない
fun main() {
val list = listOf(5, 4, 3)
myTwiceForeach(list) { i: Int -> println("要素は2倍すると「${i}」です")}
}
課題4: 条件に合う要素だけを含んだリストを返す、myFilterを作ろう
条件にあった要素だけを含んだリストを返す、myFilterを作ろう。 mainの関数を見て、何を作らなきゃいけないかを理解してください。
なお、この課題はかなり難しいので一旦答えを見て理解したあとにもう一度やり直してもいいかもしれない。 次のmyMapもほとんど同じような問題なので、ある程度理解できたら次のmyMapを解いてみるといいかもしれません。
また、この課題を元に、myFilterを使って「偶数のリスト」とか「奇数のリスト」などを取り出す方法も考えてみてください。
ヒント
引数の関数はIntを引数にとりBooleanを返す関数。
また、このmyFilter自身はリストを返すので戻りの型はList<Int>
。
でもこの問題としてはMutableList<Int>
でもいいです。
ヒント2
やりたい事をまずfor文で考えてみると、
val ret = mutableListOf<Int>()
for(a in list) {
if (/* ここに何かを考える */ ) {
ret.add(a)
}
}
みたいな事をやりたい。このretを返したい。
解答例
// TODO: ここにmyFilterを書く
fun myFilter(list: List<Int>, f: (Int)->Boolean) : MutableList<Int> {
val ret = mutableListOf<Int>()
for(a in list) {
if(f(a)) {
ret.add(a)
}
}
return ret
}
// 以下はいじらない
fun main() {
val numbers = listOf(1, -2, 3, -4, 5, -6)
val positives = myFilter(numbers, { x -> x > 0 })
val negatives = myFilter(numbers) { x -> x < 0 } // トレーリングラムダ
println(positives)
// [1, 3, 5]
println(negatives)
// [-2, -4, -6]
}
課題5: myFilter2を使って、先頭がストIIの要素だけ取り出せ
今度は使う側のコードです。 ストIIXは本来はスパIIXですが、そこは気にしないということで。
解答例
fun myFilter2(list: List<String>, pred: (String)->Boolean) : List<String> {
val ret = mutableListOf<String>()
for(s in list) {
if(pred(s))
ret.add(s)
}
return ret
}
fun main() {
val kakugee = listOf("餓狼伝説スペシャル", "ストII", "ストIIダッシュ", "ヴァンパイアセイバー", "ヴァンパイア", "サムライスピリッツ", "ストIIX", "餓狼伝説2")
// 以下をmyFilter2を使って書き直せ
val suto2 = myFilter2(kakugee) { str-> str.startsWith("ストII") }
println(suto2)
}
課題6: 各要素に渡された変形をほどこしてその結果をリストにしたものを返す、myMapを作ろう
要素を変形する関数を引数にとって、変形した結果をリストにするmyMapを作ろう。 これも良く分からなければmain関数を見て必要なものを考えてください。
ヒント
やりたい事をとりあえずmainの中でfor文で書いてみよう。 それを見ながら関数に抜き出す方法を考えよう。
解答例
// TODO: ここにmyMapを書く
fun myMap(list: List<Int>, f: (Int)->Int) : MutableList<Int> {
val ret = mutableListOf<Int>()
for(a in list) {
val a2 = f(a)
ret.add(a2)
}
return ret
}
// 以下はいじらない
fun main() {
val numbers = listOf(1, -2, 3, -4, 5, -6)
val doubled = myMap(numbers) { x -> x * 2 }
val tripled = myMap(numbers) { x -> x * 3 }
println(doubled)
// [2, -4, 6, -8, 10, -12]
println(tripled)
// [3, -6, 9, -12, 15, -18]
}
setOnClickListenerを考え直す
これまで、以下のようなコードを書いてきた。
findViewById<Button>(R.id.button1).setOnClickListener {
findViewById<TextView>(R.id.label1).text = "むぇ〜〜"
}
これはラムダ式を渡していて、トレーリングラムダを使っています。 トレーリングラムダを使わないコードにすると以下です。
findViewById<Button>(R.id.button1).setOnClickListener({ findViewById<TextView>(R.id.label1).text = "むぇ〜〜" })
変数にするともう少し分かりやすくなるかもしれない。
val f = { findViewById<TextView>(R.id.label1).text = "むぇ〜〜" }
findViewById<Button>(R.id.button1).setOnClickListener(f)
fというラムダ式を作って、それをsetOnClickListenerに渡していたのでした。