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), ...)