第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 演習

  1. Try型のmap, flatMap,filterの動作を確認しましょう。特に例外が発生したときの動作を確認してください。 Tryの実装を確認しましょう。(scala.util.control.NonFatalはExtractorです。)
  2. Map[A,B]#get(key:A):Option[B]メソッドについて、戻り値がTry[B]のバージョンの関数を作りましょう。
  3. 先の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 演習

  1. Future型のmap, flatMap, filterの動作を確認しましょう。
  2. Future型の値をunliftするにはどうすればよいですか? Futureからunliftすることは何を意味しますか?
  3. Map#getメソッドについて、Future型で返すバージョンを作りましょう。 ランダム秒のsleepを入れて、非同期感を演出してください。
  4. 先のTryの演習について、Tryの代わりにFutureを使って、非同期に処理をするようにしましょう。 Mapから値を取り出す際には、3.で作成した関数を使って時間がかかるようにしてください。 FutureはorElseメソッドはありません。2つのFutureを引数にして先に成功/失敗した方のFutureを返す関数を作って置き換えてください。
  5. 4.について、どの処理が非同期に実行されるか、考えてみてください。 また、sleepの前後にログを仕込んで実際の動作を確認してください。

Date: 2013-12-10 18:29:34 JST

Author: Yasuyuki Maeda(@maeda_)

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0