2012年2月6日月曜日

Scala Tips / Option (8)

Optionから値を取り出すイディオムです。

Optionから値を取り出す処理として以下の2つのコーディングパターンを挙げました。

  • Option[A]からOption[B]に変換
  • Option[A]からBに変換

ここまではOption[A]からOption[B]への変換についてみてきました。今回はOption[A]からBへの変換について考えます。

条件結果演算
Option[A]がSome[A]BAからBを計算
Option[A]がNoneBデフォルト値

Option[A]に有効な値が入っている場合は、この値からBを計算しますが、それ以外の場合はあらかじめ用意しているデフォルト値を返します。

以下では、Option[Int]からStringへの変換を例に考えてみます。Option[A]がNoneの場合はデフォルト値として空文字列「""」を返すことにします。

Java風

if式でOption#isDefinedを使って値の有無を判定します。デフォルト値の「""」はelse句で指定します。

def f(a: Option[Int]): String = {
  if (a.isDefined) a.get.toString
  else ""
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗ですね。

def f(a: Option[Int]): String = {
  a match {
    case Some(b) => b.toString
    case None => ""
  }
}

Scala

Option(3)のmapメソッドとOption のgetOrElseメソッドの合わせ技で実現できます。

def f(a: Option[Int]): String = {
  a.map(_.toString) getOrElse ""
}

Scalaz

Scalazも、Option(3)のmapメソッドとOption の「|」メソッドの合わせ技で実現できます。

def f(a: Option[Int]): String = {
  a.map(_.toString) | ""
}

Scalazでは、今回の用途にぴったりのメソッドOption#cataとOption#foldが用意されています。以下のように使用します。

def f(a: Option[Int]): String = {
  a.cata(_.toString, "")
}
def f(a: Option[Int]): String = {
  a.fold(_.toString, "")
}

foldメソッドはcataメソッド(cataはcatamorphismの略)に分かりやすい名前をつけたもので、全く同じ動作をします。好きな方を使用すればよいでしょう。

以上が汎用的な方法ですが、条件が合えば以下のようにorZeroメソッドや「~」メソッドを使う方法も適用できます。

def f(a: Option[Int]): String = {
  a.map(_.toString) orZero
}
def f(a: Option[Int]): String = {
  ~a.map(_.toString)
}

StringやIntといった型では、Scalazが初期値を持っています。この初期値を単位元と呼びます。デフォルト値がScalazが持っている初期値、すなわち単位元でよい場合は、これを利用するとデフォルト値の指定を省略することができます。今回使用している問題は、デフォルト値が「""」ですが、これはStringの単位元と同じなので、この条件に当てはまります。

このため、OptionがNoneだった場合にデフォルト値として単位元を使うorZeroメソッドと単項演算子の「~」メソッドを使用することができるわけです。

orZeroメソッドと「~」メソッドは好みの方を使えばよいでしょう。orZeroメソッドを使うとちょっとごっつい感じですが、「~」メソッドは簡潔に記述できます。ただ、「~」は単項演算子で、見落としてしまいがちなのと、普通の単項演算子とはちょっと違う感触の使い方なので、プログラムの可読性が必ずしも高くないかもしれません。

ノート

Scalazはモノイドという性質を扱うための型クラスMonoidを用意しています。モノイドの数学的な定義はこちら(Wikipedia)をご覧ください。

数学のモノイドはとても難しそうですが、Scalazの型クラスMonoidが適用されるオブジェクトはざっくりと以下の性質を持つと理解しておけばよいでしょう。

単位元はざっくりいうと a+ee+a の二項演算を行った時に、 a+e=ae+a=a となるeの事です。具体的には、 1+0=10+1=1 における0、 "Hello"+""="Hello"""+"Hello" における「""」です。つまりIntの単位元は0、Stringの単位元は「""」となります。

前述のorZeroメソッドや「~」メソッドは、モノイドの性質である単位元を使用しています。関数fは演算結果がStringでデフォルト値が「""」となる演算を定義しているわけですが、このデフォルト値「""」が、たまたまStringの単位元と同じなので、モノイドの性質を利用しているorZeroメソッドや「~」メソッドを使うことができるわけです。

Scalazでの代表的なMonoidのオブジェクトは、Int、String、Listといったものです。加算的な演算を持っているオブジェクトはだいたいMonoidとして操作できるようになっています。

Int、String、Listといったオブジェクトの共通の性質としてMonoidを定義することで、Monoidを対象にしたロジックを組めば、Int、String、Listといったオブジェクト指向的には全く異なったオブジェクトに同じロジックを適用できるようになります。たとえば、ScalazのValidationはこのモノイドをうまく利用しています。

今回のイディオムでは、Monoidの性質のごく一部を使っているだけでしたが、MonoidはScalazプログラミングでは頻出の極めて重要な型クラスなので少し詳しく説明してみました。

最後に、Monoidの動作をREPLで試してみたものを以下に示します。

単位元はmzero関数で取得できます。

scala> mzero[Int]
res4: Int = 0

scala> mzero[String]
res5: String = ""

scala> mzero[List[Int]]
res7: List[Int] = List()

Monoidの二項演算は|+|メソッドまたは⊹メソッドです。(後者はUnicodeの数学記号になっています。)

scala> 1 |+| 1
res8: Int = 2

scala> 1 |+| mzero[Int]
res9: Int = 1

scala> mzero[Int] |+| 1
res10: Int = 1

scala> "Hello" |+| "World"
res15: java.lang.String = HelloWorld

scala> "Hello" |+| mzero[String]
res16: java.lang.String = Hello

scala> mzero[String] |+| "World"
res17: String = World

scala> List(1, 2, 3) |+| List(4, 5, 6)
res11: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> List(1, 2, 3) |+| mzero[List[Int]]
res13: List[Int] = List(1, 2, 3)

scala> mzero[List[Int]] |+| List(1, 2, 3)
res14: List[Int] = List(1, 2, 3)

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

追加

一部、次回の記事が混入していたので、2月6日8時45分頃に内容を更新しました。

0 件のコメント:

コメントを投稿