Swift 2.0 が今週にもリリースされそうというタイミングで気が早いですが、 Swift 3.0 (になるかわからないけど 2.0 の次の Swift )で追加されそうな Either
について説明し、どんなものになるか考えてみます。
次期 Swift で Either
が追加されそうだというのは次のツイート12からです。
@NachoSoto @jspahrsummers Didn't make it for WWDC, but we plan on having standard funcs for wrapping and releasing errors into an Either.
このツイートをした Joe Groff さんは Apple の Swift チームのメンバーです。
本投稿では、 Either
とは何か、 Swift に Either
が追加されるとしたらどういうものになるか、どういう形が望ましいか考えてみたことを書きます。(英語でも簡単にまとめました。)
Eitherとは
まず初めに、 Either
とは何かを簡単に説明します。
Either
とはその名の通り「どちらか」を表す型です。例えば、 Either<Int, String>
は Int
か String
の「どちらか」を表す型となります。
そんなものが何の役に立つのかと思うかもしれません。 一番わかりやすい用途は、ある関数(やメソッド)が成功したら結果を、失敗したらエラー情報を返すような場合です。 そのような場合には、 Either
が結果かエラー情報かの「どちらか」を表す値となります。
実装
Either
の実装は色々なバリエーションが考えられますが、概ね次のような感じになります。
enum Either<T, U> {
case Left(T)
case Right(U)
}
サードパーティの実装としては、 robrix/Either や Swiftz および Swiftx の Either 、 @gfx さんによる国産の gfx/Swift-SwiftEither などがあります。
使い方
例として、得点が文字列で書かれていて、それをパースして整数に変換するケースを考えてみましょう(テキストフィールドにユーザーが入力した場合や、 CSV ファイルや XML ファイルに書かれている場合など、よくあるシチュエーションです)。得点は、最低点を 0
点、最高点を 100
点とします。
String
で与えられた得点を Int
の得点に変換する関数 toScore
を実装します。ただし、変換に成功した場合には Int
を、失敗した場合にはエラー情報を返すようにしたいです。 "abc"
のような文字列が与えられた場合や、たとえ整数としてパースできても 0
から 100
の範囲外であればエラーとします。
Stringでエラーメッセージを表す場合
ます、シンプルに失敗した場合は String
でエラーメッセージを返すことにしてみましょう。エラーメッセージは、整数としてパースできたけど範囲外だった場合と、そもそも整数としてパースできなかった場合に分けるものとします。
なお、 Either
をエラー処理に使う場合、 Left
でエラーを、 Right
で値を表すことが多いので、 toScore
の戻り値の型は Either<String, Int>
となります。以下、本投稿ではすべてエラーを前に書くスタイルで統一しますが、 Swift に追加される Either
がそうなるかはわかりません。
func toScore(string: String) -> Either<String, Int> {
switch Int(string) {
case .Some(let score) where 0...100 ~= score: // 成功
return .Right(score)
case .Some(let score): // 失敗:整数としてパースできたが範囲外の場合
return .Left("Out of range: \(score)")
case .None: // 失敗:整数としてパースできなかった場合
return .Left("Failed to parse as an integer: \"\(string)\"")
}
}
上記のコードの簡単な説明は下記です。
-
Int(string)
でString
をInt
に変換 -
Int
のinit(_ text: String)
は Failable Initializer なのでパースに失敗するとnil
を返す(戻り値はInt
ではなくInt?
) -
switch
文のパターンマッチで場合分けして値を返す
これを使うコードは次のようになります。
let result: Either<String, Int> = toScore("42")
switch result {
case .Left(let errorMessage):
// エラー処理
print(errorMessage)
case .Right(let score):
// score を使った処理
...
}
ErrorTypeでエラーを表す場合
エラーメッセージを直接 Either
で扱うのは良いアイデアではありません。
例えば、エラーメッセージを多言語化する場合を考えて下さい。 toScore
の中にすべてのエラーメッセージを実装しようとすると toScore
がどんどん肥大化してしまいます。 toScore
のロジックを実装する人と、エラーメッセージを実装する人は別の人かもしれません。役割を明確化し、 toScore
はエラーメッセージそのものを返すよりもエラーの原因を特定できるだけの情報を返すに留めておいた方が良いでしょう。エラーメッセージを実装する人は、その情報を使って実装すれば良いのです。
せっかく Swift 2.0 で ErrorType
が追加されたので、ここではエラー情報を ErrorType
として実装してみましょう。
まず、 toScore
が整数のパースには成功したものの、範囲外になってしまった場合を考えます。この場合は、範囲外になってしまったことに加えて、一体その整数が何だったのかをエラー情報として保持したいです。
次に、そもそも与えられた文字列を整数としてパースできなかった場合には、パースに失敗したことに加えて、その文字列が何だったのかをエラー情報として保持したいです。
これをまとめると、次のような実装になります。
enum ScoreFormatError: ErrorType {
case OutOfRange(Int) // 整数としてパースできたが範囲外の場合
case NotInteger(String) // 整数としてパースできなかった場合
}
これを使うと toScore
は次のようになります。
func toScore(string: String) -> Either<ScoreFormatError, Int> {
switch Int(string) {
case .Some(let score) where 0...100 ~= score: // 成功
return .Right(score)
case .Some(let score): // 失敗:整数としてパースできたが範囲外の場合
return .Left(.OutOfRange(score))
case .None: // 失敗:整数としてパースできなかった場合
return .Left(.NotInteger(string))
}
}
toScore
を使うコードは次のようになります。
let result: Either<ScoreFormatError, Int> = toScore("42")
switch result {
case .Left(.OutOfRange(let score)):
// このように score の値でエラーメッセージを出し分けるのも簡単
if score < 0 {
print("値が小さすぎます: \(score)")
} else {
print("値が大きすぎます: \(score)")
}
case .Left(.NotInteger(let string)):
print("整数ではありません: \(string)")
case .Right(let score):
// score を使った処理
...
}
Optionalとの比較
Either
と似た型として Optional
があります。
Optional
はこれまで、 Swift の標準ライブラリで成功か失敗かを表す型として使われてきました。例えば、 Failable Initializer の戻り値は Optional
で、失敗したときに nil
を返します。
let number: Int? = Int("42") // パースに失敗したら nil
Either
を成功か失敗かを表す型として使う場合、 Optional
の違いは エラー情報を含むことができるかどうか です。
例えば、 SwiftyJSON は Optional
を多用していますが、次のようなケースで nil
が返って来たときにその原因を知ることはできませんでした。
let json: JSON = JSON(data: data)
let bar: Int? = json["foo"]["bar"].int
bar
が nil
だった場合、次のような理由が考えられます。
- JSON に
"foo"
という要素がなかった -
"foo"
はあったけど"bar"
がなかった -
"bar"
もあったがInt
ではなかった - そもそも JSON のパースに失敗した
しかし、 nil
から上記のいずれであるかを知ることはできません。失敗の原因がわかればデバッグがはかどりますし、原因によって処理を変えることもできます(ユーザーに提示するメッセージを出し分けるとか、 "bar"
がなかった場合だけデフォルト値に置き換えるとか)。
String
を Int
に変換するというように失敗の原因が明らかなときは Optional
で十分ですが、様々な原因が考えられる場合には Either
であってほしいところです。
モナドとしてのEither
Optional
同様、 Either
も モナド として実装されることが多いです。つまり、 map
や flatMap
を持つということです。
Either
が モナド であることで Optional
同様 map
や flatMap
を使って便利に取り回すことができます。
let score1: Either<ScoreFormatError, Int> = toScore("42")
let score2: Either<ScoreFormatError, Int> = toScore("97")
// Either に包んだまま score1 の得点の二乗を計算
let square: Either<ScoreFormatError, Int> = score1.map { $0 * $0 }
// Either に包んだまま score1 と score2 の平均を計算
let mean: Either<ScoreFormatError, Int> = score1.flatMap { s1 in score2.flatMap { s2 in .Right((s1 + s2) / 2) } }
Either
の map
や flatMap
は次のように実装できます3。あと、 モナド の条件である、値を包むだけの イニシャライザ も併せて実装します。
extension Either {
init(_ value: U) {
self = .Right(value)
}
func map<V>(transform: U -> V) -> Either<T, V> {
return flatMap { .Right(transform($0)) }
}
func flatMap<V>(transform: U -> Either<T, V>) -> Either<T, V> {
switch self {
case .Left(let left):
return .Left(left)
case .Right(let right):
return transform(right)
}
}
}
do-try-catchとの比較
Eitherとdo-try-catchの類似性
Either
ができることは do-try-catch とほぼ同等です。
例として、 toScore
を do-try-catch で実装してみましょう。
func toScore(string: String) throws -> Int {
switch Int(string) {
case .Some(let score) where 0...100 ~= score: // 成功
return score
case .Some(let score): // 失敗:整数としてパースできたが範囲外の場合
throw ScoreFormatError.OutOfRange(score)
case .None: // 失敗:整数としてパースできなかった場合
throw ScoreFormatError.NotInteger(string)
}
}
do {
let score = try toScore("42")
// score を使った処理
...
} catch ScoreFormatError.OutOfRange(let score) {
if score < 0 {
print("値が小さすぎます: \(score)")
} else {
print("値が大きすぎます: \(score)")
}
} catch ScoreFormatError.NotInteger(let string) {
print("整数ではありません: \(string)")
}
Either
版のコードと同じことができているのがわかると思います。
現状のSwiftにおけるdo-try-catchの問題
ただし、将来的には解消されると思いますが(というかされてほしい)、 Xcode 7 beta 6 の時点では、トップレベルスコープ以外で上記のコードを実行すると、 catch
が網羅的でないとして次のようなコンパイルエラーになります。
Errors thrown from here are not handled because the enclosing catch is not exhaustive
Java と違って Swift の throws
句にはエラーの型を記述することができないので、関数やメソッドが throw
し得るエラーの型を特定することができません。初めは推論されるのかと思っていましたが、どうも今のところそういうわけではなさそうです。もしこのままの仕様であれば次のようなコードとなり、タイプセーフを重視している Swift が台無しなのでどうにかしてほしいところです。個人的には、面倒でも throws
句に型が記述できるようになるのがいいと思います(推論はあってもいいけど、実装を変更しただけでシグネチャが勝手に変わるのは微妙)。
do {
let score = try toScore("42")
// score を使った処理
...
} catch ScoreFormatError.OutOfRange(let score) {
...
} catch ScoreFormatError.NotInteger(let string) {
...
} catch {
// ここには絶対に到達しないが毎回これを書かないといけない
fatalError("Never reaches here.")
}
最後の catch
節は本来不要なのにコンパイルを通すために必須というのは面倒ですし、誤ってその他のエラーを throw
してしまうとクラッシュしてしまいます。これまでのタイプセーフな Swift を踏襲して、コンパイル時に型の制約でそのような事態を防げることが望まれます。
Eitherとdo-try-catchとの使い分け
話がそれましたが、 Either
と do-try-catch で表現できることが変わらなくても、 Either
で失敗を扱うことで次のような利点があります。
- 失敗を値として取り回せるので、その場で処理をする必要がない
-
map
やflatMap
を使ったり、後述の便利な構文を使うことができる(!
一つでエラーを無視するとか) - 関数型のスタイルと相性がいい
一方で、 Either
にはできないこともあります。それは、 副作用 を持つ(戻り値を返すだけでなく状態を書き換える)関数やメソッドにエラー処理を強制することができないことです。
func setScore(score: Int) throws {
switch score {
case 0...100:
self.score = score // 副作用
default:
throw OutOfRangeError(range: 0...100, value: score)
}
}
上記の setScore
を使う場合にはエラー処理を強制できますが、 Either
ではそれができません。無理やり Either
で書くなら
func setScore(score: Int) -> Either<OutOfRangeError, Void>
となりますが、これなら Optional
の方がスマートです。
func setScore(score: Int) -> OutOfRangeError?
いずれにせよ、戻り値を無視されれば(もしくは忘れていたら)エラーが無視されてしまいます。
ちなみに Either
は元々関数型言語で使われていたものですが、純粋関数型言語ではそもそも副作用が存在しないのでそのような問題も存在しません。
以上をまとめると、次のような指針で Either
と do-try-catch を使い分けるのが良いでしょう。
- 副作用がない関数やメソッドでは
Either
を使う - 副作用がある関数やメソッドでは do-try-catch を使う
do-try-catchのEitherへの変換
このように、 do-try-catch ではなく Either
でエラー処理をしたいニーズがあるため、冒頭の Groff さんのツイートにつながります。 Groff さんが言っているのは、 throws
付きの関数(やメソッド)をコールするときに、それを do-try-catch で扱うのではなく、代わりに Either
でラップして受け取れるようにする関数を標準ライブラリに追加するという話だと思います。
仮にこの関数を either
関数 と呼ぶことにしましょう。 either
関数は次のように実装できます。
func either<T, U>(@autoclosure f: () throws -> U) -> Either<T, U> {
do {
return .Right(try f())
} catch let error as T {
return .Left(error)
}
}
この関数のキモは @autoclosure
です。 @autoclosure
によって、この関数は次のように try
演算子を使った式を囲んで使うことができます。
let result: Either<ScoreFormatError, Int> = either(try toScore("42"))
なお、前述の throws
の型が特定されない問題があるので実際には次のように書かなければコンパイルエラーになってしまいます。
func either<T, U>(@autoclosure f: () throws -> U) -> Either<T, U> {
do {
return .Right(try f())
} catch let error as T {
return .Left(error)
} catch {
fatalError("Never reaches here.")
}
}
どんな仕様が採用されるか
一般的には前節で見たようなものを Either
と言いますが、 Swift の Either
がそのような仕様になるとは限りません。例えば、 Swift では一般的に ラムダ式 や 匿名関数 などと呼ばれるものを クロージャ と呼んでおり、他の言語における クロージャ と意味合いが違います。
そのように、同じ名前のものが言語間で少しずつ異なるということはよくあることです。前節で挙げた Either
の実装の他に、 Swift の Either
として採用されそうなものを挙げてみます。
Result
現在 Swift で Either
的なものとしてデファクトになっているのが antitypical/Result です。
前節で Either
の一番わかりやすい用途は、ある関数(やメソッド)が成功したら結果を、失敗したらエラー情報を返すような場合だと書きましたが、それならはじめから片方はエラー情報にしてしまうおうと考えたのが Result
です。 Result
は成功か失敗かに特化した Either
と考えることができます。
Result
の実装をシンプルにすると次のような感じです。
enum Result<T, Error: ErrorType> {
case Success(T)
case Failure(Error)
}
これを使って toScore
を実装すると次のようになります。 Left
が Failure
に、 Right
が Success
に変わっただけです。成功か失敗かのために Either
を使う分には Result
と変わりません。
func toScore(string: String) -> Result<Int, ScoreFormatError> {
switch Int(string) {
case .Some(let score) where 0...100 ~= score: // 成功
return .Success(score)
case .Some(let score): // 失敗:整数としてパースできたが範囲外の場合
return .Failure(.OutOfRange(score))
case .None: // 失敗:整数としてパースできなかった場合
return .Failure(.NotInteger(string))
}
}
この toScore
を使うコードも Either
のときとほとんど変わらないので省略します。
Groff さんのツイートでは Either
についてエラー処理の文脈で言及しているので、 Swift の Either
は Result
のようにエラー処理専用の型となる可能性も考えられます。
また、 Swift が大きな影響を受けた言語 Rust の標準ライブラリには Result があり、 Swift でも Either
ではなく Result
という名前で追加される可能性もあると思います。
タプル & Optional
Either
代わりのお手軽な代替手段として用いられることが多いのがタプルと Optional
の組み合わせです。 Either<T, U>
を (T?, U?)
または (left: T?, right: U?)
として表すのです。 T
が値を持つときは U
を nil
に、 U
が値を持つときは T
を nil
にすることで Either
のようなことができます。 Go 言語ではよくこれと似たような方法が採られます。
この方法だと toScore
は次のように実装できます。
func toScore(string: String) -> (ScoreFormatError?, Int?) {
switch Int(string) {
case .Some(let score) where 0...100 ~= score: // 成功
return (nil, score)
case .Some(let score): // 失敗:整数としてパースできたが範囲外の場合
return (.OutOfRange(score), nil)
case .None: // 失敗:整数としてパースできなかった場合
return (.NotInteger(string), nil)
}
}
これなら 新しく Swift の標準ライブラリに Either
を追加しなくても、戻り値として (T?, U?)
を持った either
関数を追加するだけで済ませられます。
しかし、僕は この方法は望ましくない と思います。この方法には致命的な欠陥があります。それは、 (T?, U?)
は型として T
か U
のどちらかだけが値を持つことを保証しない ということです。 T
も U
も値を持つかもしれませんし、両方 nil
かもしれません。関数の仕様として、必ずどちらかが値を持ちどちらかは nil
と決めてしまうことはできても、それはタイプセーフな Swift の考え方とはマッチしません。
例えば、上記の toScore
を使おうとすると次のようなコードを書かなければなりません。
let result: (ScoreFormatError?, Int?) = toScore("42")
switch result {
case (let error, nil):
// エラー処理
...
case (nil, let score):
// score を使った処理
...
default:
// ここには絶対に到達しないが毎回これを書かないといけない
fatalError("Never reaches here.")
}
最後の default
節は不要なのにコンパイルを通すために必須ですし、万が一どちらも値が入ってしまったりどちらも nil
になった場合はクラッシュしてしまいます。コンパイル時に型の制約でそのような事態を防げる Either
や Result
の方が優れていると言えるでしょう。
Failable
Swift の Either
は Result
をさらにシンプルにし、 @kanotomo さんの "Swift 2でエラーをthrowする関数からEitherを作る" の中で述べられている Failable
のような実装になるかもしれません。
enum Failable<T> {
case Success(T)
case Failure(ErrorType)
}
この Failable
は Failure
が取れる値を汎用的な ErrorType
とすることで、 Result
から第二の型パラメータを消したものです。型パラメータが一つになるので見た目にはすっきりします。
func toScore(string: String) -> Failable<Int> {
... // 実装は同じなので省略
}
しかし、エラーの型情報が消えてしまっているため、利用時には面倒ですしタイプセーフではありません。"タプル & Optional" の項で述べたのと同じ状況です。
let result: Failable<Int> = toScore("42")
switch result {
case .Failure(ScoreFormatError.OutOfRange(let score)):
print("値が範囲外です: \(score)")
case .Failure(ScoreFormatError.NotInteger(let string)):
print("整数ではありません: \(string)")
case .Success(let score):
// score を使った処理
...
default:
// ここには絶対に到達しないが毎回これを書かないといけない
fatalError("Never reaches here.")
}
標準ライブラリに Failable
相当の Either
を追加しなければならず、しかもタイプセーフでもないのであれば、このような Either
にする理由がないように見えます。
しかし、このような仕様になるかもしれないと僕が思うのは、 "現状のSwiftにおけるdo-try-catchの問題" で述べたように、現状では throws
がついた関数(やメソッド)はすべて汎用的な ErrorType
を throw
することになっているようだからです。
関数が throw
するものが汎用的な ErrorType
なら、 either
関数が返す Either
のエラーの型を決定することができません。もしこのままの仕様が続くようであれば、 either
関数の戻り値として採用される Either
が Failable
相当のものになる可能性もあるでしょう。
そのようなことになるのは望ましくない です。 throw
されるエラーの型が推論されるようにするか、 throws
句で型を記述できる仕様にしてほしいところです。
なお、本項で述べた Failable
のようなものを一般的に Failable
と呼ぶかはわかりません。僕も "Swift 2.0 の try, catch ファーストインプレッション" の中で Failable
を作っていますがこれは Result
相当のものでしたし、 Failable
と名付けたのは Optional
と語感や形容詞であることをそろえたかったのと、 Failable Initializer という用語から考えても適切な名前だろうと考えたからです。 @kanotomo さんがどういう意図で Failable
と名付けたのかや、一般的に Failable
と呼ばれるものがあるのかはわかりません。
Union Type (直和型)
Swift 以外の言語に目を向ければ、 Either
と似たものに Union Type があります。
Union Type とは多くの場合 T|U
のように書かれる型で、 T
か U
のどちらかを表します。そう書くとまさに Either
と同じように思えますが次のような違いがあります。
まず、 Union Type では T
や U
が T|U
の派生型として扱われるため、 T
や U
のインスタンスを直接 T|U
型の変数に代入することができます。
let union: Int|String = 42 // OK
// let either: Either<Int, String> = 42 // コンパイルエラー
let either: Either<Int, String> = .Left(42) // Either で包む必要あり
一方で、その裏返しとして Int|Int
は Int
と同じものになってしまい Either<Int, Int>
でなら表せるものが表せません4。
let union: Int|Int = 42 // 左と右の Int を区別できない
let either1: Either<Int, Int> = .Left(42) // 左の Int
let either2: Either<Int, Int> = .Right(42) // 右の Int
Swift には Union Type はありませんが、 TypeScript や Ceylon 、 Flow で採用されるなど最近のプログラミング言語のトレンドなので、次期 Swift で追加されるかもしれません。 その場合、 either
関数の戻り値の型は Union Type T|U
で表される可能性が高いでしょう。
Uniton Type を使えば、 toScore
は次のように書けます。戻り値を Either
で包む必要がないのでとてもシンプルです。
func toScore(string: String) -> ScoreFormatError|Int {
switch Int(string) {
case .Some(let score) where 0...100 ~= score: // 成功
return score
case .Some(let score): // 失敗:整数としてパースできたが範囲外の場合
return .OutOfRange(score)
case .None: // 失敗:整数としてパースできなかった場合
return .NotInteger(string)
}
}
使うときは次のようになります。こちらもシンプルです。
let result: ScoreFormatError|Int = toScore("42")
switch result {
case .OutOfRange(let score):
print("値が範囲外です: \(score)")
case .NotInteger(let string):
print("整数ではありません: \(string)")
case let score as Int:
// score を使った処理
...
}
これはタイプセーフですし、シンプルという点では素晴らしいです。ただ、個人的には Swift の文化とはなじまない ように思います。 Optional
との整合性なども考えると、やはり enum
で Either
を実装する方が Swift らしく、一貫性があるでしょう。
ただし、次節で述べるように、 Either
を enum
で実装しながらも Union Type っぽく振る舞わせるのはありだと思います。
Either の言語レベルでの特別なサポート
ただ Either
を導入するだけならライブラリを入れなくていいというだけで、使い勝手は大きくは変わりません。標準ライブラリでサポートするからには Optional
のように便利な手段を色々と提供してほしいものです。
型の記述のシンタックスシュガー
正直、 Either<Foo, Bar>
などと記述するのは面倒です。 Optional<Foo>
を Foo?
、 Array<Foo>
を [Foo]
、 Dictionary<Foo, Bar>
を [Foo: Bar]
と記述できるのと同じように、 Either
にも型を記述する際の便利なシンタックスシュガーがほしいです。
その候補として、僕は Union Type の項で見たような Foo|Bar
という記法で書けるのがいいんじゃないかと思います。シンプルでわかりやすく書きやすいですし、意味的にも Union Type との共通点があります。他の言語で Union Type が記述できてることからも、コンパイラがコードをパースする上での問題も少なそうです。
これは、 Either
を Union Type で表そうということではなくて、 Optional
の実体が Optional<T>
という enum
だけど T?
の形で書けるように、 Either
も実体は Either<T, U>
という enum
だけど T|U
の形で書けるようにしようということです。
なお、この場合 Foo|Bar|Baz
が Either<Either<Foo, Bar>, Baz>
となるか Either<Foo, Either<Bar, Baz>>
となるか(つまり、 |
が左結合か右結合か)は検討の余地があります。 ErrorA|ErrorB|Int
のような使い方が多いのであれば前者の方が便利ですし、 Error|Int|String
のような使い方が多いのであれば後者の方が便利です( map
, flatMap
する場合や、後述の Either Chaining, Forced Unwrapping, ??
演算子などを使う場合を考えて)。 Either
のエラーを後ろに書くスタイルの場合、議論がすべて反転するので気を付けて下さい。いずれにせよ (Foo|Bar)|Baz
のように結合の優先順位を指定できる必要があるでしょう。
(追記: 2015-12-08 ) Swift がオープンソース化されたので、試しに Int|String
のような構文が使えるようにコンパイラを改造してみました。
暗黙の型変換
Optional
は enum
でありながら次のような代入が可能です。
let a: Int? = 42
これは本来であれば次のように書かなければならないはずです。
let a: Int? = .Some(42)
Int
と Int?
は別の型であり、本来的には直接代入することはできません。これができるということは、 Int
から Int?
への 暗黙の型変換 が行われたということです。シンタックスシュガーを使わず次のように書けばその違和感に気付くと思います。
let a: Optional<Int> = 42 // Optional<Int> に Int を代入
暗黙の型変換は様々な問題を引き起こしやすく、 Swift では暗黙の型変換は厳しく取り締まられています。初期の Swift から Swift 1.0 になる過程で暗黙の型変換を定義する文法は削除されましたし、 Float
から Double
など、他の多くの言語で認められているようなものでも暗黙の型変換が認められていません。さらに、 Swift 1.2 では String
と NSString
などの間の暗黙の型変換も禁じられました。
そのような中で Optional
の例外扱いは特別なものです。さすがに Optional
で暗黙の型変換を禁止するとコードを書くのが面倒すぎるということなのでしょう。
Either
は Optional
の兄弟のようなものなので、 Either
においても、暗黙の型変換が適用されれば使い勝手が良くなるのではないかと思います(ただ、前述のように暗黙の型変換は問題を起こしやすいので、本当にそれがいいかはわかりません)。
let a: Either<Int, String> = 42 // 明示的に Either に包む必要がない
これと Foo|Bar
のようなシンタックスシュガーが組み合わされば、 Either
は enum
でありながら相当 Union Type っぽく振る舞えることになります。
let a: Int|String = 42 // Union Type っぽい
Subtyping
Optional
にはさらに驚くべき特別扱いがあります。なんと、 Int
は Int?
の派生型として扱われるのです。これはどのようにすれば確かめられるでしょう?
次のように書いても、前項で書いたように暗黙の型変換だと考えることができます。
let a: Int? = 42
しかし、次のコードがコンパイルできることは暗黙の型変換で説明することはできません。
class A {
func foo() -> Int? {
return nil
}
}
class B: A {
// override して戻り値の型を Int に変更
override func foo() -> Int {
return 42
}
}
foo
の戻り値の型に注目して下さい。元々 Int?
だったのが、オーバーライドされたときに Int
に変更されています。
一般的に、オブジェクト指向言語では継承してメソッドをオーバーライドするときに時に戻り値の型を狭める(派生型にする)ことができます。これは、 Animal
を返すメソッドをオーバーライドして Cat
を返すようにしても、 Cat
は Animal
なのでスーパークラスとしてインスタンスを扱っているときにも常に Animal
を返していることになり何も困らないからです。
class A {
func bar() -> Animal {
return Animal()
}
}
class B: A {
// override して戻り値の型を Cat に変更
override func bar() -> Cat {
return Cat()
}
}
let b: B = B()
let a: A = b
// b の bar() が呼ばれ Cat が返るが Animal として振る舞える
let animal: Animal = a.bar()
前述の foo
のケースも、 Int?
を Animal
に、 Int
を Cat
に読みかえれば全く同じ関係になっているのがわかると思います。つまり、 Swift では Int
は Int?
の派生型なのです。
Optional
は次のような enum
です。
enum Optional<T> {
case None
case Some(T)
}
しかし、同じような enum
を作っても Optional
のような派生関係は生まれませんし、それを実現する構文も今のところありません。 Int
が Int?
になるのは、 Optional
だけに与えられた特別な仕様なのです。
これは、前項で挙げた暗黙の型変換が Optional
だけに許されている理由の説明にもなります。つまり、 暗黙の型変換が行われているのではなく、型に派生関係があるので代入できているだけと解釈することができるのです。
同じことを Either
にも許すと、次のようなことができるようになります(前述の Foo|Bar
スタイルのシンタックスシュガーを使って書きます)。
class A {
func foo() -> Int|String {
return "abc"
}
}
class B: A {
// override して戻り値の型を Int に変更
override func foo() -> Int {
return 42
}
}
ここまで来ると Union Type にしか見えません。
Either Chaining
Optional
には Optional Chaining という便利な構文があります。 Swift では Optional
を使ってタイプセーフなコードが書ける代わりにすぐに Optional
だらけになってしまうので、このような便利な構文は重宝します。
let foo: Foo? = ...
let baz: Baz? = foo?.bar?.baz // foo か bar か baz のどれかが nil なら nil
Either
でも同じことが言えます。次のように Optional Chaining のような構文があれば便利です。
let foo: Error|Foo = ...
let baz: Error|Baz = foo?.bar?.baz // foo か bar か baz のどれかが Error なら Error
Forced Unwrapping
エラーを無視するために Forced Unwrapping も欲しい所です。
let foo: Foo? = ...
let qux: Qux = foo!.qux // foo が nil でないことを確信、 nil ならクラッシュ
let foo: Error|Foo = ...
let qux: Qux = foo!.qux // foo が nil でないことを確信、 nil ならクラッシュ
Either Binding
Optional Binding も Optional
だけに許されている便利構文です。
let fooOrNil: Foo? = ...
if let foo = fooOrNil {
// foo を使った処理
...
}
これも Either
でできれば便利です。
let fooOrError: Error|Foo = ...
if let foo = fooOrError {
// foo を使った処理
...
}
?? 演算子
Optional
には ??
という便利な演算子があります。
let fooOrNil: Foo? = ...
let foo: Foo = fooOrNil ?? Foo() // nil のときは Foo()
Either
だと次のようになります。
let fooOrError: Error|Foo = ...
let foo: Foo = fooOrNil ?? Foo() // Error のときは Foo()
なお、 ??
はただの演算子なので自分で実装することもできます。そのため、 gfx/Swift-SwiftEither や antitypical/Result では実装されています。
init|
Swift の イニシャライザ は、インスタンスの構築に失敗したときに nil
を返せるように Failable Initilizer という仕組みがあります。次の init?
は Int
の イニシャライザ ですが、その戻り値の型は Int
ではなく Int?
です。
struct Int {
// Int としてのパースに失敗したら nil を return する
init?(_ text:String) {
...
}
}
イニシャライザ の失敗を考えるなら、当然エラー情報を返したいこともあるでしょう。現状では イニシャライザ の戻り値の型はその型そのものか Optional
だけですが、 Either
も返す手段ができればうれしいです。そのときには init?
と一貫性があるように init|
というキーワードを使うのがいいんじゃないでしょうか。
init|(foo: Foo, bar: Bar) {
// 失敗した場合はエラー情報を Either に渡して return する
...
}
ただ、 Either
のエラーの型を推論させるのでなければ、何かしらの方法で指定する方法が必要になります。
try| 演算子
try?
演算子を使えば、 do-try-catch を使わなくても簡単に throws
付の関数を扱うことができます。これは、 ErrorType
の詳細はどうでも良くて、ただ失敗したことを知りたいだけの場合には重宝します。
例えば、前述の
func toScore(string: String) throws -> Int
の場合には
do {
let score = try toScore("42")
// score を使った処理
...
} catch {
// エラー処理
...
}
となるところを、
if let score = try? toScore("42") {
// score を使った処理
...
} else {
// エラー処理
...
}
というように書けます。これだけだと大した違いが内容に思えますが、
let scoreOrNil: Int? = try? toScore("42")
として、その場でエラー処理をせずに値を取り回したり、式の途中で使って Optional Chaining するなどできます。
同じことは Either
にも言えます。そもそも Groff さんが述べているような either
関数が欲しいのは、 "Eitherとdo-try-catchの使い分け" でも述べたように do-try-catch ではなく Either
としてエラー処理がしたいからです。 Either
であれば、値として取り回せる利便性を享受しながら、 Optional
のようにエラー情報を捨ててしまうこともなく理想的です。
either
関数でもいいのですが、せっかくだったら try?
演算子と一貫性を持たせて try|
演算子とするのが望ましいと思います。
let scoreOrError: ScoreFormatError|Int = try| toScore("42")
まとめ
Swift に Either
が追加されそうなので、それがどのようなものになるか、どのようなものであれば望ましいかを考えてみました。
Swift の Optional
が強力なのは、タイプセーフであることに加えて、その反面の面倒さを軽減するための様々な言語サポートが提供されているからです。 Either
にも同様の言語サポートを提供することで使い勝手の良いものとなるでしょう。そして、それらは自然で( Foo|Bar
など)、これまでのものと一貫性のあるもの( init|
や try|
など)であってほしいです。
-
Joe Groff: https://twitter.com/jckarter/status/608002431578865664 ↩
-
ちなみに、このツイートを見つけたきっかけは Argo の issue を眺めていたことです。 ↩
-
本当は
@noescape
があった方がいいですが、本題とは関係ないので省略します。あと、map
の実装で.Right(transform($0))
となっている部分を本当はEither(transform($0))
としたかったのですが、コンパイラがクラッシュしてしまったので諦めました。 ↩ -
これと同種の話として、 Swift の
Optional
はInt??
でOptional<Optional<Int>>
という二重のOptional
を表せますが、 Kotlin の Nullable Type ではInt??
はInt?
と同じになってしまうという話があります。 ↩