遠くから大きな画像を見ることができれば、近くの本質を理解できます。 HaskellとScalaを実験するとき、Swiftを使用してプログラミングするとき、私には遠い、そして率直に言って不思議な概念が、広範囲の問題に対する目を見張るほど明白な解決策になりました。
ここでエラー処理を行ってください。 具体例は、2つの数値の除算です。除数がゼロの場合、例外がスローされます。 Objective-Cでは、次のような問題を解決します。
NSError *err = nil; CGFloat result = [NMArithmetic divide:2.5 by:3.0 error:&err]; if (err) { NSLog(@"%@", err) } else { [NMArithmetic doSomethingWithResult:result] }
時間が経つにつれて、これはコードを記述する最も馴染みのある方法のように見え始めました。 どんな種類の波線を書かなければならないか、そしてそれらがどのように間接的にプログラムから本当に欲しいものに関連しているかに気付きません:
意味を教えてください。 うまくいかない場合は、エラーを処理できるようにお知らせください。私はパラメーターを渡し、ポインターを逆参照し、いずれの場合も値を返し、場合によっては無視します。 これは、次の理由により組織化されていないコードです。
- 私は機械語を話します-ポインター、逆参照。
- 私自身、メソッドにエラーを通知する方法を提供する必要があります。
- このメソッドは、エラーが発生した場合でも特定の結果を返します。
これらの各点は潜在的なバグの原因であり、Swiftはこれらすべての問題を独自の方法で解決します。 たとえば、Swiftの最初のポイントは、フードの下にあるポインターですべての作業を隠すため、まったく存在しません。 残りの2つのポイントは、転送を使用して解決されます。
計算中にエラーが発生する可能性がある場合、次の2つの結果があります。
- 成功-戻り値あり
- 失敗-できればエラーの原因の説明付き
これらのオプションは相互に排他的です。この例では、0で除算するとエラーが発生し、他のすべては結果を返します。 Swiftは、「
列挙 」を使用して相互排除を表現し
ます 。 エラーの可能性がある計算結果の説明を次に示します。
enum Result<T> { case Success(T) case Failure(String) }
このタイプのインスタンスは、値を持つ
Success
ラベル、または理由を説明するメッセージを持つ
Failure
いずれかです。 各ケースキーワードはコンストラクターによって記述されます。最初のキーワードは
T
インスタンス(結果の値)を受け取り、2番目の
String
はエラーテキストを受け取ります。 これは、以前のSwiftコードがどのように見えるかです:
var result = divide(2.5, by:3) switch result { case Success(let quotient): doSomethingWithResult(quotient) case Failure(let errString): println(errString) }
もう少し本物ですが、はるかに優れています!
switch
構築により、値を名前(
quotient
および
errString
)に関連付けてコード内でアクセスでき、エラーの発生に応じて結果を処理できます。 解決されたすべての問題:
- ポインターはありませんが、さらに参照を解除します
divide
関数に追加のパラメーターを渡す必要はありません- コンパイラは、すべての列挙オプションが処理されているかどうかを確認します。
quotient
とerrString
は列挙されているため、分岐でのみ宣言され、エラーが発生した場合に結果を参照することはできません
しかし、最も重要なこと-このコードは私が望んでいたことを正確に行います-値を計算し、エラーを処理します。 タスクに直接関連しています。
次に、より深刻な例を見てみましょう。 結果を処理したいとします-結果からマジックナンバーを取得し、それから最小の素数を見つけ、その対数を取得します。 計算自体には魔法のようなものは何もありません-私はランダムな操作を選択しました。 コードは次のようになります。
func magicNumber(divisionResult:Result<Float>) -> Result<Float> { switch divisionResult { case Success(let quotient): let leastPrimeFactor = leastPrimeFactor(quotient) let logarithm = log(leastPrimeFactor) return Result.Success(logarithm) case Failure(let errString): return Result.Failure(errString) } }
簡単そうです。 しかし、マジックナンバーから取得したい場合は...それに一致するマジックスペルはどうでしょうか。 私はこのように書くでしょう:
func magicSpell(magicNumResult:Result<Float>) -> Result<String> { switch magicNumResult { case Success(let value): let spellID = spellIdentifier(value) let spell = incantation(spellID) return Result.Success(spell) case Failure(let errString): return Result.Failure(errString) } }
しかし、今では、各関数に
switch
式があり、それらはほぼ同じです。 さらに、両方の関数は正常な値のみを処理しますが、エラー処理は常に注意をそらします。
物事が繰り返されるようになったら、抽象化の方法を検討する価値があります。 また、Swiftには適切なツールがあります。 Enumにはメソッドを含めることができ、
map
メソッドを使用して
Result
をリストすることで、これらの
switch
必要性を取り除くことができます。
enum Result<T> { case Success(T) case Failure(String) func map<P>(f: T -> P) -> Result<P> { switch self { case Success(let value): return .Success(f(value)) case Failure(let errString): return .Failure(errString) } } }
mapメソッドの名前は、
Result<T>
を
Result<P>
に変換するため、非常に簡単に機能するためです。
- 結果がある場合、関数
f
が適用されます。 - 結果がない場合、エラーはそのまま返されます
単純であるにもかかわらず、この方法を使用すると、真の奇跡を起こすことができます。 内部でエラー処理を使用して、プリミティブ操作を使用してメソッドを書き換えることができます。
func magicNumber(quotient:Float) -> Float { let lpf = leastPrimeFactor(quotient) return log(lpf) } func magicSpell(magicNumber:Float) { var spellID = spellIdentifier(magicNumber) return incantation(spellID) }
これで、スペルは次のように取得できます。
let theMagicSpell = divide(2.5, by:3).map(magicNumber) .map(magicSpell)
メソッドを完全に取り除くことができますが:
let theMagicSpell = divide(2.5, by:3).map(findLeastPrimeFactor) .map(log) .map(spellIdentifier) .map(incantation)
かっこいいですね。 エラー処理の必要性はすべて抽象化の内部で削除され、必要な計算を指定するだけで済みます-エラーは自動的に転送されます。
一方、これは、
switch
再度使用する必要がないという意味ではありません。 ある時点で、エラーを出力するか、結果をどこかに渡す必要があります。 しかし、これは処理チェーンの最後の1つの式になり、中間メソッドはエラー処理を気にしないでください。
魔法、教えて!
これは単なる学術的な「知識のための知識」ではありません。 エラー処理の抽象化は、データ変換で頻繁に使用されます。 たとえば、
JSON
(エラー文字列または結果)の形式のサーバーからデータを取得し、辞書に変換してからオブジェクトに変換し、このオブジェクトをUIレベルに転送して、複数の個別のオブジェクトを作成する必要がある場合がよくあります。 。 リストを使用すると、有効なデータに対して常に機能するようにメソッドを記述でき、
map
呼び出しの間にエラーがスローされます。
そのようなトリックを一度も見たことがなければ、しばらく考えて、コードをいじってみてください。
(しばらくの間、コンパイラーは一般化された列挙のコード生成に問題を抱えていましたが、おそらくすべてが既にコンパイルされています)。 このアプローチがどれほど強力であるかを評価していただけると思います。
数学が得意であれば、おそらく私の例のバグに気づいたでしょう。 対数関数は負の数に対して宣言されておらず、
Float
値が宣言されている場合があります。 この場合、
log
は
Float
だけでなく、
Result<Float>
を返し
log
。 そのような値をmapに渡すと、ネストされた
Result
を取得し、それを操作するだけで機能しなくなります。 このために、トリックもあります-自分で考え出してみてください。しかし、怠け者には-次の記事で説明します。