Задание 2. Решение — Возвращаем enum Result (дополнительные пункты 2, 3) Swift 1.2 и Swift 2.0

Screen Shot 2015-03-31 at 5.32.05 PM
Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Developing iOS 8 app: Programming: Project 2″.  Текст Домашнего задания на русском  языке доступен на

Задание 2 iOS 8_new.pdf

Результаты выполнения задания можно обсуждать на форуме Swift[ru] . Ваше решение может быть лучше и интереснее. Выкладывайте его в Github, давайте ссылку на Dropbox или используйте другие системы управления версиями. Xcode  работает напрямую с Github.

Это окончание решения Задания 2- дополнительные пункты 2 и 3, предыдущие пункты можно посмотреть:

Задание 2. Решение — обязательные пункты 1-4. 

Задание 2. Решение — обязательные пункты 5-8. 

Задание 2. Решение — обязательные пункты 9-12. 

Задание 2. Решение — Убираем лишние скобки. Дополнительный пункт 1. 

Окончательный вариант кода (включая все обязательные и дополнительные пункты) для Задания 2  можно найти на Githubиспользование паттерна проектирования Singleton для NSNumberFormatter можно посмотреть в посте

Как создать NSNumberFormatter Singleton в Swift (1.2  и 1.1) —  дополнение к Заданию 2, а код на Github

Пункт 2 дополнительный

Добавьте “Undo” к вашему калькулятору: в дополнительном пункте Задания 1 вы добавляли  кнопку  “backspace”, если пользователь ввел неверную цифру. Теперь мы говорим о комбинации “backspace” и реального “Undo” в единственной кнопке. Если пользователь находится в середине ввода числа, то это кнопка работает как  “backspace”. Если пользователь не находится  в середине ввода числа, то должно выполняться Undo последней вещи, которая была выполнена в CalculatorBrain.     

Добавляем в API калькулятора новый метод

[js]
func popStack() -> Double? {
if !opStack.isEmpty {
opStack.removeLast()
}
return evaluate()
}
[/js]

И используем его в нашем Controller на кнопке «backspace»

Screen Shot 2015-08-24 at 9.51.52 AM
Код находится на Github.

Пункт 3 дополнительный

Добавьте новый метод,  evaluateAndReportErrors().  Он должен работать как evaluate() за исключением того, что если возникают проблемы с оценкой стэка (не установлены значения переменных или пропущены операнды, деление на 0, квадратный корень из отрицательного числа и т.д.), то вместо возвращения nil, он возвращает String с тем, что собой представляет проблема (если проблем много, то вы можете вернуть любую из них, но одну). Покажите все такие ошибки на display вашего калькулятора (вместо того, чтобы делать его пустым или показывать какие-то странные значения). Вы все равно должны реализовать evaluate() как определено в Обязательных пунктах задания, но если хотите, у вас может быть evaluate() , возвращающий nil, если имели место любые ошибки ( не просто “не установлено значение” или “недостаточно операндов”).  Методы push и perform все еще возвращают Double? (это своего рода “пустая” оценка, но мы хотим иметь возможность дать заключения по вашим Обязательным и Дополнительным пунктам Задания раздельно).    

Идем строго по подсказкам для этого дополнительного пункта задания.

  • Какая структура данных прекрасно подходит для ситуации “или- или” и которую мы уже использовали?

Мы знаем, что в этом случае нужно использовать перечисление enum и предлагается в CalculatorBrain перечисление enum Result

[js]
// enum Resul — либо для значения стэка, либо для сообщения об ошибке
// public, так как используется ViewController для получения оценки стэка
enum Result {
case Value(Double)
case Error(String)
}
[/js]

Это общий подход к обработки ошибок в функциональных языках программирования типа Haskel или Scala, а теперь и в Swift. В общем случае тип enum Result выглядит более обобщенным (generic)

[js]
enum Result<A> {
case Value(A)
case Error(NSError)
}
[/js]

Но мы будем использовать более простой вариант: успешный результат оценки стэка — Double, ошибка на любом этапе оценки — String.

  • Сообщение об ошибках не должно влиять на ваш  description.

Нам не требуется описание стэка в инфиксной форме, нам нужно только оценивать стэк и «ловить ошибки», поэтому создадим две функции — одну non-private, а другую private

[js]
func evaluateAndReportErrors() -> Result  
private func evaluateResult(ops: [Op]) -> (result: Result, remainingOps: [Op])
[/js]

на подобие пары функций

[js]
func evaluate() -> Double?
private func evaluate(ops: [Op]) -> (result: Double?, remainingOps: [Op])
[/js]

Только evaluate()  возвращает  Double?, а evaluateAndReportErrors()  будет возвращать Result .

  • Необходимо, чтобы  evaluateAndReportErrors() умела спрашивать операции BinaryOperation и UnaryOperation, какие ошибки (если они есть) будут генерироваться при передачи в них  операнда (ов).

