Scalaを使ってみる: (3) 出現回数を数える (mutable版)
Scalaを勉強している.勉強中の身ではあるが,以下を例題として,Scalaプログラムの作り方について説明してみる.
テキストファイル中に現れる英単語の出現回数を数えて,出現回数の多い語から表示する.入力のテキストファイルとしては,Project Gutenberg 中のHalmet を用いる(ファイル名を hamlet.txt,改行はLFにした).
なお利用している環境は,Ubuntu 8.04 LTS上の Scala 2.8.0 RC2 (2010年5月10日リリース), Java 1.6.0である.
mutable Map
英単語の出現回数を数えるには,Mapを用いる.英単語はStringで出現回数はIntなので,型は Map[String,Int] である.
Mapには,内容が変化するmutable Mapと変化しないimmutable Mapがある.
ここでは,JavaのHashMap等と同様のmutable Mapを利用する.
まず,空のMapを生成する.
scala> val words = collection.mutable.Map[String,Int]() words: scala.collection.mutable.Map[String,Int] = Map()
「+=」により値を登録できる.
scala> words += "hamlet" -> 10 words.type = Map((hamlet,10))
ここで「"hamlet" -> 10」は,キーと値の対である.以下のいずれのように記述しても良い.
words += "hamlet" -> 10
words += )(("hamlet",10))(
words += Pair("hamlet",10)
words += Tuple2("hamlet",10)
値が含まれているかどうかは,containsで調べる.
scala> words.contains("hamlet") Boolean = true scala> words.contains("ophelia") Boolean = false
値を取り出すには apply あるいは Map名そのものを用いる.
scala> words.apply("hamlet") Int = 10 scala> words("hamlet") Int = 10
キーが登録されていない場合には,エラーとなる.
scala> words("ophelia")
java.util.NoSuchElementException: key not found: ophelia
getを用いると,scala.Option[Int] 型の値として Some(Int) あるいは None が返ってくる.
scala> words.get("hamlet") Option[Int] = Some(10) scala> words.get("ophelia") Option[Int] = None
getOrElseを用いると登録されていない場合の値を指定できる.
scala> words.getOrElse("hamlet", 0) Int = 10 scala> words.getOrElse("ophelia", 0) Int = 0
したがって,以下のようにすれば文字列を登録した回数を数えるための関数countUpを定義できる.
scala> def countUp(w: String) = words += (w -> (words.getOrElse(w, 0)+1)) countUp: (w: String)scala.collection.mutable.Map[String,Int] scala> countUp("ophelia") scala.collection.mutable.Map[String,Int] = Map((hamlet,10), (ophelia,1)) scala> countUp("ophelia") scala.collection.mutable.Map[String,Int] = Map((hamlet,10), (ophelia,2))
clearは,Mapをクリアする.
scala> words.clear
newが不要な理由
上では,新しいMapを生成するのに collection.mutable.Map[String,Int]() と記述した.なぜ new collection.mutable.Map[String,Int]() ではないのだろうか.逆に new を付けるとエラーとなってしまう.
scala> new collection.mutable.Map[String,Int]() error: trait Map is abstract; cannot be instantiated
scala.collection.mutable.Map は trait (Javaでの interface に相当するがメソッドを継承できる)であり, constructor を持たないため new できない.
では,なぜ collection.mutable.Map[String,Int]() で新しい Map が生成されるのだろうか.実は,こちらの Map は,scala.collection.mutable.Map オブジェクトである(正確には scala.Predef.Map 変数に代入されているオブジェクト).このMapオブジェクトには apply メソッドが用意されているため, collection.mutable.Map[String,Int]() は collection.mutable.Map.apply[String,Int]() として処理される.
scala> collection.mutable.Map.apply[String,Int]() scala.collection.mutable.Map[String,Int] = Map()
初期データを指定する場合も同様である.
scala> collection.mutable.Map[String,Int]("a" -> 1, "b" -> 2) scala.collection.mutable.Map[String,Int] = Map((a,1), (b,2)) scala> collection.mutable.Map.apply[String,Int]("a" -> 1, "b" -> 2) scala.collection.mutable.Map[String,Int] = Map((a,1), (b,2))
たとえば,以下のような内容のファイルを作成する.
object test { collection.mutable.Map[String,Int]("a" -> 1) }
これを -print オプションをつけて scalac でコンパイルすると,以下のように処理されていることがわかる.
scala.collection.mutable.Map.apply( scala.this.Predef.wrapRefArray( Array[Tuple2]{scala.this.Predef.any2ArrowAssoc("a").->(scala.Int.box(1))}. $asInstanceOf[Array[java.lang.Object]]() ) )
foreachメソッド
foreachにより,ListやIterator等のコレクションに対して,繰り返し処理を行える.
scala> getLines("hamlet.txt").flatMap(toWords).take(5).foreach(println) project gutenberg etext of hamlet
出現回数を数えるには,上で定義したcountUpをforeachで繰り返し実行すれば良い.
scala> words.clear scala> getLines("hamlet.txt").flatMap(toWords).foreach(countUp) scala> words scala.collection.mutable.Map[String,Int] = Map((concernings,1), ...)
sortByとsortWithメソッド
次は,出現回数によるソーティングである.
sortByを用いると words の値を用いてソートできるが, Int 上の自然な順序,すなわち昇順にソートされる.
scala> words.keys.toList.sortBy(words(_))
List[String] = List(concernings, tristful, emperor, ...)
逆順にするには, sortBy にscala.math.Ordering[Int].reverseを引数として追加する. sortBy(words(_), Ordering[Int].reverse) ではなく,カッコを追加して sortBy(words(_))(Ordering[Int].reverse) と書くことに注意.
scala> words.keys.toList.sortBy(words(_))(Ordering[Int].reverse)
List[String] = List(the, and, to, of, i, you, a, my, in, it, ...)
あるいは,sortWith を利用して比較関数を与えるのでも良い.
scala> words.keys.toList.sortWith((x,y) => words(x) > words(y))
List[String] = List(the, and, to, of, i, you, a, my, in, it, ...)
forメソッド
次に,英単語と出現回数の対 (pair)を作成する.
map を用いるなら,以下のようになる.
scala> words.keys.toList.sortBy(words(_))(Ordering[Int].reverse).map(w => (w,words(w))) List[(String, Int)] = List((the,1218), (and,1019), (to,834), (of,733), ...)
あるいは for および yield を用いることもできる.
scala> for (w <- words.keys.toList.sortBy(words(_))(Ordering[Int].reverse)) | yield (w,words(w)) List[(String, Int)] = List((the,1218), (and,1019), (to,834), (of,733), ...)
「Scalaを使ってみる」の目次
- (1) ファイルからの入力
- (2) 英単語の抽出
- (3) 出現回数を数える (mutable版)
- (4) Mapのメソッド
- (5) プログラム作成 (mutable版)
- (6) MultiSetを定義する (mutable版)
- (7) immutable MultiSetを定義する
- (8) immutable MultiSetのメソッドを定義する
- (9) immutable MultiSetのメソッドを高速化する
- (10) immutable MultiSetのメソッドを高速化する (続き)
- (11) immutable MultiSetのメソッドをリファクタリング
- (12) Martin Oderskyによるオンライン授業
- (13) Martin Oderskyによるオンライン授業 (第2回)