case classとパターンマッチ
次いで、case classと組み合わせて使うことで威力を発揮する、パターンマッチ機能を試していきましょう。
今回は、 名前(name)と(age)を有するHumanクラスをcase classとして用意します。
scala> case class Human (val name:String, var age:Int) defined class Human
ためしに、オブジェクトを生成しておきましょう。
scala> val m0 =Human("加藤",55)
m0: Human = Human(加藤,55)
例題として、カンマ(,)で区切られたCSV形式のデータを扱うこととしましょう。ただし、以下のように、扱うのは一癖あるCSVデータです。
田中,32 鈴木,32,ペットの名前はジョンです。 木村 斎藤,42,最近、禁煙しています。 山田,25 ・・(続く)・・
各行の1番目のデータは「名前」、2番目のデータは「年齢」を表していることが分かっているものとします。一見して分かるとおり、各行の列数は一定していません(コメントらしい3番目のデータがあったり、逆に年齢が記述されていなかったり、まったくデータが入っていない行もある)。たわいのない例かもしれませんが、処理するプログラムを書こうとすると、対応しなければならないケースはそこそこありそうです。
こうしたイレギュラーなパターンの多いデータの取扱いは、Scalaが得意とするところです。今回は、CSVに擬したデータを三重引用符(""")で囲い、文字列リテラルとして表現し、その取扱いに挑戦することとします。
scala> val data = """
| 田中,32
| 鈴木,32,ペットの名前はジョンです。
| 木村
| 斎藤,42,最近、禁煙しています。
|
| 山田,25"""
data: java.lang.String =
田中,32
鈴木,32,ペットの名前はジョンです。
木村
斎藤,42,最近、禁煙しています。
山田,25
縦棒(|)は、REPLが自動で出力するものです。
文字列を三重引用符(""")で囲って定義すると、空白行を含め、改行がすべて保存されます。
まずは、前述のコレクションクラスを用いていきましょう。String型であるdata変数に、Javaのsplitメソッドを適用すると、配列データを生成できます。
data.split("\n")
res: Array[java.lang.String] = Array(, 田中,32, 鈴木,32,ペットの名前はジョンです。, 木村, 斎藤,42,最近、禁煙しています。, , 山田,25)
foreachメソッドを用いると、各要素を1行ずつ表示させることができます。
scala> data.split("\n").foreach {println _}
田中,32
鈴木,32,ペットの名前はジョンです。
木村
斎藤,42,最近、禁煙しています。
山田,25
今のところ、データが何もない要素が存在しています。データ長が1文字以上という条件でフィルター(filter)すると、何もない要素を取り除くことができます(※filter中でtrimが使われているのは、データ中にTab(\t)などが入っていたときの対応のためです)。
scala> data.split("\n").filter(_.trim.length > 0 ).foreach {println _}
田中,32
鈴木,32,ペットの名前はジョンです。
木村
斎藤,42,最近、禁煙しています。
山田,25
ここからが本番です。先ほど定義したHumanクラスに各要素を変換処理します。ここでは、要素数が2以上の場合のみHumanクラスを生成し、要素数が1の時には文字列(String)とします。こうした場合、mapによって記述できます。処理を
scala> val humanList =
| data.split("\n").filter(_.trim.length > 0 ).map { s =>
| val arr = s.trim.split(",")
| if (arr.length >2) Human(arr(0),arr(1).toInt)
| else arr(0)
| }.toList
humanList: List[java.lang.Object] = List(田中, Human(鈴木,32), 木村, Human(斎藤,42), 山田)
humanListは、要素として、Humanクラスと、java.lang.ObjectのListとして生成されています。
さて、このhumanListを扱うためのメソッドtryToGetAgeInfoを定義します。humanListが来る型は、ここでは、Human型(Humanクラス)か、文字列型(String)です。そのため、以下のようなパターンマッチを用います。
scala> def tryToGetAgeInfo (str:Any) = str match {
| case Human(n,a) => "%sさんは、%02d歳です。".format(n,a)
| case _ => "%sさんの年齢は不明です。".format(str)
| }
tryToGetAgeInfo: (Any)String
リストの先頭は文字列型であるため、以下のようになります。
scala> tryToGetAgeInfo(humanList(0)) res129: String = 田中さんの年齢は不明です。
リストの2番目の要素はHuman型であるため、以下のようになります。
scala> tryToGetAgeInfo(humanList(1)) res130: String = 鈴木さんは、32歳です。
全体を表示させてみましょう。
scala> humanList.map { tryToGetAgeInfo _}.foreach {println _}
田中さんの年齢は不明です。
鈴木さんは、32歳です。
木村さんの年齢は不明です。
斎藤さんは、42歳です。
山田さんの年齢は不明です。
見事に、不定形のCSVを処理できたようです。
以上、駆け足ではありますが、case classとコレクションクラス、パターンマッチを組み合わせて使ってみました。最大のポイントは、コレクションライブラリのListに属しているmapやfilterというメソッドだと思います。これらは、Listのひとつひとつの要素(ここでは、1行1行のデータ)に対し、特定の条件をつけたり(filter)、変換処理(map)をしたりするもので、高階メソッドと呼ばれることもあります。これらのメソッドは、次々とつなげていくことができます(いわゆるメソッド・チェーン)。また、パターンマッチもポイントでした。
これらの表現は、Scalaのパワーを活かすのに欠かせないものです。興味をお持ちになった方は、ScalaのAPIライブラリなどを参照しつつ、ぜひとも、自らの手でScalaのコードを書いてみてください。
まとめ
今までは、動的言語のお手軽さを取るか、静的言語の厳密さを取るか、の2分の1の選択肢だった所に、Scalaは型を推論するという新しい発想で、2つを高いレベルで両立させてくれています。それ以外にも、近年の情報工学の進展を取り入れた、多くの新しい機能が実装されています。
今回の記事においては、ごく一部の機能しか説明できませんでした。このことは少し残念ではありますが、読者の皆さまにScalaを知っていただき、実際に動作させて試してもらうことができる所までは解説できたのでは、と思っています。Scalaを選択することによって得られる生産性の向上・スケーラビリティの向上などは、新しい言語を覚えるコストを払うに値するものだと考えています。読者の皆様には、ぜひともScalaに触れてもらい、Scalaの良さを感じてもらいたいと思います。
