主にWebアプリケーションを書くときに、エラーやValidation結果を返すのに、いろいろと悩むことがありますよね。
Option?
Either?
Try?
/?
で最近は、けっこーTry一択で実装してて、なんとなしにパターン的なものが掴めてきた気がするので、自分用も兼ねてまとめてみた。
Try型は低レイヤーのメソッドで生成しよう
最も小さいメソッドでTryを生成し、そのメソッドを呼び出すメソッド内では、for式を使うと良い。
lazy val assertNotEmpty: String => Try[Unit] = (value: String) => Try {
assert(value.nonEmpty, "value is empty")
}
lazy val findBy: String => Try[String] = (id: String) => Try {
someTable.findBy(id)
}
def printSomething(id: String): Try[Unit] =
for {
u <- assertNotEmpty(id)
some <- findBy(id)
} yield
println(some)
みたいな。
Try.apply
で処理を受けてやるのは、小さい粒度?のメソッドでやってやって、そいつを呼ぶメソッドでは、for式で。
こうすると、スッキリとしたコードが書けて良いなって思ってる。
これを守らなかったら、
lazy val assertNotEmpty: String => Unit = (value: String) =>
assert(value.nonEmpty, "value is empty")
lazy val findBy: String => String = (id: String) =>
someTable.findBy(id)
def printSomething(id: String): Try[Unit] = Try {
assertNotEmpty(id)
val result = findBy(id)
println(result)
}
こんな感じになって、なんか、valとか入るし、printSomethingの実装のしかたがあんまり決まってこないし、なんかビミョー。
って気がする。
小さい単位でTry型を返すようにしてやると、そいつを呼び出すところの処理がある程度、決まってくるから、なんか書いてて気持ちいい。
仕様が変わったり、処理を追加したりにも柔軟に対応できる。
たとえば、updateしてから、printしてって仕様に変わったとしても、
lazy val update = (id: String) => (something: String) => Try {
someTable.update(id)(something)
}
って関数を追加してやって、for式に一行呼び出しを書くんだろうなってすぐに分かる。
def printSomething(id: String): Try[Unit] =
for {
u <- assertNotEmpty(id)
some <- findBy(id)
updatedSome <- update(id)(some + "thing") // <= ココに追加したった。
} yield
println(updatedSome)
処理の流れも見易くて、Gitとかで差分見ても、一目瞭然。
やっぱこの単位でTryにしていくのがベストな気がします。
Try[Seq[Try[String]]]をほぐす
低レイヤーのメソッドでTryにしていくと、 Try[Seq[Try[String]]]
みたいなことになりがち。
lazy val assertNotEmpty: String => Try[Unit] = (value: String) => Try {
assert(value.nonEmpty, "value is empty")
}
lazy val findAll: () => Try[Seq[String]] = () => Try {
someTable.findAll
}
lazy val findBy: String => Try[String] = (id: String) => Try {
someTable.findBy(id)
}
def printSomething(): Try[Seq[Try[String]]] =
for {
ids <- findAll()
} yield for {
id <- ids
} yield for {
u <- assertNotEmpty(id)
some <- findBy(id)
} yield
some
上記のよーな感じ。これをほぐす。
def printSomething(): Try[Seq[String]] =
for {
ids <- findAll()
somes <- Try {
for {
id <- ids
} yield (for {
u <- assertNotEmpty(id)
some <- findBy(id)
} yield some).get
}
} yield somes
みたいな。
これで良い感じ。
Option.getは悪だけどTry.getは良い
Option.getをすると、NullPointerになるから、getとか絶対使うなよ!!って言うけど、だからって、Try.getもだぞ!!ってことにならないと考えてます。
Try {
// something
} match {
case Success(s) => s
case Failure(e) => throw e
}
って、
Try {
// something
}.get
と同じなんだよね。
結構、前者のコードを見る。(まあ、Scala初級者なだけですが…)
けど、そんなん冗長なことせんと、そこは素直にgetでいこーぜって思う。
まとめ
ざっと、眠たい目をこすりながら、思い出したのは、以上。
他に思い出したら追記かなんかします。
以上です。