В следующих двух пунктах предлагается решение, которое мы и возьмем на вооружение.

  • Один из способов сделать это — иметь ассоциированное значение для BinaryOperation и UnaryOperation , которое представляют собой функцию, анализирующую потенциальные аргументы и возвращающую соответствующее String сообщение об  ошибке, если выполнение операции генерирует ошибку (или nil в противном случае).                        
  • Большинство операций не могут сообщать о каких-то ошибках. Вам нужно облегчить создание таких операций. Если вы использовали подсказку, приведенную выше, то передача  nil в качестве функции, тестирующей ошибки, будет наиболее простым способом сделать это. Вы можете сделать Optional целиком тип функции, размещая тип функции в круглых скобках и ставя знак ? после круглых скобок. Например, ((Double, Double ) -> String?)? — это  Optional  функция, которая берет два Double и возвращает Optional String?.

Добавляем к операциям BinaryOperation и UnaryOperation новое ассоциированное значение в виде типа тестирующей функции 

Screen Shot 2015-03-31 at 12.22.38 PM
В нашей реализации только операции ÷ и  √ получают «тестирующие» функции

Screen Shot 2015-03-31 at 12.52.03 PM
В методе  evaluateResult рекурсивно работаем со стэком и прежде, чем выполнить унарную или бинарную операцию, тестируем полученные операнды на ошибку. Если ошибка обнаружена, то фиксируем ошибку в Result.Error  и прекращаем рекурсию. Если у операции Variable не установлено значение, то также фиксируем ошибку в Result.Error  и прекращаем рекурсию. Когда метод достигает конца, фиксируем в Result.Error ошибку о недостаточном количестве операндов  и прекращаем рекурсию.

Screen Shot 2015-03-31 at 1.46.52 PM

Требуемый метод evaluateAndReportErrors() вызывает свой вспомогательный рекурсивный метод и возвращает результат оценки стэка, используя Type Result

Screen Shot 2015-03-31 at 2.08.24 PM

Смотрим следующую подсказку для дополнительного пункта 3 Задания 2

  • Не забывайте использовать синтаксис “цепочек” Optionals. Например, вы можете использовать  Optional  функцию с именем errorTest следующим образом… if let failureDescription = errorTest?(arguments) { }… и здесь интересны два варианта:
    • первый — если она ошибочная, то есть сама функция errorTest -это nil,
    • второй — функция errorTest не nil, но она декларирована так, что возвращает nil. Это очень удобно.

Мы использовали синтаксис «цепочек» Optional при оценке ошибок, например, бинарных операций

[js highlight=»1″]
if let errMessage = errorTest?(operand1, operand2) {
return (.Error(errMessage), op1Evaluation.remainingOps)
}
return (.Value(operation(operand1, operand2)),
op2Evaluation.remainingOps)
[/js]

Здесь действительно очень удачно работают два варианта, указанные выше. Если у вашей операции, например, «+» или «» нет тестирующей функции, то errMessage = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция как у «÷«, а аргументы не дают «Деление на ноль», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции  errorTest является очень удобным при обработке ошибок.
Прежде чем мы перейдем к использованию нового метода evaluateAndReportErrors() в ViewController, необходимо, чтобы наш тип Result подтвердил протокол Printable и реализовал свойство description

Screen Shot 2015-03-31 at 2.35.57 PM

В CalculatorBrain NSNumberFormatter() используется дважды: один раз в description, а второй — в Result. В этом случае нужно помнить, что создание форматеров является экстремально затратной операцией, способной «поставить на колени» целое приложение. Поэтому настоятельно рекомендуется создавать только один экземпляр класса NSNumberFormatter и использовать его повсюду в приложении. Поэтому появился NumberFormatter и его метод класса formatter

Screen Shot 2015-08-24 at 12.18.39 PM
Теперь переходим к нашему ViewController и продолжаем читать «подсказки» к дополнительному пункту 3 Задания 2.

  • В вашем ViewController вы, возможно, обнаружите, что реализация нового    свойства var displayResult ( которое обрабатывает как значения, так и ошибки) и реорганизация старой переменной displayValue  в терминах этой новой переменной var displayResult, сделает ваш код более понятным. Или нет. Но это только подсказка.

В Controller создаем свойство  displayResult, запоминающее результаты оценки стэка. Его тип —  Result, который задекларирован в Модели CalculatorBrain.

Screen Shot 2015-03-31 at 3.10.13 PM
При установки этого свойства в Наблюдателе Свойства didSet{} автоматически устанавливаются две метки: наш дисплей display с результатами расчета и метка history , показывающая стэк в удобной инфиксной форме. У Свойства  displayValue остался только get{} и оно стало read-only

Screen Shot 2015-03-31 at 3.20.06 PM

В реализация функциональных кнопок «С», «enter» и т.д. устанавливаем переменной displayResult наше новое значение оценки стэка brain.evaluateAndReportErrors()

Screen Shot 2015-03-31 at 3.24.59 PM
Переменную displayValue используем только для преобразования строки, сформированной на дисплее display, в Double? значение.

Screen Shot 2015-08-24 at 1.32.57 PM
Код для Swift 1.2  и iOS 8.4 можно посмотреть на Github.

Код для Swift 2.0  и iOS 9 можно посмотреть на Github.
Результаты
Screen Shot 2015-03-31 at 3.36.24 PM

Screen Shot 2015-08-24 at 2.35.16 PM

Задание 2. Решение — Возвращаем enum Result (дополнительные пункты 2, 3) Swift 1.2 и Swift 2.0: 2 комментария

Обсуждение закрыто.