ScalaでJavaっぽいコードを書いて、それをScalaっぽくします。
あと部分関数を紹介して合成します。
Javaっぽいコードのときは可変な変数(変数)が存在していますが、Scalaっぽくなると不変な変数(定数)のみになります。
(可変な変数とか不変な変数とか、変な表現ですね・・・)
初めにScalaの変数について2つほど。
- valで定義される値は定数である(Javaのfinal変数、Valueのこと)
- varで定義される値は変数である(Javaの普通の変数、Variableのこと)。
これだけ分かれば大丈夫!
さて本題。
題材として以下の問題を考えます。
0から9までの10個の要素を持つリストから偶数のみを抽出して、その値を2倍して返す問題を考えます。要するに
0, 1, 2, 3, 4, 5, 6, 7, 8, 9という入力から
0, 4, 8, 12, 16という結果を得るというものです。
Javaっぽく書くとこんな感じになると思います(以下のコードはREPLで実行しています)。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> val list = List.range(0, 10) | |
list: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) | |
scala> var rslt = List[Int]() | |
rslt: List[Int] = List() | |
scala> :paste | |
// Entering paste mode (ctrl-D to finish) | |
for(i <- 0 until list.size){ | |
val e = list(i) | |
if(e % 2 == 0){ | |
rslt = rslt :+ e * 2 | |
} | |
} | |
// Exiting paste mode, now interpreting. | |
scala> println(rslt) | |
List(0, 4, 8, 12, 16) | |
4行目で結果を格納するrsltという変数を作っています。これは変数です。
10行目〜15行目で問題を解いています。
11行目で偶数だけを抽出して、12行目で2倍した値を結果の変数に格納しています。
:+という演算子はリストと要素を結合する演算子です。
リストにインデックスでアクセスしてるし、変数あるし気持ち悪いですね。
イテレータっぽくアクセスしてみましょう。Javaでもこのぐらいは出来ます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> val list = List.range(0, 10) | |
list: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) | |
scala> var rslt = List[Int]() | |
rslt: List[Int] = List() | |
scala> :paste | |
// Entering paste mode (ctrl-D to finish) | |
for(e <- list){ | |
if(e % 2 == 0){ | |
rslt = rslt :+ e * 2 | |
} | |
} | |
// Exiting paste mode, now interpreting. | |
scala> println(rslt) | |
List(0, 4, 8, 12, 16) | |
でもまだif文とか変数があって気持ち悪いですね。
何よりもlistの要素を一つ一つ見て見て偶数だったら2倍して結果に格納っていう処理が絡み合ってて気持ち悪いです。
10行目から12行目にかけての処理を説明するだけでも一苦労です。
ちょっとScalaっぽく書いてみましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> val list = List.range(0, 10) | |
list: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) | |
scala> val rslt = list.filter( e => e % 2 == 0 ).map( e => e * 2) | |
rslt: List[Int] = List(0, 4, 8, 12, 16) | |
scala> println(rslt) | |
List(0, 4, 8, 12, 16) | |
4行目で一連の計算しています。
変数がなくなって定数だけになりましたね!
でも、このままだと分かり難いし、再利用性も皆無なので各処理を関数に分けて書いてみます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> val list = List.range(0, 10) | |
list: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) | |
scala> val isEven = (x: Int) => x % 2 == 0 | |
isEven: Int => Boolean = <function1> | |
scala> val twice = (x: Int) => x * 2 | |
twice: Int => Int = <function1> | |
scala> val rslt = list.filter(isEven).map(twice) | |
rslt: List[Int] = List(0, 4, 8, 12, 16) | |
scala> println(rslt) | |
List(0, 4, 8, 12, 16) |
8行目のtwiceという関数は引数に取った値(xのこと)を2倍して返します。
そして、12行目でそれらの関数を使って一連の計算をしています。
ここで大事なのは、偶数だったら2倍して結果に格納という感じに色々な処理がごちゃまぜになっていないところです。
まずはfilter関数で偶数を抽出、次に抽出した偶数の値をmap関数で2倍しています。
filterは条件を満たす要素のみを抽出する関数です。ある集合の部分集合を作り出すイメージです。
mapは集合の要素一つ一つに関数を適用して別の集合を作り出すイメージです。
このように処理が一つ一つのステップに分解されているので、非常にわかりやすく、説明も楽です。
最初に書いたJavaっぽいコードと比較すると見通しの良さは抜群だと思います。
関数の書き方など、Javaと少し違うので最初は戸惑いますが色々やってるうちに読めるようになってくると思います。練習あるのみ!
更に一歩進んで部分関数というものを紹介したいと思います。
Scalaの部分関数(PartialFunction)とはある条件を満たした場合のみ処理が出来る関数のことです。なんだか意味不明な関数ですね…
”ある条件”とは今回で言えば”偶数”というのがそれに当たります。
ある条件を満たさない値を与えて実行するとエラーになります。
Listはcollectという関数を持っていて、こいつに部分関数を与えると、条件を満たした要素だけに処理を適用する事ができます。
先のfilterとmapを一発で行う為の関数です。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scala> val list = List.range(0, 10) | |
list: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) | |
scala> val twiceIfEven:PartialFunction[Int, Int] = { case e if e % 2 == 0 => twice(e) } | |
twiceIfEven: PartialFunction[Int,Int] = <function1> | |
scala> val rslt = list.collect(twiceIfEven) | |
rslt: List[Int] = List(0, 4, 8, 12, 16) | |
scala> println(rslt) | |
List(0, 4, 8, 12, 16) | |
4行目で部分関数を定義しています。パターンマッチでifを使って偶数のみとしています。
条件と処理が不可分な場合は一括して定義できて良い感じがします。
更に部分関数を掘り下げてみます。
いくつかの部分関数を定義して、それらを合成して大きな部分関数を作る場合を考えます。
題材として以下の問題を考えます(先ほどの問題の拡張版です)。
0から9までの10個の要素を持つリストから偶数は値を2倍して、それ以外(奇数)の場合は3倍して返す問題を考えます。要するに
0, 1, 2, 3, 4, 5, 6, 7, 8, 9という入力から
0, 3, 4, 9, 8, 15, 12, 21, 16, 27という結果を得るというものです。
これをJavaっぽく書くと以下の感じになります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val list = List.range(0, 10) | |
var rslt = List[Int]() | |
for( e <- list){ | |
if( e % 2 == 0){ | |
rslt = rslt :+ e * 2 | |
} else { | |
rslt = rslt :+ e * 3 | |
} | |
} | |
println(rslt) | |
部分関数を合成して書くとこんな感じです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val list = List.range(0, 10) | |
// 偶数の場合に処理する部分関数 | |
val ifEven: PartialFunction[Int, Int] = { case e if e % 2 == 0 => e * 2 } | |
// 偶数以外の場合に処理する部分関数 | |
val ifElse: PartialFunction[Int, Int] = { case e => e * 3 } | |
// 部分関数を合成する. | |
// 偶数だったらifEven関数を、それ以外ならifElse関数を適用するイメージ | |
val proc = ifEven.orElse(ifElse) | |
// 合成した部分関数を適用する. | |
val rslt = list.collect(proc) | |
println(rslt) | |
良い感じですね(思考停止してる)。
これの条件を更に増やして例えば、7の倍数の時はとか素数の時はとか色々な条件をドンドン増やしていくとJavaのコードは大変な事になって、面倒くさいデザインパターンの適用が必要になってくると思います。
しかしScalaのコードで部分関数が増えていって、最後にそれらを合成した関数を適用すればいいだけなので、それほど複雑になりません。
小さな関数を作ってそれを合成していくのはUnix系でよく使われる小さなコマンドをパイプで繋げていくのと同じ感じがして非常に好きです。
ちなみにいちいち部分関数を定義するのは面倒くさいよという場合には以下のように書けます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val list = List.range(0, 10) | |
val rslt = list.collect{ | |
case e if e % 2 == 0 => e * 2 | |
case e => e * 3 | |
} | |
println(rslt) | |
0 件のコメント:
コメントを投稿