Взять вот обработку ошибок. Конкретный пример – деление двух чисел, которое должно вызвать исключение если делитель равен нулю. В 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 вообще не существует, поскольку он прячет под капотом всю работу с указателями. Остальные два пункта решаются с помощью перечислений.
Если во время вычисления может возникнуть ошибка, то результата может быть два:
- Успешный – с возвращаемым значением
- Безуспешный – желательно, с объяснением причины ошибки
Эти варианты взаимоисключающие – в нашем примере, деление на 0 вызывает ошибку, а все остальное – возвращает результат. Swift выражает взаимоисключение с помощью «перечислений». Вот так выглядит описание результата вычисления с возможной ошибкой:
enum Result<T> { case Success(T) case Failure(String) }
Экземпляром данного типа может быть либо метка Success
со значением, либо Failure
с сообщением, описывающим причину. Каждое ключевое слово case описывает конструктор: первый принимает экземпляр T
(значение результата), а второе 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 есть нужные инструменты. Перечисления могут иметь методы, и я могу избавиться от необходимости в этих выражениях switch
с помощью метода map
для перечисления Result
:
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
. В какой-то момент придется либо вывести ошибку, либо передать результат куда-то. Но это будет одно единственное выражение в самом конце цепочки обработки, и промежуточные методы не должны заботиться об обработке ошибок.
Магия, скажу я вам!
Это все – не просто академические «знания ради знаний». Абстрагирование обработки ошибок очень часто применяется при трансформации данных. Например, частенько бывает нужно получить данные с сервера, которые приходят в виде JSON
(строка с ошибкой или результат), преобразовать их в словарь, потом в объект, а потом передать этот объект на уровень UI, где из него будет создано еще несколько отдельных объектов. Наше перечисление позволит писать методы так, будто они всегда работают на валидных данных, а ошибки будут пробрасываться между вызовами map
.
Если вы никогда до этого не видели подобных приемов, задумайтесь об этом ненадолго и попробуйте повозиться с кодом. (У компилятора какое-то время были проблемы с генерацией кода для обобщенных перечислений, но возможно, все уже компилируется). Думаю, вы оцените то, насколько это мощный подход.
Если вы разбираетесь в математике, вы наверняка заметили баг в моем примере. Функция логарифма не объявлена для отрицательных чисел, и значения типа Float
могут таковыми быть. В таком случае, log
вернет не просто Float
, а скорее Result<Float>
. Если передать такое значение в map, то мы получим вложенный Result
, и работать с ним так просто не получится. Для этого тоже есть прием – попробуйте придумать его самостоятельно, а для тех, кому лень – опишу в следующей статье.
ссылка на оригинал статьи http://habrahabr.ru/post/244575/