Scala の for-comprehension

  • 1
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Option

以下の仕様を満たす Map[String, String] から Option[User] に変換する optUser 関数を作るとします。

object Main extends App {
  case class User(email: String, name: String)

  /** Converts Map[String, String] to Option[User].
   * @param params
   * @return
   */
  def optUser(params: Map[String, String]): Option[User] = None

  assert(optUser(Map.empty[String, String]) ==
    None, "empty parameters")

  assert(optUser(Map("email" -> "foo@example.net")) ==
    None, "invalid keys")

  assert(optUser(Map("email" -> "", "name" -> "Foo")) ==
    None, "email is empty")

  assert(optUser(Map("email" -> "foo@example.net", "name" -> "")) ==
    None, "name is empty")

  assert(optUser(Map("email" -> "foo@example.net", "name" -> "Foo")) ==
    Some(User("foo@example.net", "Foo")), "valid parameters")
}

for-comprehension を使うと、以下のように簡潔に書けます。

def optUser(params: Map[String, String]): Option[User] = 
  for {
    email <- params.get("email") if !email.isEmpty
    name <- params.get("name") if !name.isEmpty
  } yield User(email, name)

for-comprehension は

  • 先頭を flatMap[B](f: (A) ⇒ Option[B]): Option[B]
  • 以降は map[B](f: (A) ⇒ B): Option[B]
  • ifwithFilter(p: (A) ⇒ Boolean): WithFilter

に置き換えてくれますので、以下のコードと同じです。

def optUser(params: Map[String, String]): Option[User] = 
  params.get("email").withFilter(!_.isEmpty) flatMap { email =>
    params.get("name").withFilter(!_.isEmpty) map { name =>
      User(email, name)
    }
  }

無理してパターンマッチで書くと、以下のような冗長なコードになってしまいます。

def optUser(params: Map[String, String]): Option[User] =
  params.get("email") match {
    case Some(email) => 
      if (!email.isEmpty) {
        params.get("name") match {
          case Some(name) =>
            if (!name.isEmpty) Some(User(email, name))
            else None
          case None => None
        }
      } else None
    case None => None
  }

Java 風なコードだと以下のようにも書けます。これはこれで良いような気もします(笑)

def optUser(params: Map[String, String]): Option[User] = {
  val email = params.getOrElse("email", "")
  val name = params.getOrElse("name", "")
  if (!email.isEmpty && !name.isEmpty) Some(User(email, name))
  else None
}

Try

Try でも flatMap map を持っているので for-comprehension が使えます。

BankAccountApp.scala
import scala.util.{Try, Success, Failure}

object BankAccountApp {
  def withdraw(balance: Int, amount: Int): Try[Int] = {
    val x = balance - amount
    if (x >= 0) Success(x)
    else Failure(new Error("balance is less than " + amount))
  }

  def main(args: Array[String]) {
    val result: Try[String] = for {
      a <- Try(args(0).toInt)
      b <- Try(args(1).toInt)
      balance <- withdraw(a, b)
    } yield s"balance: ${balance}"
    println(result)
  }
}

上記のプログラムは

  • args(0): 残高
  • args(1): 引出額

を指定して引出後の残高を表示します。

  • 正常終了なら Success(message: String)
  • 途中で引数や残高の例外が発生した場合は Failure(e: Throwable)

が得られます。

$ sbt
> run-main BankAccountApp
Failure(java.lang.ArrayIndexOutOfBoundsException: 0)

> run-main BankAccountApp 100
Failure(java.lang.ArrayIndexOutOfBoundsException: 1)

> run-main BankAccountApp 100 foo
Failure(java.lang.NumberFormatException: For input string: "foo")

> run-main BankAccountApp 100 101
Failure(java.lang.Error: balance is less than 101)

> run-main BankAccountApp 100 100
Success(balance: 0)

> run-main BankAccountApp 100 50
Success(balance: 50)