Option を使いこなすと、より関数型言語らしいコードが書けるようになります。
null
の代わりに使う
Map[K,V]#get(key)
の返り値は Option[V]
)モナドと聞いて怖じ気付く必要はありません。これから詳しく説明していきます。
Option[A]は値が存在するか、しないかを表すクラスで、Some(a)とNoneの二種類の値があります。パターンマッチでこの二種類の値を処理するのが基本です。
val m = Map("A" -> "Apple", "B" -> "Banana")
def lookup(symbol:String) = m.get(symbol) match { // m.get でOption[String]が返る
case Some(name) => name + " is found!"
case None => "No name is found for " + symbol
}
println(lookup("A")) // Apple is found
println(lookup("C")) // No name is found for C
Optionの値がSome(a)の時だけ続きの処理を行うにはmap
を使います。
val m = Map("A" -> "Apple", "B" -> "Banana")
m.get("A") map { f => println(f + "is found") } // Apple is found
m.get("C") map { f => println(f + "is found") } // (何も表示されない)
getOrElse - Optionの値がSome(v)ならvを、それ以外にはdefault値を返す
def getOrElse[B >: A](default: ⇒ B): B
使い方
val v = List(Some("A"), None)
for(each <- v) yield { each.getOrElse("empty") } // List(A, empty)
Optionには、map, filter, foreachなどcollectionでよく使う関数も定義されており、これらもやはり値がSome(x)の場合のみ処理が実行されるように定義されています。詳しくはOptionのAPIを参照してください。
まず例題を示します。
例えば、ユーザー名とパスワードを受け取ってログインする関数login
を作ることを考えます。ユーザー名とパスワードの情報が揃っていないと次の処理ができないので、nullかどうかのチェックが入りますが、以下のように、コードが入り組んでしまいます。
def login(name:String, password:String) : Boolean = {
if(name != null) {
if(password != null)
database.isValidPassword(name, password)
else
false
}
else
false
}
あまりScala的ではないですが、事前にnullかどうかをチェックしてデータに不備があれば早々にreturn(early exit)しる書き方も、C, Javaなどのプログラミング言語でよく使われています。
def login(name:String, password:String) : Boolean = {
if(name == null)
return false
if(password == null)
return false
return database.isValidPassword(name, password)
}
ただし、不備のあった場合の処理(falseを返すコード)が重複して現れてしまうので無駄があります。
事前にOption(v)
として値をラップしておくと、vがnullの場合はNone, それ以外の場合はSome(v)に変換してくれます。
def login(name:String, password:String) : Boolean = (Option(name), Option(password)) match {
case (Some(u), Some(p)) => database.isValidPassword(u, p)
case _ => false
}
ここでパターンマッチを利用するのもありです。
大分コードがすっきりしてきましたが、ユーザー名が与えられていない場合、そこで処理を終了してほしいのですが、上記のコードではパスワードの方も常にSome(p)かどうかを判定しているので無駄がありそうです。
コードをきれいにしつつ、処理の無駄も省くのに登場するのがモナドです。
ScalaのOptionはモナドになっています。モナドはとりあえずmap, flatMapの二種類の関数が定義されているものと理解すればよいでしょう。少なくともこの理解だけですぐに使い始めることができます。
trait Monad[A] {
// Monad[A]の中身Aを取り出し、fを適用して、その結果BをMonad[B]でwrapする
def map[B](f: A => B) : Monad[B]
// Monad[A]の中身Aを取り出し、Monadを返すfを適用する
def flatMap[B](f: A => Monad[B]) : Monad[B]
}
(実際にこのような単体のMonad traitがあるわけではありませんが、同等のものがScala標準ライブラリの中には存在します)
Monadは値をくるむ毛皮のようなもので、map, flatMapはその毛皮を剥がしてから何かの操作を行い、その結果に対してまた毛皮を着せる操作に対応しています。Monadを使ったコードでは、中に含まれている値に対して何らかの操作を行っても、Monad[A] からMonad[Monad[B]]のようなネストした型に変換するのではなく、Monad[A] -> Monad[B]と毛皮を一枚で済ませるようにするのが特徴です。
ScalaのOptionがmonadになっていると言いましたが、では実際にOptionの実装の一部を簡単にしたものを取り出して見てみましょう。
sealed trait Option[A] {
def map[B](f: A => B) : Option[B]
def flatMap[B](f: A => Monad[B]) : Option[B]
}
case class Some[A](a: A) extends Option[A] {
def map[B](f: A => B) : Option[B] = Some(f(a))
def flatMap[B](f: A => Monad[B]) : Option[B] = f(a)
}
case class None[A] extends Option[A] {
def map[B](f: A => B) : Option[B] = None
def flatMap[B](f: A => Monad[B]) : Option[B] = None
}
map, flatMapは、Someの中身の値a
に対して実行されますが、Noneの場合、中身がないので、map, flatMapは共に実行結果としてNoneが返ります。ここで注目しておきたいのは、Noneに対してもmap, flatMapが定義されているので、map, flatMapの操作は、Optionの値がSomeであろうとNoneであろうと連続して適用していけるという点です。
if文やパターンマッチを使ったnull(またはNone)の値のチェックは、ログインをするためのコードの本質的な部分ではないので、本来はエラー処理の部分を気にせずプログラミングできることが好ましいはずです。ここでmonadのmap, flatMapを使うとコードの流れを妨げずに、必要な処理に的を絞ってコードを書けるようになります。以下はその例です。
def login(name:Option[String], password:Option[String]) : Boolean =
name flatMap { u => password map { p => database.isValidPassword(u, p) } } getOrElse false
nameやpasswordがNoneの場合、map, flatMapの適用結果はNoneになるだけなので、コード中に出てくるエラー処理は最後のgetOrElseの部分のみになり、残りはエラー処理を気にせずに一本道で書けます。
コメントを挟んで、コードの中身をより詳しく説明すると以下のようになります。
def login(name:Option[String], password:Option[String]) : Boolean = {
val r = name flatMap { u => // name monadの中身がとりだされる
password map { p => // password monadの中身が取り出される
database.isValidPassword(u, p)
} // mapなので、monadを外したBooleanが返る
}
// rの型はbooleanをmonadでくるんだ Option[Boolean]
r getOrElse false // getOrElseもmonadを剥がす。
ただし、map, flatMapを活用すると、コードは一行に収まるもののやや面倒な書き方になってしまうのが玉に瑕です。
そこでScalaのfor文による網羅(for-comprehension)を使うと、上記のコードを手短に書けるようになります。
def login(name:Option[String], password:Option[String]) : Boolean =
val r = for(u <- name; p <- password) yield database.isValidPassword(u, p)
r getOrElse false // user, passの情報が揃って無い場合にはfalseが返る
これがどうmonadなのか不思議に思うのは当然ですが、for-comprehensionが具体的に何をしているのかを知れば納得できるはずです。
Scalaのfor-comprehensionは、map, flatMapなどmonadによる操作を簡潔に使うためのsyntax sugarとなっています。以下に置き換えの定義を示します。
for { p0 <- e0 } yield e
は、mapを使って以下に置き換えられます。
e0 map { p0 => e }
for {
p0 <- e0
p1 <- e1
...
pn <- en } yield e
一番外側のパラメータがflatMapに置き換えられます。これが再帰的に繰り返され、最後に上記のパラメータが1つの場合のルールによりmapが適用されます。
e0.flatMap { p0 =>
for {
p1 <- e1
p2 <- e2
...
pn <- en
} yield e
}
これらの変換を適用すると、先に述べたfor文によるlogin関数のコードがmap, flatMapで置き換えたものと同等になることを確認してください。