Scala的な考え方 - Scalaがとっつきにくいと思っている人へ
Javaな人から見ると、「Scalaって難しい」ってイメージがありますね。俺も最初はそう思ってました。今もですけど。
で、考えてみたんですが、何が難しいって考え方・イディオムになじみがないのが原因かと思ったんです。
ここでは、俺が今までScalaをやってきて得た考え方を紹介します。「Scalaをちょっとやってみたんだけど、とっつきにくくて…」と思われている方は、ぜひご一読ください。
参考資料:
Scala入門 - Scalaで書きはじめたJava使い向け - Scala勉強会@東北
Dropbox - 404
神は言われた。「リストあれ。」
Lisperは、リストをどう作るかをまず考えるらしいです。適切なリストが出来たら、プログラムはもうできたも同然だと。同じ考え方は、Scalaでも通用すると思います。
大まかに、こんな流れで考えてます。(リストは最初から与えられることもあるでしょう)
「(A)リストを作る」
→「(B)合成したり、条件に合わせて絞り込む」
→「(C)個々の要素を加工する」
→「(D)個々の要素を利用した操作を行う」
Scalaでは、リスト(SetやMapなどのコレクションを含む)に対して条件にあう要素を抽出したり(filter)、個々の要素を加工した結果を新しいリストとして生成したり(map)、個々の要素に操作を適用したり(foreach)、と上記の流れのそれぞれに合わせたAPIが予め用意されています。このAPIを上記の流れにはめ込んでいけば処理ができあがる、と言うわけです。
簡単な例で考えてみましょう。おなじみのFizzBuzz問題をちょっと改変して、1からNまでの整数の中で、偶数のもののみFizzBuzzする、という問題をといてみます。
先ほどの流れにあてはめると、
「(A)1からNまでのListを作る」
→「(B)偶数のもののみ抽出する」
→「(C)文字列(Fizz,Buzz,FizzBuzz)またはそのままに加工する」
→「(D)結果を出力する」
こうなります。これをScalaのコードで書いてみます
「(A)(1 to N )」
→「(B)filter{ n => n % 2 == 0 }」
→「(C)map{ case n if n % 15 ==0 => "FizzBuzz";case n if % 3 == 0 => "Fizz";case n if n % 5 == 0 => "Buzz"; case n => n.toString } 」
→「(D) foreach{ e => pritnln(e)}」
あとは、これをつなげるだけです。
def fizzbuzz( n:Int ) = { 1 to n // (A)リストを作る } filter{ n => n % 2 == 0 // (B)条件に合わせて絞り込む } map { // (C)要素を加工する case n if n % 15 == 0 => "FizzBuzz" case n if n % 3 == 0 => "Fizz" case n if n % 5 == 0 => "Buzz" case n => n.toString } foreach { e => println( e ) // (D) 操作を行う }
ふつうのFizzBuzzだったら、filterしている(B)を取り除けばよいだけです。
ここでは、単純な整数のリストを考えましたが、例えば文字列はCharのリストですし、MapはTupple2(key,value)のリスト、XMLはscala.xml.Elemのリスト、と様々なものをリストとして捉えることができます。
「これはリストだっ!」と思ったら、上記の考え方が出来るわけですね。
練習問題として、テキストファイルを読み込んで、行番号を付与して出力する処理を考えてみましょう。解答は、Gistに貼っておきます。見る前に、みなさんも考えてみてください。
「変わらない」世界 - immutableなプログラミング
finalな変数、不変(immutable)オブジェクトを利用することで、副作用を抑えた堅牢なプログラムになる、と言われています。ここではその効果について詳しく述べることは控えさせてもらいますが、一般的によい習慣であるのはまちがいないでしょう。
valの利用は事前条件を保証する(かもしれない?)
Scalaでは、"val"で宣言した変数は再代入できません。これだけを聞くと使い勝手がわるそうだ、と思われるかもしれません。
が、変数を利用する時に"変更されていない"ことが保証されているというのは、とてつもない安心感があります。(特にマルチスレッドを利用したり、ある程度大きなアプリケーションでは)
あるクラスのインスタンス変数が"var"(変更可能)な場合、その"var"を利用する処理は、状態に依存してしまうことになります。
URLを表すクラスを例として考えます。このURLクラスは、コンストラクタに文字列でURLをもらい、schemeやhost名などを返す関数をもちます。
class URL(var url:String ) { if( url == null )throw new IllegalArgumentException def scheme = url.split(":").first def hostname = url.split("/").toList.tail.dropWhile("" == ).first def pathinfo = url.split("/").toList.tail.dropWhile("" == ).tail }
このURLオブジェクトのプロパティurlが、どこかのタイミングでnullを代入されてしまったら、hostname関数などを呼び出すとNullPointerExceptionがthrowされてしまうでしょう。
これを防御するには、urlプロパティを利用する処理で事前にnullチェックを行うしかありません。
そもそも、urlがnullの場合にこのURLオブジェクトはどのように振る舞うのが正しい仕様でしょうか? 例外? それともnullを返す?
このurlプロパティが変更不可であれば、上記のような問題に煩わされることはありません。
urlプロパティは、nullではないことが保証されているからです。
URLクラスの宣言で、"var"になっている箇所を"val"に変更すれば、心の平安を得ることができます。
immutableなクラス - caseクラスの利用
先ほどのURLクラスのようなimutableなクラスを作る手っ取り早い方法がScalaにはあります。
caseクラスにすればよいのです。
classキーワードの前に"case"をつけて、コンストラクタ引数のvar/valを外す、だけです。
case class URL( url:String ) { if( url == null )throw new IllegalArgumentException def scheme = url.split(":").first def hostname = url.split("/").toList.tail.dropWhile("" == ).first def pathinfo = url.split("/").toList.tail.dropWhile("" == ).tail }
これだけで、このURLクラスはimutableなクラスになります。他にも様々な恩恵を得ることができます。
- newキーワードなしでURL("http://d.hatena.ne.jp/yuroyoro")のようにオブジェクトを生成できる
- 引数のプロパティが自動的に読み取り専用で公開される
- equals,hashCode,toStringが適切に実装される
- パターンマッチ(後述)で使えるようになる
// newなしでインスタンス生成 scala> val url = URL("http://d.hatena.ne.jp/yuroyoro/" ) url: URL = URL(http://d.hatena.ne.jp/yuroyoro/) // プロパティに代入できない scala> url.url = null <console>:21: error: reassignment to val url.url = null ^ // 読み取りできる scala> url.url res22: String = http://d.hatena.ne.jp/yuroyoro/ // 同じプロパティを持つインスタンス同士は"同値" // (equals,hashCodeが適切に実装されている) scala> url == URL("http://d.hatena.ne.jp/yuroyoro/" ) res23: Boolean = true // toStringも自然に scala> url.toString res24: String = URL(http://d.hatena.ne.jp/yuroyoro/)
Scalaでクラスを作るときはまず、caseクラスで問題ないか考えるところから始まる、といっても過言ではありません。
再帰とimmutable
ScalaのListクラスは、基本はimmutableです。つまり、後から要素を追加したり削除したりできません。(mutableなコレクションを使いたい場合は、scala.collection.mutableパッケージのクラスを使います。
あとからListに要素追加できないなんて不便すぎる、と思うかもしれません。
たとえば、ある文字列中に出現する文字の回数をMap(文字 -> 回数)という形で数える関数を考えてみます。
javaではこんな感じでしょう。最初にMapを用意して、文字が出現するたびにMapの値をインクリメントしていく形です。
static Map<Character,Integer> countChar( String s ){ Map<Character,Integer> m = new HashMap<Character,Integer>(); for ( char c :s.toCharArray() ){ if( m.containsKey(c)){ int cnt = m.get(c); m.put( c, cnt + 1); } else{ m.put(c, 1 ); } } return m; }
同様の処理をScalaで書いてみましょう。
def countChar( s:String ) = { val m = Map.empty[Char,Int] s foreach { c => val cnt = m.getOrElse(c,0) // ここでmに要素を追加したいがmはimmutable m + ( c -> cnt + 1 ) } m }
ScalaのMapはimmutableなので、このアプローチは使えません。ではどうするのでしょうか?
手段は二つあります。
一つは、利用するMapをmutableなMapに変更することです。
もう一つは、再帰を利用して書き直す方法です。
再帰で書き直すと、こんな感じです。
def countChar( s:String ) = { // 再帰させるワーカー関数 // 引数lのCharリストの先頭要素の出現数を+1したMapを返す def count( l:List[Char], m:Map[Char,Int] ):Map[Char,Int] = { if( l.isEmpty ){ // lが空Listだったら再帰終了 m }else { val cnt = m.getOrElse( l.head, 0 ) + 1 // lの残りと先頭要素の出現数を+1したMapを再帰で渡す count( l.tail, m + ( l.head -> cnt ) ) } } // ワーカー関数を空Mapを作って呼び出す count( s.toList, Map.empty[Char,Int] ) }
これで、immutableなMapのままで目的の処理ができました。再帰のためにワーカー関数を作るのはよくやるパターンです。
ちなみに、この集計処理はfoldLeftを利用して一行で書けたりします。この手のコレクションの集計系の処理でfoltLeftを使うのも常套手段です。
def countChar( s:String ) = (Map.empty[Char,Int] /: s ){( m ,c ) => m + ( c -> (m.getOrElse( c, 0 ) + 1)) }
なにがなんでもimmutableがいいの?
そうではありません。immutableなプログラミングは、一般的には簡潔な記述になることが多いですが、varを利用した方がわかりやすいコードになることもあるでしょう。
また、immutableなプログラミングではパフォーマンス面で不利になることがありえます。
このような場合は、すなおにvarを利用した方がよいでしょう。
例えば、ScalaのListクラスは利用者からするとimmutableなクラスですが、内部の実装はvarを使って性能面を考慮しています。
/scala/tags/R_2_7_5_final/src/library/scala/List.scala – Scala
「パターン青!使徒です!」 - ifよりパターンマッチ
条件判断を行わないプログラムというのはまれでしょう。普通はif文で条件分岐を書きますよね?
Scalaで条件分岐を行うには、もちろんif文も利用できますが、パターンマッチ(match式)を利用すると柔軟な記述ができるようになります。
コップ本からの引用ですが、javaのswitchとscalaのmatch式の記述の比較です。
Java:
switch( <セレクター式> ){ case 場合 : 処理 ... }
Scalaのmatch式:
<セレクター式> match {
case パターン => 処理
...
}
これだけですと対して違いが無いように見えます。しかし、Scalaではパターンとして記述できる表現がJavaに比べてはるかに柔軟なのです。
マッチ式の基本
Javaでは、caseの後に書けるのは整数型(Enum含む)と文字型(char)だけでした。
Scalaでは、以下のようなパターンを書くことができます。
ワイルドカードパターン | case _ | あらゆるものにマッチ。ようはdefault |
定数パターン | case "foo" | "foo"など値にマッチ。 |
変数パターン | case n | 全てにマッチするが、マッチした結果をnという変数名で使える。 |
型付きパターン | case d:Double | Double型の場合にマッチ |
パターンガード | case n if n % 2 == 0 | ifで条件を指定。例では偶数のみマッチ |
コンストラクタパターン | case URL( u ) | caseクラスであるURLクラスにマッチし、URLクラスのプロパティurlを変数uに束縛 |
match式で何がうれしいの?
match式は、caseクラスと組み合わせて使うと幸せ度がヤバイことになります。
電話番号のcaseクラスTelを定義します。市外局番、市内局番、加入者番号をプロパティで持つシンプルなものです。
case class Tel( area:String, local:String, sub:String)
携帯電話番号(ここでは090のもののみとします)とそれ以外で処理を分けたい場合、match式でこんなに簡単に書くことができます
scala> val tel = Tel( "090","1234","5678") tel: Tel = Tel(090,1234,5678) scala> tel match { | case Tel("090", l, s ) => println("携帯:090-%s-%s".format( l, s ) ) | case Tel( a, l, s ) => println("電話番号:%s-%s-%s".format( a, l, s ) ) | } 携帯:090-1234-5678
「case Tel("090", l, s ) => ...」はTelオブジェクトのareaプロパティが"090"だったらマッチして、残りのlocalプロパティを変数lに、subプロパティを変数sに束縛させています。
このように、caseクラスに対してコンストラクタパターンでmatchさせることで、そのcaseクラスのオブジェクトが持つプロパティを、マッチした時に変数に束縛して利用できるのです。
正規表現もパターンマッチで使えます。URLを正規表現でscheme,hostname,pathinfoに分割するのはこんな感じです。(正規表現自体は適当です><)
scala> val url = "http://d.hatena.ne.jp/yuroyoro" url: java.lang.String = http://d.hatena.ne.jp/yuroyoro scala> val r = """(.+)://([^/]+)/?(.*)*""".r r: scala.util.matching.Regex = (.+)://([^/]+)/?(.*)* scala> url match { | case r( s, h, _ ) => println( "%sの%s" format( s, h ) ) | case _ => println("URLじゃなくね?") | } httpのd.hatena.ne.jp
Scalaでは「val r = """(.+)://([^/]+)/?(.*)*""".r 」のように正規表現の文字列に.r関数を呼び出すことで正規表現オブジェクトを取得できます。この正規表現オブジェクトはmatch式で利用できて、正規表現内のパターンをmatch式で変数パターンとして束縛して得ることができるのです。
便利ですね?
もう、「条件分岐はmatch式、if文は三項演算子」くらいの勢いのほうがいいかもです。
nullだって? そんなものは忘れたよ - Option
Option型っていうのが便利すぎて鼻血がほとばしります。
scala.Option
「MayBeモナドっ!」なんていうと各方面からフルボッコにされてしまいそうなので控えますが、Optionってのは値が存在するかもしれない状態をあらわすものです。
Optionはcaseクラスです。Option型には、サブタイプとして値が存在する場合のSomeと、値がない場合のNoneがあります。
論よりソースです。javaのMap#getは、値が存在しない場合はnullを返しました。
ScalaのMap#get関数は、引数のkeyに対して値をOption型で"包んで"かえします。値がある場合はSome(値)で、ない場合はNoneが返ります。
scala> val m = Map('a' -> 1 , 'b' -> 2 ) m: scala.collection.immutable.Map[Char,Int] = Map(a -> 1, b -> 2) scala> m.get('z') res97: Option[Int] = None ← 値がないのでNone scala> m.get('a') res96: Option[Int] = Some(1) ← 値があるのでSome(1) scala> m.get('a').get ← Someから値を取り出すのはOption#get res99: Int = 1
なんでわざわざOptionに"包む"のでしょうか?取り出すのが面倒じゃないですか?
そうではないのです。Optionはcaseクラスですので、match式が使えます。match式と組み合わせると、値がある場合の処理とない場合の処理を自然と分離できます。
scala> m.get('b') match { | case Some( n ) => println(n) | case None => println("none.") | } 2
値がある場合のみ処理を行いたい場合は、match式すら必要ありません。Option#foreachでSomeの場合のみ処理を行わせることができます。nullチェックなんて不要です!
scala> m.get('a').foreach{ n => println( "find! %s" format( n ) )} find! 1
Scala LibraryのAPIではnullを返すものは存在しません。値がないかもしれいないAPIはすべてOption型が返されるようになっているのです。
Scalaでnullチェックが必要な局面は、Javaのライブラリを利用する場合でしょう。その場合も、nullだったらNoneを返すようにラップしてあげるのが便利だと思います。
関数を"ものあつかい"する - 関数オブジェクト
リストのところで説明した考え方ですが、なぜ段階にわけているのでしょうか。一つのループの中で、絞り込みや加工や操作を一度にやってもいいじゃないか、と思われる方も多いと思います。
さきほどのFizzBuzz問題、条件を偶数ではなく奇数にしたり、呼び出し側が条件を指定できるようにしたい場合はどのようなアプローチをとりますか?
Javaの場合ですと、偶数か奇数かを判定する処理をメソッドとして抽出する、というアプローチがあります。あるいは特定のinterfaceを実装したオブジェクトに条件判断を委譲する、などでしょうか?
Scalaでは、もっとストレートなアプローチをとります。条件を判断する"関数オブジェクト"を引数にもらうようにするのです。
def fizzbuzz( n:Int, cond:Int => Boolean ) = { 1 to n // (A)リストを作る } filter{ cond // (B)条件を引数の関数オブジェクトで絞る } map { // (C)要素を加工する case n if n % 15 == 0 => "FizzBuzz" case n if n % 3 == 0 => "Fizz" case n if n % 5 == 0 => "Buzz" case n => n.toString } foreach { e => println( e ) // (D) 操作を行う }
これが改良版のfizzbuzz関数です。引数に、"Int型を引数で受け取ってBooleanを返す関数"を追加しました。(B)の条件の絞り込みは、この引数でもらった関数オブジェクトにやらせています。
これで、このfizzbuzz関数の利用者は絞り込みの条件を好きに指定できるようになりました。奇数だろうが、3の倍数だろうがなんだろうが対応できます。
scala> fizzbuzz( 30 , (n:Int) => n % 2 == 1 ) 1 Fizz Buzz 7 ...
つなげてわたす - もしかして:UNIX
最初に説明しましたがリストを処理させるときに、「リストを作って、条件を絞って、加工して…」というように、ちいさな処理をつなげていきました。
この考え方、どこかで聞いたことありませんか?
「UNIXという考え方―その設計思想と哲学 」と言う本にありますが、UNIXの思想の一つに、"小さい機能のコマンドを、パイプでつないで複雑な処理をする"というものがあります。
さきほどの考え方と似ていますね? 俺がScalaでプログラムを書くときには、この"つなげてわたす"というのを意識しています。
何か入力を受け取って結果を返す関数を出来るだけちいさい単位でたくさん用意しておけば、あとあと使い回しが効きます。途中に処理を挟んだり、差し替えたり…。
なので、関数を作るときは、できるだけちいさく、そして結果をListやMapやOptionなど"わたせる形"で返すようにしておくのを心がけています。ListやOptionなら、つないでいる途中で結果が空のListやNoneになってしまった場合は以降の処理は単純にスキップされますし。
結果を返せないUnit型の関数は、この"つなぎ"の終端にしか接続できないので、使い勝手があまりよくありません。Unit型をできるだけ小さな単位にすることも、また意識しています。
くりかえしますが、"つなげてわたす"はとっても大事なことだと思います。