2012年2月9日木曜日

Scala Tips / Option (11) - Some/None

OptionのサブクラスであるSomeとNoneを生成するイディオムです。

Scala

Some[Int]とNoneは以下のようにして生成します。(Noneは実際にはシングルトンです。)

val a = Some(10)
val a = None

通常はこの生成方法で良いのですが、一つ問題があります。Some(10)の型はOption[Int]ではなくSome[Int]に、Noneの型もOption[Int]ではなくNone[Nothing]になってしまいます。

scala> val a = Some(10)
a: Some[Int] = Some(10)

scala> val a = None
a: None.type = None

このため、型をOption[Int]にするためには以下のように変数定義に記述するか:

val a: Option[Int] = Some(10)
val a: Option[Int] = None

オブジェクト側に型注釈を付ける必要があります。

val a = Some(10): Option[Int]
val a = None: Option[Int]

いずれもちょっと冗長ですね。

Scalaz

Scalazでは、以下の記述方法でSomeとNoneを生成することが可能です。

val a = 10.some
val a = none[Int]

「10.some」のように任意のオブジェクトのsomeメソッド(Scalazが追加)によってOption[Int]型のインスタンスとしてSome[Int]を生成することができます。また、「none[Int]」のように型名を指定することでOption[Int]型のインスタンスとしてNoneを生成することができます。

scala> 1.some
res53: Option[Int] = Some(1)

scala> none[Int]
res54: Option[Int] = None
BooleanをOptionに変換

Scalazでは、Optionを生成する方法としてBooleanのoptinメソッドを使用する方法があります。(Option(6))

def f(cond: Boolean, value: Int): Option[Int] = {
  cond.option(value)
}

条件が真だった場合は指定した値でSomeを、偽だった場合はNoneを作成します。BooleanをOptionに変換する機能ということができます。

ノート

Some(10)10.some の違いは、一つは見た目として関数型プログラミング的に 10.some の方が見やすいというのがあると思います。この問題はテーマとしていずれ取り上げたいと思います。これは、あくまで主観的なものなので、見た目の問題だけならどちらの記法を採るのかというのは好みの問題となります。

ということで、もっと実利的な意味で 10.some が有益なケースというのを知りたいところです。

Scalazの 10.some という記法が具体的に役に立つのは、関数の引数で型情報まで記述する場合です。

典型的な例がfoldLeftメソッドです。foldLeftメソッドはで畳込みの結果を積算する値の型は、引数に指定する値に付帯する形で指定しなければなりません。

List#foldLeftメソッドを使ってList[Int]の内容を積算するプログラムを例に考えます。Int値がすべて0以上の場合は積算を行いますが、一つでも0未満のものが合った場合はエラーとします。積算が成功した場合はSome[Int]を、失敗した場合はNoneを返します。

まず、初期値として普通にSome(0)を指定してみます。

以下の指定はコンパイルエラーになります。初期値として Some(0) とするとSome[Int]型になってしまうので、Option[Int]として演算ができなくなってしまうわけです。

def f(l: List[Int]): Option[Int] = {
  l.foldLeft(Some(0)) { (a, e) =>
    if (e >= 0) a.map(_ + e) else None
  }
}

これは、以下のように型を明記すれば解決しますが、ちょっと冗長ですね。

def f(l: List[Int]): Option[Int] = {
  l.foldLeft(Some(0): Option[Int]) { (a, e) =>
    if (e >= 0) a.map(_ + e) else None
  }
}

Scalazの 0.some は型がOption[Int]になるので、この問題が発生しません。

def f(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    if (e >= 0) a.map(_ + e) else none
  }
}
Optionに対するfoldLeftでの畳込み

ついでなのでOption操作の復習も兼ねて、foldLeftでOptionに対する畳込みをいくつか書いてみました。

def f1(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    if (e >= 0) a.map(_ + e) else none
  }
}

def f2(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    (e >= 0).fold(a.map(_ + e), none)
  }
}

def f3(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    for (x <- a if e >= 0) yield x + e
  }
}

def f4(l: List[Int]): Option[Int] = {
  l.foldLeft(0.some) { (a, e) =>
    a.withFilter(_ => e >= 0).map(_ + e)
  }
}

同じ処理でも色々な書き方ができます。もう少し複雑な処理だと、それぞれの書き方との相性が出てくるので、色々ストックしておいて適材適所で選んでいくようにしたいですね。

関数説明参考
f1if式を使った普通の手法 Option(4)
f2Booleanのfoldを使った手法Option(8)
f3for式を使った手法Option(7)
f4withFilterとmapを使った手法Option(4)

追記 (2012-02-12)

xuwei_kさんのご指摘で抜けがあることが分かったので補足です。

Some(10)10.some の比較をして、後者の方がOption[Int]という型になるので、使いやすいという話をしました。この点について補足です。

Scalaの標準ライブラリでは Some(10) とは別に Option(10) というOptionの生成方法を用意していて、この場合はOption[Int]型の Some(10) を生成します。本記事の用途では、Someに関しては Option(10) の方法でも実現可能です。Scalazを使わない場合は、こちらを使うとよいでしょう。

ただし、Noneに関しては、ちょっとややこしくなります。

Option(null) で生成できるものの型がOption[NULL]になってしまいます。

格納するオブジェクトがAnyRefである場合には、Option[String](null)Option(null: String) という記法が可能です。(関連「null」)

scala> Option[String](null)
res32: Option[String] = None

scala> Option(null: String)
res29: Option[String] = None

ただし、Anyの場合は使えません。

scala> Option[Int](null)
<console>:8: error: type mismatch;
 found   : Null(null)
 required: Int
Note that implicit conversions are not applicable because they are ambiguous:
 both method Integer2intNullConflict in class LowPriorityImplicits of type (x: Null)Int
 and method Integer2int in object Predef of type (x: java.lang.Integer)Int
 are possible conversion functions from Null(null) to Int
              Option[Int](null)
                          ^

scala> Option(null: Int)
<console>:8: error: type mismatch;
 found   : Null(null)
 required: Int
Note that implicit conversions are not applicable because they are ambiguous:
 both method Integer2intNullConflict in class LowPriorityImplicits of type (x: Null)Int
 and method Integer2int in object Predef of type (x: java.lang.Integer)Int
 are possible conversion functions from Null(null) to Int
              Option(null: Int)
                     ^

以上のようにNoneの場合は、色々と考えないといけないことがあります。この辺の事情を覚えておいてプログラミング中に使い分けても、特にメリットがあるところではないので、Noneを生成する場合は変数や関数シグネチャの型定義またはオブジェクトに対する型注釈で型を補うという方針にしておくのがよいと思います。

scala> None: Option[Int]
res31: Option[Int] = None

Scalazを使っている場合は、本文中にあったように、10.some , none[Int] の記法を使うようにするのが分かりやすくてよいでしょう。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

3 件のコメント:

  1. Option(10)
    などで、Optionのapplyメソッド使うのは ?

    返信削除
  2. 確かにその方法がありました。
    後ほど、情報を追加します。

    返信削除
  3. 情報を追加しました。
    ご指摘ありがとうございます。

    返信削除