タイトルではEither
といいながら片方がErrorType
決め打ちなのでFailable
型ということで。
enum Failable<T> {
case Success(T)
case Failure(ErrorType)
init(@autoclosure _ f: () throws -> T) {
do {
self = .Success(try f())
} catch {
self = .Failure(error)
}
}
}
ポイントはFailable.init(_:)
にエラーをthrow
するクロージャを渡しているところ。
使ってみる。
enum MyError : ErrorType {
case ParseIntError(String)
case PatternNotFoundError(String, String)
}
func parseInt(text: String) throws -> Int {
if let num = Int(text) {
return num
} else {
throw MyError.ParseIntError(text)
}
}
// 普通ならこう
do {
try parseInt("5")
} catch {
print(error)
}
// Failableを使うとこうなる
Failable(try parseInt("5")) // .Success(5)
Failable(try parseInt("x")) // .Failure(MyError.ParseIntError("x"))
引数は{}
で囲われてないが@autoclosure
によってクロージャになっている。try
はクロージャからエラーを投げるために必要。do
やcatch
で囲う必要がなく、またtry!
のような危険もなく、簡単に失敗する可能性がある処理を扱える。
ちなみにエラーをthrow
する関数はthrows
も型に含まれる。例えば上に出てくるparseInt
関数の型はString throws -> Int
型になる。
map
とかで途中のエラーが伝わっていくやつ。
extension Failable {
func map<U>(f: T -> U) -> Failable<U> {
switch self {
case .Success(let v): return .Success(f(v))
case .Failure(let e): return .Failure(e)
}
}
func flatMap<U>(f: T -> Failable<U>) -> Failable<U> {
switch self {
case .Success(let v): return f(v)
case .Failure(let e): return .Failure(e)
}
}
}
Failable(try parseInt("5")).map { $0 * $0 } // .Success(25)
Failable(try parseInt("x")).map { $0 * $0 } // .Failure(MyError.ParseIntError("x"))
エラーを投げてくるNSRegularExpression.init(pattern:options:)
やNSString.init(contentsOfFile:encoding:)
を使った例。下のようなテキストファイルをPlaygroundのリソース等に入れておく。
Hello, Swift 2!
func search(pattern: String, filePath: String) -> Failable<String> {
let regex = Failable(try NSRegularExpression(pattern: pattern, options: []))
let text = Failable(try NSString(contentsOfFile: filePath, encoding: NSUTF8StringEncoding))
return regex.flatMap { r in
text.flatMap { t -> Failable<String> in
if let match = r.firstMatchInString(t as String, options: [], range: NSMakeRange(0, t.length))
where match.range.location != NSNotFound {
return .Success(t.substringWithRange(match.range))
} else {
return .Failure(MyError.PatternNotFoundError(pattern, filePath))
}
}
}
}
let path = NSBundle.mainBundle().pathForResource("File", ofType: nil)!
// 正規表現が間違ってる場合
search("[", filePath: path) // .Failure(... The value “[” is invalid. ...
// ファイルパスが間違ってる場合
search("Swift", filePath: "hoge") // .Failure(... there is no such file. ...
// ファイルの文字列にパターンがマッチしなかった場合
search("ObjC", filePath: path) // .Failure(MyError.PatternNotFoundError("ObjC" "..."))
// マッチが成功した場合
search("Swift", filePath: path) // .Success("Swift")
正規表現が間違ってるのか、ファイルが無いのか、正規表現もファイルパスも正しいが文字列が見つからなかったのか、エラーがちゃんと伝わってるのがわかる。
「エラーを投げる関数」を「エラーを投げずにFailable
を返す関数」に変換する高階関数バージョン。
func failable<T, U>(f: T throws -> U) -> T -> Failable<U> {
return {
do {
return .Success(try f($0))
} catch {
return .Failure(error)
}
}
}
failable(parseInt)("5") // .Success(5)
search("[0-9]+", filePath: path)
.flatMap(failable(parseInt)) // .Success(2)
エラーを投げるString throws -> Int
型のparseInt
関数を、エラーを投げないString -> Failable<Int>
型の関数に変換してflatMap
に渡している。
Cocoaが投げてくるエラーを処理するだけならdo-try-catchで十分かもしれない。すでにアプリケーションのコード内でエラー処理にEither
的なものを使っていて、そこにCocoaから来るエラー処理も組み入れたい場合は、こういう方法が使える。