LoginSignup
1
1

More than 5 years have passed since last update.

Scala の for-comprehension

Last updated at Posted at 2014-02-22

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)
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1