2010年3月5日金曜日

OptionとListとforとflatMap

ScalaのOptionのよいところは、仕様を正確に記述できて、仕様違反の検出をしてくれる上に、プログラミングが楽になるというおまけがついているところである。一般的には正確な仕様記述とプログラミング効率は相反することが多いので、この要因が両立するというのはかなりうれしいことだ。

クラスPersonのメソッドaddressの実装は、OptionとListとforを使って無理なく1行に収まっているけど、関数型言語の技を使わないとこの分量ではすまない。このあたりも面白いので少し細かく説明したいところだけど、今回はポイントだけ。

addressメソッドのポイントは、Option[String]のListに対してfor文で「Some(s)」という受け取り方をすることで、以下の処理を簡潔に記述できるところにある。

  • Noneを削除する
  • Some(s)からsを取り出す

「Some(s)」によるパターンマッチングとfor文を組み合わせることで、if文なしでこの処理が行えてしまうのがOptionを使った時のメリット。

Person.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    (for (Some(s) <- List(zip, prefecture, city)) yield s).mkString(" ")
  }
}

もちろん、Optionを使わずnullを使った場合でも以下のように実装すればaddressメソッドは簡潔に記述することができるけど、for文中の「if s != null」ではなく、「Some(s)」という形のパターンマッチングを使って表示する情報の取捨選択を行っているOption方式の方が美しいし、コードも短くバグも起きにくい。小さな差だけれど、コーディング量が増えてくればボディーブローのように効いてくるところだ。

PersonNull.scala
class Person(val name: String) {
  var zip: String = null
  var prefecture: String = null
  var city: String = null

  def address: String = {
(for (s <- List(zip, prefecture, city); if s != null) yield s).mkString(" \
    ")
  }
}

Optionが出てきたらmatch文というのが一つのパターンなので、match文を使う実装を考えてみたんだけど、いいものが思いつかない。無理をして書くと以下のようになるけど、あまり便利ではなさそうだ。というよりOptionをnullに変換して処理を進めるロジックなのでダメプログラムの典型みたいなものである。

PersonMatch.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    (for (s <- List(zip, prefecture, city)) yield {
      s match {
        case Some(s) => s
        case None => null
      }
    }).filter(_ != null).mkString(" ")
  }
}

また、以下のように:

(for (s <- List(zip, prefecture, city; if s.isDefined)) yield \
    s).map(_.get).mkString(" ")

や:

(for (s <- List(zip, prefecture, city)) yield \
    s).filter(_.isDefined).map(_.get).mkString(" ")

とも書けるけど、filterやmapを使い出すとfor文を使う意味があまりない。以下のように直接filterとmapを使った方が格段によい。

List(zip, prefecture, city).filter(_.isDefined).map(_.get).mkString(" ")

実はもっと良い方法がある。scala.Traversable(Scala 2.7系ではscala.Iterable)に定義されているflatMapが魔法のメソッド。Scalaのコレクションクラスは基本的にTraversableの子孫なのでflatMapはどのコレクションクラスでも使うことができる。

addressメソッドはflatMapメソッドを使って以下のように実装することができる。

PersonFlatMap.scala
class Person(val name: String) {
  var zip: Option[String] = None
  var prefecture: Option[String] = None
  var city: Option[String] = None

  def address: String = {
    List(zip, prefecture, city).flatMap(s => s).mkString(" ")
  }
}

OptionのListのflatMapメソッドが以下の処理を綺麗に行なってくれるので、ほとんどロジックらしいものを書かずにメソッドを実現することができた。

  • Noneを削除する
  • Some(s)からsを取り出す

さらに、flatMapメソッドの引数が「s => s」と無変換のクロージャとなっているので、ここに何らかの変換を行うクロージャを指定することで、それほど手間をかけずに、より複雑な処理を記述することができる余地がある。

このようにOptionは、「仕様を正確に記述できて、仕様違反の検出をしてくれる上に、プログラミングが楽になるというおまけがついている」のである。ここまでくるとOptionを積極的に使いたくなってくるでしょう。

もちろん、Listとfor文、パターンマッチング、さらにflatMapメソッドによるイディオムを知らなければ、Optionのこのような能力を引き出すことはできない。逆にmatch文の例のようにおかしなプログラムを書く事になってしまうかもしれない。

Scalaプログラミングは今まで以上にイディオムが鍵になりそうだ。

0 件のコメント:

コメントを投稿