LoginSignup
37

More than 5 years have passed since last update.

Swift 2でエラーをthrowする関数からEitherを作る

Posted at

タイトルではEitherといいながら片方がErrorType決め打ちなのでFailable型ということで。

Failable.swift
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するクロージャを渡しているところ。

使ってみる。

Failable.swift
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はクロージャからエラーを投げるために必要。docatchで囲う必要がなく、またtry!のような危険もなく、簡単に失敗する可能性がある処理を扱える。

ちなみにエラーをthrowする関数はthrowsも型に含まれる。例えば上に出てくるparseInt関数の型はString throws -> Int型になる。

mapとかで途中のエラーが伝わっていくやつ。

Failable.swift
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のリソース等に入れておく。

File
Hello, Swift 2!
Failable.swift
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を返す関数」に変換する高階関数バージョン。

Failable.swift
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から来るエラー処理も組み入れたい場合は、こういう方法が使える。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37