目次

Version 6, last updated by Chaba at Aug 01 00:46 UTC

次の記事も参考にしてください。

はじめに

Lift の Box[T] モナドは、Scala の Option[T] に似ています。

  • Option は Some と None の 2 つの値を持ちます。
  • Box は Full、Empty、および Failure の 3 つの値を持ちます。

どちらも、いくつかの値に特別な意味を持たせることを回避するために作成されました。
たとえば、NULL ポインタは “nowhere” を意味する値 0 です。NULL ポインタは明示的にチェックする必要があります。なぜならコンピュータの 0 は、同時にほかの数でもあるからです。

ボックスの使い方

  • val s = Full(“Thing”)
  • val e: Box[String] = Empty

ボックスの開き方

  • val myString: String = s openOr “(none)”
  • val fail = s ?~ “Opening the box failed”
  • s match { case Full(myS) => println(myS); case _ => println(“not found”) }

なぜ値を箱に入れるのか

NullPointerException をスローしたことはないですか? 率直に言って、開発中に予期しないできごとが起き、それに続いて NullPointerException が発生するのを経験したことがない開発者はいないでしょう。簡単に言えば、NullPointerException は、想定していなかった一連のできごとによって生じる状態で、さまざまな形でアプリケーションの動作不良を引き起こします。もちろん、これは望ましくないことです。

たとえば、データベースから返された値に何らかの処理を加えるシナリオを考えてみましょう。データベースクエリで適切な結果が得られることを前提にして、その結果に対して何らかの操作を行うプログラムはちっとも珍しくありません。しかし、データベースクエリが期待どおりに動作せず、想定外の結果が帰ってくることも、大いにありうることです。

そこで役に立つのがボックスです。名前から想像できるように、Box とはそこに何かを入れたり、何かを取り出したり、空だったりすることができるものです。現実の世界のボックス、箱とほとんど同じようなものです。簡単な例で説明しましょう。

import net.liftweb.common.{Box,Full,Empty,Failure,ParamFailure}

scala> val x: Box[String] = Empty
x: net.liftweb.common.Box[String] = Empty

scala> val y = Full("Thing")
y: net.liftweb.common.Full[java.lang.String] = Full(Thing)

上に 2 つの簡単な例を示しました。どちらも、val に新しいボックスを割り当てています。コードを見るとわかるように、xBox[String] として割り当てられていますが、実際の値は、Box のサブタイプである Empty です。2 番目の y への割り当てでは、最初のものとタイプシグニチャーは同じですが、今度は値 (“Thing”) を持った “Full” です。

例外はどう処理されるか

Lift には、net.liftweb.util.Helperstryo と呼ばれるヘルパメソッドがあります。tryo は特別な制御構造で、ブロック内でコードを実行すると、ブロック内で行った処理や実行結果がどうなったかに関係なく、Box 化された値を返すことができます。lift-webkit を使っている場合には、すでにこうしたユーティリティメソッドが利用できるようになっていますが、身軽なままでいて webkit を使わない場合には、tryo の定義は次のようなものになります。

def tryo[T](
  ignore: List[Class[_]], 
  onError: Box[Throwable => Unit])
  (f: => T): Box[T] = {
    try {
      Full(f)
    } catch {
      case c if ignore.exists(_.isAssignableFrom(c.getClass)) => 
        onError.foreach(_(c)); Empty
      case c if (ignore == null || ignore.isEmpty) => 
        onError.foreach(_(c)); Failure(c.getMessage, Full(c), Empty)
    }
}

構文上必要な要素があるので少しわかりにくくなっているかもしれませんが、本質的には次のことを行います。

tryo {
  // ここにコードを記述
}

たとえば、あるリモート API を呼び出すとして、この API が相手先に接続できなかったり、予想外の形でクラッシュしたりした場合、tryo ブロック / ラップがあるとどうなるのでしょうか。まず、次の例を見てください。

scala> tryo("www".toInt) 
res4: net.liftweb.common.Box[Int] = Failure(
  For input string: "www",
  Full(java.lang.NumberFormatException: For input string: "www"),
  Empty)

“www” は Int ではないため、当然ながら変換できず、java.lang.NumberFormatException がスローされます。しかし、tryo (ボックス化された値) を使うと、Failure と呼ばれる特別な Box サブタイプを受け取ることができます。このことを利用すれば、考えうるすべての結果をチェックしたり、長大な try-catch ブロックを用意したりしなくても、簡潔にエラーを処理できます。

scala> tryo("www".toInt) openOr 1234                       
res7: Int = 1234

scala> tryo("www".toInt).map(_.toString).openOr("Invalid Strings")
res8: java.lang.String = Invalid Strings

scala> for(x <- tryo("www".toInt)) yield x
res11: net.liftweb.common.Box[Int] = Failure(
  For input string: "www",
  Full(java.lang.NumberFormatException: For input string: "www"),
  Empty)

上の例から、BoxFull、および Failure サブタイプのさまざまな使い方、インラインでデフォルトを用意し、結果をマップする方法などがわかると思います。

空の値の扱い

では、予想外の動作をしたときに詳しい情報がほしい場合や、見つけたい値が失敗ではなく、値が Empty (Scala の Option では None) のケースである場合は、どうすればよいのでしょうか。たとえば、REST API または同様の API のリクエストパラメータを取得する場合を考えてみましょう。目的のパラメータがない場合、何も理由を付けずに HTTP 500 エラーを返すのではなく、もっとも具体的な情報をユーザーに渡した方が有益です。次に示すのは、Lift の S.param メソッドを使ってリクエストパラメータを取得する場合の例です。

scala> S.param("id") ?~ "You must supply an ID parameter" 
res17: net.liftweb.common.Failure = 
  Failure(You must supply an ID parameter,Empty,Empty)

?~ メソッドを使うと、エラーメッセージを指定して EmptyFailure に変換することができます。これは、for-comprehension と一緒に使った場合に非常に大きな効果を発揮します。

ボックスのチェイン

いろいろな場所から値を受け取る場合、あるいは一連の複数の値を調べて、特定の結果へと “fall through” する必要がある場合を考えてみましょう。Box では or オペランドによって、このような操作をサポートしています。次に例を示します。

scala> val x = Empty
x: net.liftweb.common.Empty.type = Empty

scala> val y = tryo("qqq".toInt) 
y: net.liftweb.common.Box[Int] = 
  Failure(For input string: "qqq",
  Full(java.lang.NumberFormatException: For input string: "qqq"),
  Empty)

scala> x or y or Full("Default")
res18: net.liftweb.common.Box[Any] = Full(Default)

Box にはここに示した以外にもたくさんの機能がありますが、この記事が Box という option 風の特別な型の理解に役立てばさいわいです。

中身の変換

ボックスの中身は、単純に map メソッドで変換できます。map メソッドを使うと、変換後の値の入ったボックスが返されます (ボックスがフルの場合)。よくある別のパターンとして、ボックス内の変換結果が不要な場合には、次のようにします。

myInputValue.map(_.transformIt) openOr myFallbackValue

関連メソッドに choice があります。このメソッドを使うと、Box[A] から Box[B] へと、より複雑な変換を行うことができます。