エラーを値として処理する
学ぶこと
nullと例外を使わずにすべてのエラーを処理する方法
すべてのコーナーケースが確実に処理されるようにする方法
エラーとして考えられるものをすべて関数のシグネチャで知らせる方法
さまざまなエラーが起こり得る状況で小さな関数から大きな機能を合成する方法
ユーザーフレンドリで記述的なエラーを返す方法
題材として、テレビ番組解析エンジンのプログラムを作りながら、関数型プログラミングでのエラーの扱いを学ぶ
潜在的なエラーの扱い
下記のコードは、テレビ番組の文字列を受け取り、TvShowモデルに変換する
code:scala
case class TvShow(title: String, start: Int, end: Int)
// rawShowは、TITLE (YEATR_START-YEAR_END)のフォーマットを想定している
def parseShow(rawShow: String): TvShow = {
val bracketOpen = rawShow.indexOf("(")
val bracketClose = rawShow.indexOf(")")
val dash = rawShow.indexOf("-")
val name = rawShow.substring(0, bracketOpen).trim
val yearStart = Integer.parseInt(rawShow.substring(bracketOpen + 1, dash))
val yearEnd = Integer.parseInt(rawShow.substring(dash + 1, bracketClose))
TvShow(name, yearStart, yearEnd)
}
純粋関数は例外をスローしない
対応していないフォーマットの文字列が入ってきた場合、例外が発生する
code:scala
val invalidRawShow = "Breaking Bad, 2008-2013"
parseShow(invalidRawShow)
// Exception in thread "main": Range [0, -1) out of bounds for length
この場合、parseShow関数は純粋関数ではなくなる
try ~ catchで対応し、エラーの場合nullを返して対応した場合
ifとtry ~ catchがコードの至る所に存在することになる
code:java
TvShow = show parseShow(invalidRawShow);
if (show != null) {
// showを使って他の処理を行う
}
Option型を使ってエラーをハンドリングする
Option型を返す関数を組み合わせることで、処理に失敗したら例外をスローするのではなく、Noneが返されるようになる。 つまり、エラーも値として扱う。値を受け取ってOptionを返す純粋関数 code:scala
def extractName(rawShow: String): OptionString = ??? def extractYearStart(rawShow: String): OptionInt = ??? def extractYearEnd(rawShow: String): OptionInt = ??? // 文字列テレビ番組をTvShowに変換する
def parseShow(rawShow: String): OptionTvShow = { for {
name <- extractName(rawShow)
yearStart <- extractYearStart(rawShow)
yearEnd <- extractYearEnd(rawShow)
} yield TvShow(name, yearStart, yearEnd)
}
val invalidRawShow = "Breaking Bad, 2008-2013"
parseShow(invalidRawShow) // None
独立した別々の値を組み合わせて別の値を計算できる仕組み
Noneが返された際の挙動
Option型を扱う関数をflatMapを使って連結した場合
途中でNoneが返されると、flatMapの仕組み上、一番上の関数までバルクアップされ、計算全体の結果として返される
code:scala
def parseShow(rawShow: String): OptionTvShow = { extractName(rawShow).flatMap { name =>
extractYearStart(rawShow).flatMap { yearStart =>
extractYearEnd(rawShow).map { yearEnd =>
TvShow(name, yearStart, yearEnd)
}
}
}
}
// 途中でNoneが返された場合
def parseShow(rawShow: String): OptionTvShow = { Some("Mad Men").flatMap(name =>
None.flatMap(yearStart =>
実行されない
実行されない
)
)
}
例外よりOptionを優先すべき理由
Optionであれば、起こり得るエラーに対して、処理を行うリカバリーコードを記述しやすい点
例外を扱う場合のリカバリーコードは、try ~ catchを使って書くことになるので、コードが膨れ上がり、複雑になる可能性がある。
code:java
public static TvShow parseShow(String: rawShow) throws Exception {
String name = extractName(rawShow);
Integer yearStart = null;
try {
yearStart = extractYearStart(rawShow);
} catch(Exception e) {
yearStart = extractSingleYear(rawShow);
}
Integer yearEnd = null;
try {
yearEnd = extractYearEnd(rawShow);
} catch(Exception e) {
yearEnd = extractSingleYear(rawShow);
}
return new TvShow(name, yearStart, yearEnd);
}
Option型を使う場合のリカバリーコードは、orElseを使って処理できるのでシンプルに対応できる code:scala
def parseShow(rawShow: String): OptionTvShow = { for {
name <- extractName(rawShow)
yearStart <- extractYearStart(rawShow).orElse(extractSingleYear(rawShow))
yearEnd <- extractYearEnd(rawShow).orElse(extractSingleYear(rawShow))
} yield TvShow(name, yearStart, yearEnd)
}
複数のエラーを同時に処理する
Option型をList型に変換する
Some(1) -> List(1)
None -> List()
flatMapとtoList を組み合わせて、Some内の値のみをリストを生成することができる
こうすることで、エラーによって生まれたNoneを取り除いた新たなリストに変換することができる
code:scala
scala> val list = List(Some(1), None, Some(2))
val list: List[OptionInt] = List(Some(1), None, Some(2)) scala> list.flatMap(_.toList)
val res0: ListInt = List(1, 2) 2種類のエラー処理戦略
ベストエフォート戦略
エラーが発生しても処理を止めず、実行可能な部分だけでも完了させようとする方針
下記のコードの場合、途中でエラーが発生しNoneが入ってきても最後まで処理を継続
code:scala
.map(parseShow) // List[OptionTvShow] .map(_.toList) // List[ListTvShow] }
オール・オア・ナッシング戦略
処理の一部でエラーが発生した場合、すべてを取り消して「失敗」とする方針
下記のコードの場合、Listの中にNoneが存在していたら、処理を停止しNoneを返す
code:scala
def addOrResign(
parsedShows: Option[ListTvShow], for {
shows <- parsedShows
parsedShow <- newParsedShow
} yield shows.appended(parsedShow)
}
def parseShows(rawShows: ListString): Option[ListTvShow] = { rawShows // List("Breaking Bad (2008-2013)", Chernobyl ())
.map(parseShow) // List(Some(TvShow("Breaking Bad", 2008, 2013)), None)
.foldLeft(init)(addOrResign) // None
}
Optionの問題点
Optionを使うことで、エラーの処理とエラーからのリカバリーができるが、エラーが起きてもNoneしか返されないため、エラーの詳細を知ることができない
Either
Eitherを使うことでエラー発生時の詳細を扱うことができる Eitherを使ってparseShows関数をリファクタする