第4回 Functional忍者 Option → Try → Future
Table of Contents
1 今日の概要
Option[A]/Try[A]/Future[A]は一つのA型の値を抱えるコンテナです。
| 型 | 説明 | ScalaDoc |
| Option[A] | A型の値を持っている。でも、持ってないかもしれない | http://www.scala-lang.org/api/current/#scala.Option |
| Try[A] | うまくいったら、A型の値を持っている。失敗してる場合、失敗についての情報を持っている | http://www.scala-lang.org/api/current/#scala.util.Try |
| Future[A] | そのうちA型の値を持つかもしれない。失敗するかもしれない | http://www.scala-lang.org/api/current/#scala.concurrent.Future |
Optionを使いこなすことができれば、TryやFutureへの発展は簡単です。Optionに重点をおいて、Try、Futureも説明をします。
1.1 注意
サブクラスがある都合でシグネチャが分かりにくくなるところは、適当に書き換えてます。
2 Option
2.1 型の定義
Option[A]型の値はSome(値あり)かNone(値なし)のいずれかの型になる。
sealed trait Option[+A] case class Some[+A](x: A) extends Option[A] case object None extends Option[Nothing]
2.2 Map[A,B]でのOptionの例(Aがキーの型、Bが値の型)
Mapにkeyを渡して、valueを取得するgetメソッドは下記のシグネチャになっている。 Mapにkeyに対応する値があればSome(value)、なければNoneを返す。 (型をみれば、値がないことがありうることが分かってうれしい)
// Mapの定義
trait Map[A,B] {
def get(key: A): Option[B]
}
// 実行例
val dict = Map("one" -> 1, "two" -> 2)
dict.get("one") // -> Some(1):Option[Int]
dict.get("three") // -> None:Option[Int]
2.3 大事なメソッド
2.3.1 def map[B](f: A => B): Option[B]
中に抱えている値を変換する時に使う。中の値がない場合(レシーバがNoneの場合)はNoneを返す。
val dict:Map[String, Int] = Map("one" -> 1, "two" -> 2)
def stringSucc(strNum: String):Option[Int] = {
val num = dict.get(num)
num.map(_ + 1)
}
stringSucc("one") // Some(2)
stringSucc("unknown") // None
2.3.2 def flatMap[B](f: A => Option[B]): Option[B]
Optionがネストするようなケースで使う。 例えば、2つのOption型の値があり、その両方がSomeのときにそれぞれの中の値を使って何かしたい。どちらか一方でもNoneならばNoneを返す。
val dict = Map("one" -> 1, "two" -> 2)
def stringAdd(x: String, y: String):Option[Int] = {
val optX = dict.get(x)
val optY = dict.get(y)
optX.flatMap{ a => // このflatMapをmapにするとOption[Option[Int]]になるので、flattenが必要になる。map + flatten = flatMap (※)
optY.map{ b =>
a + b
}
}
}
stringAdd("one", "two") // Some(3)
stringAdd("one", "unknown") // None
(※) 実際の実装はmapしてflattenしているわけではありません。
2.4 for内包表記(for comprehension)
先のflatMapやmapを使ったstringAddの例は高階関数がネストしていて読みにくい。 下記のようにfor内包表記を使うとすっきり書ける。(for内包表記はflatMap, mapの糖衣構文)
def stringAdd(x: String, y: String):Option[Int] =
for {
a <- dict.get(x) // この行はflatMapに変換される
b <- dict.get(y) // 最後はmapに変換される
} yield a + b
矢印(<-)の右側をOption[X]型の式にすると、矢印の左側の変数に中に抱えている値が入る。 矢印の右側のOptionのうち、ひとつでもNoneがあると全体がNoneになる。
for {
x <- (Option[X]型の式)
// ここより下ではxを参照すれば中の値が使える
y <- (Option[Y]型の式)
// ここより下でyが...
z <- (Option[Z]型の式)
} yield ... (ここでは、x,y,zが使える) ...
2.5 Lifting(持ち上げ)
- T型の値をOption[T]型にすることをOptionの文脈(context)にliftする(持ち上げる)、という
- 逆にOption[T]型からT型にすることを文脈からunliftする(日本語訳を知らない)、という
パターンマッチやgetOrElseメソッドなどを使うことでunliftできる。 つまり、Optionの文脈から出るには、Option[T]がNoneの場合にT型の値をどう作るかが必要になる。 (型が正しい実装を要求してくる。うれしい)
値があるかないか分からないという文脈の中で、あるなら何かするけど、ないなら何もしない、 という場合は結果もOption型になる。 こういう場合は、mapやflatMapなどのメソッドを使うと、liftしたまま処理を記述できる。 for内包表記を使う事で、mapやflatMapの処理が裏に隠されて、その型の文脈での処理を素直に書ける。
2.6 その他のメソッド
2.6.1 def filter(p: A => Boolean): Option[A]
特定の条件で、中に抱えている値を捨てる時に使う。
val dict:Map[String, Int] = Map("one" -> 1, "two" -> 2)
def onlyEven(x: String) = dict.get(x).filter(_ % 2 == 0)
2.6.2 def getOrElse(default: => A): A
中の値を取り出す。中の値がない場合は引数で渡された値を返す。 引数は名前渡しなので、レシーバがNoneの場合のみ実行される。
val cache = Map(1 -> "one", 2 -> "two")
def heavyProcess(x:Int):String = {
Thread.sleep(3000)
x.toString
}
def getWithCache(x:Int):String = cache.get(x).getOrElse(heavyProcess(x))
2.6.3 nullになりうるT型の値をOption[A]にする
Option(x)とするとxがnullの場合None、それ以外はSome(x)になる
val dict = Map(1 -> "one", 2 -> "two") import scala.collection.JavaConverters._ val javaDict = dict.asJava javaDict.get(3) Option(javaDict.get(3)) Option(javaDict.get(1))
2.7 演習
下記のコードのgenerate_simpleとgenerate_complexをOptionを使って、リファクタリングしてください。 紹介したメソッド以外にも便利なメソッドがあります。ScalaDocを参照してください。 コードはgithubにあります。 → http://github.com/maedaunderscore/ninja-future/
object Main {
sealed trait Emotion
case object Love extends Emotion
case object Hate extends Emotion
type User = String
class Conversation(
affinity: Map[(User, User), Emotion],
message: Map[(User, Emotion), String]
){
def generate_simple(u1: User, u2: User): Option[String] = {
if(affinity.contains((u1, u2))){
val emotion = affinity((u1, u2))
if(message.contains((u1, emotion))){
Some(template(u1, u2, message((u1, emotion))))
} else None
} else None
}
def generate_complex(u1: User, u2: User): Option[String] = {
if(affinity.contains((u1, u2)) && affinity.contains((u2, u1))){
val emotion1 = affinity((u1, u2))
val emotion2 = affinity((u2, u1))
if(emotion1 == emotion2) {
if(message.contains((u1, inverse(emotion1)))){
Some(template(u1, u2, message((u1, inverse(emotion1)))))
}else if(message.contains((u1, emotion1))){
Some(template(u1, u2, message((u1, emotion1))))
}else None
} else None
} else None
}
private def template(u1: User, u2: User, msg: String) = s"$u2「$msg?」\n$u1「$msg!」"
private def inverse(e: Emotion) = e match {
case Love => Hate
case Hate => Love
}
}
}
3 Try
3.1 定義
package scala.util sealed trait Try[+A] case class Success[+A](x: A) extends Try[A] case class Failure[+A](ex: Throwable) extends Try[A]
map, flatMap, filterはOptionと同じように使える。
3.2 演習
- Try型のmap, flatMap,filterの動作を確認しましょう。特に例外が発生したときの動作を確認してください。 Tryの実装を確認しましょう。(scala.util.control.NonFatalはExtractorです。)
- Map[A,B]#get(key:A):Option[B]メソッドについて、戻り値がTry[B]のバージョンの関数を作りましょう。
- 先のOptionの演習で、Optionの代わりにTryを使って、なぜ失敗したのかの理由を残すようにしましょう。
4 Future
4.1 Futureの値の作り方(ヘルパを使う)
import scala.concurrent._
// Future型の演算をする場合はExecutionContextの宣言が必要。
import scala.concurrent.ExecutionContext.Implicits.global
def x : Future[Int] = future {
val wait = scala.util.Random.nextInt(4) + 1
Thread.sleep(wait * 1000)
wait
}
// 同期して値を取得(うれしくない)
import scala.concurrent.duration._
Await.result(x, 10 seconds) // 最大10秒待つ。
// 非同期で処理(うれしい)
// def onComplete(f: Try[A] => Unit):Unit
x.onComplete(println)
// map, flatMap, filterも非同期で動く
x.map{x => s"it took ${x} seconds"}.onComplete(println)
4.2 Futureの値の作り方(Promiseを使う)
Promise[A]からFuture[A]を生成できる。Promise.successまたはPromise.failureに値を渡すと、 生成したFutureの値が確定する
val promise = Promise[Int]() val x = promise.future x.isCompleted promise.success(3) x.isCompleted x.value
4.3 演習
- Future型のmap, flatMap, filterの動作を確認しましょう。
- Future型の値をunliftするにはどうすればよいですか? Futureからunliftすることは何を意味しますか?
- Map#getメソッドについて、Future型で返すバージョンを作りましょう。 ランダム秒のsleepを入れて、非同期感を演出してください。
- 先のTryの演習について、Tryの代わりにFutureを使って、非同期に処理をするようにしましょう。 Mapから値を取り出す際には、3.で作成した関数を使って時間がかかるようにしてください。 FutureはorElseメソッドはありません。2つのFutureを引数にして先に成功/失敗した方のFutureを返す関数を作って置き換えてください。
- 4.について、どの処理が非同期に実行されるか、考えてみてください。 また、sleepの前後にログを仕込んで実際の動作を確認してください。