web-dev-qa-db-ja.com

Scalaコードでエラーを処理するためにEitherを使用する

Optionモナドは、Scalaで何かを処理するための優れた表現方法です。しかし、「何も起こらない」ときにメッセージをログに記録する必要がある場合はどうでしょうか。 Scala APIドキュメントによると、

Eitherタイプはscala.Optionの代わりによく使用されます。Leftは(慣例により)失敗を表し、RightはSomeに似ています。

しかし、失敗を処理するためにEitherまたはEitherを含む実際の例を使用して、ベストプラクティスを見つけることができませんでした。最後に、自分のプロジェクト用に次のコードを思いつきました。

    def logs: Array[String] = {
        def props: Option[Map[String, Any]] = configAdmin.map{ ca =>
            val config = ca.getConfiguration(PID, null)
            config.properties getOrElse immutable.Map.empty
        }
        def checkType(any: Any): Option[Array[String]] = any match {
            case a: Array[String] => Some(a)
            case _ => None
        }
        def lookup: Either[(Symbol, String), Array[String]] =
            for {val properties <- props.toRight('warning -> "ConfigurationAdmin service not bound").right
                 val logsParam <- properties.get("logs").toRight('debug -> "'logs' not defined in the configuration").right
                 val array <- checkType(logsParam).toRight('warning -> "unknown type of 'logs' confguration parameter").right}
            yield array

        lookup.fold(failure => { failure match {
            case ('warning, msg) => log(LogService.WARNING, msg)
            case ('debug, msg) =>   log(LogService.DEBUG, msg)
            case _ =>
        }; new Array[String](0) }, success => success)
    }

(これは実際のプロジェクトのスニペットであるため、それ自体ではコンパイルされないことに注意してください)

コードでEitherをどのように使用しているか、および/または上記のコードをリファクタリングするためのより良いアイデアを知っていただければ幸いです。

50

Optionが単一の意味のある結果を返すか何も返さないのとは異なり、どちらかが可能な2つの意味のある結果の1つを返すために使用されます。

わかりやすい例を以下に示します(Scalaメーリングリストでしばらく前に回覧されました)。

def throwableToLeft[T](block: => T): Either[Java.lang.Throwable, T] =
  try {
    Right(block)
  } catch {
    case ex => Left(ex)
  }

関数名が示すように、「block」の実行が成功すると、「Right(<result>)」が返されます。それ以外の場合、Throwableがスローされると、「Left(<throwable>)」を返します。パターンマッチングを使用して結果を処理します。

var s = "hello"
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints "HELLO"

s = null
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints NullPointerException stack trace

お役に立てば幸いです。

50
Walter Chang

Scalazライブラリには、検証という名前のいずれかが似ています。 「有効な結果または失敗を取得する」として使用する場合は、どちらよりも慣用的です。

検証では、エラーを蓄積することもできます。

編集: "alike" Validationはアプリケーションファンクタであるため、どちらかは完全にfalseであり、scalaz Either(\ /(「disjonction」または「どちらか」と発音))はモナドです。検証がエラーを累積できるという事実は、その性質によるものです。一方、/は「早期停止」の性質を持ち、最初に-\ /(「左」または「エラー」)で停止します。ここに完全な説明があります: http://typelevel.org/blog/2014/02/21/error-handling.html

参照: http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html

コメントで要求されているように、上記のリンクのコピー/貼り付け(一部の行は削除されています):

// Extracting success or failure values
val s: Validation[String, Int] = 1.success
val f: Validation[String, Int] = "error".fail

// It is recommended to use fold rather than pattern matching:
val result: String = s.fold(e => "got error: " + e, s => "got success: " + s.toString)

s match {
  case Success(a) => "success"
  case Failure(e) => "fail"
}

// Validation is a Monad, and can be used in for comprehensions.
val k1 = for {
  i <- s
  j <- s
} yield i + j
k1.toOption assert_≟ Some(2)

// The first failing sub-computation fails the entire computation.
val k2 = for {
  i <- f
  j <- f
} yield i + j
k2.fail.toOption assert_≟ Some("error")

// Validation is also an Applicative Functor, if the type of the error side of the validation is a Semigroup.
// A number of computations are tried. If the all success, a function can combine them into a Success. If any
// of them fails, the individual errors are accumulated.

// Use the NonEmptyList semigroup to accumulate errors using the Validation Applicative Functor.
val k4 = (fNel <**> fNel){ _ + _ }
k4.fail.toOption assert_≟ some(nel1("error", "error"))
13
fanf42

あなたが投稿したスニペットは非常に不自然なようです。次の状況でEitherを使用します。

  1. データが利用できないことを知るだけでは十分ではありません。
  2. 2つの異なるタイプのいずれかを返す必要があります。

例外を左に変換することは、実際、一般的な使用例です。 try/catchに比べて、コードをまとめて保持できるという利点があります。これは、例外が期待される結果の場合に意味があります。いずれかを処理する最も一般的な方法は、パターンマッチングです。

result match {
  case Right(res) => ...
  case Left(res) => ...
}

Eitherを処理するもう1つの興味深い方法は、コレクションに現れる場合です。コレクションに対してマップを行う場合、例外をスローすることは現実的ではない可能性があり、「不可能」以外の情報を返したい場合があります。 Eitherを使用すると、アルゴリズムに負担をかけずにそれを行うことができます。

val list = (
  library 
  \\ "books" 
  map (book => 
    if (book \ "author" isEmpty) 
      Left(book) 
    else 
      Right((book \ "author" toList) map (_ text))
  )
)

ここでは、ライブラリ内のすべての著者のリストを取得しますplus著者のいない本のリスト。そのため、それに応じてさらに処理できます。

val authorCount = (
  (Map[String,Int]() /: (list filter (_ isRight) map (_.right.get))) 
   ((map, author) => map + (author -> (map.getOrElse(author, 0) + 1)))
  toList
)
val problemBooks = list flatMap (_.left.toSeq) // thanks to Azarov for this variation

つまり、基本的なEitherの使用法はそのようになります。これは特に便利なクラスではありませんが、以前に見たことがあるとしたら、一方、それも無駄ではありません。

7

Catsには、例外をスローするコードからEitherを作成する素晴らしい方法があります。

val either: Either[NumberFormatException, Int] =
  Either.catchOnly[NumberFormatException]("abc".toInt)
// either: Either[NumberFormatException,Int] = Left(Java.lang.NumberFormatException: For input string: "abc")

inhttps://typelevel.org/cats/datatypes/one.html#working-with-exception-y-code =

0
vlfig