CodeFest 2019. Евгений Ёлчев (RedMadRobot) — Функциональный Swift — это просто

16b6c87229eaf58768d25ed7b2bbbf52?s=47 CodeFest
April 06, 2019

CodeFest 2019. Евгений Ёлчев (RedMadRobot) — Функциональный Swift — это просто

— ФП — это не сложно;
— Мы уже используем ФП и можем использовать ещё активнее;
— Элементы ФП в Swift;
— Варианты использования без предварительной подготовки команды.

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 06, 2019
Tweet

Transcript

  1. Функциональный Swift Евгений, Елчев iOS разработчик Redmadrobot

  2. О чем я буду говорить ▪ Функциональная парадигма ▪ Функциональные

    инструменты (Функторы, монады) ▪ Работа с UI =2
  3. =3 f(x)

  4. Математическая функция =4 x− f(x)− f−

  5. Математическая функция func summ(a: Int, b: Int) -> Int {

    return a + b } let x = summ(a: 2, b: 3) =5
  6. Обычная функция =6 f− x− f(x)−

  7. Глобальная переменная var z = 5 =7

  8. Обычная функция func summ(a: Int, b: Int) -> Int {

    return a + b + z } let x = summ(a: 2, b: 3) =8
  9. Обычная функция =9 x− f(x)− f−

  10. Обычная функция func summ(a: Int, b: Int) -> Int {

    z = b - a return a + b } =10
  11. Чистые функции ▪ Не зависят от глобального состояния ▪ Не

    создают сайд эфектов ▪ Все математические функции чистые =11
  12. «Чистый» класс class User { let name: String let surname:

    String let email: String func getFullname() -> String { return name + " " + surname } func change(email: String) -> User { return User(name: name, surname: surname, email: email) } } =12
  13. «Чистый» класс class User { let name: String let surname:

    String let email: String } =13
  14. «Чистый» класс func getFullname() -> String { return name +

    " " + surname } func change(email: String) -> User { return User(name: name, surname: surname, email: email) } =14
  15. «Чистый» класс let ivan = User( name: "Иван", surname: "Иванов",

    email: «ivanov@example.com") let fullName = User.getFullname(ivan) =15
  16. «Чистый» класс let fullName = ivan.getFullname() let newIvan = ivan.change(email:

    «god@example.com") =16
  17. Плюсы/минусы чистоты ➕ Проще удержать в голове ➕ Проще отлаживать

    ➕ ⛓ Проще распаралеливать ➕ Проще тестировать ➖ Больше кода ➖ ⚡ Большее потребление ресурсов =17
  18. Грязь, она по всюду ▪ Сетевые запросы ▪ Файлы ▪

    БД ▪ Датчики (локация, камера, …) ▪ UI =18
  19. В чем проблема грязных функций? ➡ ➡ ➡ =19

  20. Мы не контролируем что они вернут ➡ ❓ ➡ ➡

    ❓ =20
  21. Они могут вообще ничего не вернуть ➡ ➡ ➡ ❌

    =21
  22. Постоянные проверки =22 let firstInt = 3 let intFromServer: Any?

    = getIntFormServer() let summ: Any? if let any = intFromServer, let int = any as? Int { summ = summ(a: firstInt, b: int) } else { summ = nil }
  23. Данные Шредингера ➡ ➡ ➡ ➡ ➡ ❌ =23

  24. Optional =24 let firstInt = 3 let intFromServer: Int? =

    getIntFormServer() let summ: Int? if let int = intFromServer as? Int { summ = summ(a: firstInt, b: int) }
  25. Проблема =25 x− f− y−

  26. Функтор =26 ⬆ ❌ ➡ ➡ ➡ ➡ ➡ ⬇

    ➡➡➡ ➡ ⬆
  27. Функтор =27 let firstInt = 3 let intFromServer: Int? =

    getIntFormServer() let result = intFromServer.map { summ(a: firstInt, b: $0) } result.map { print($0) }
  28. Вычисление в одно действие =28 getIntFormServer().map { summ(a: 3, b:

    $0) }.map { print($0) }
  29. Обработка нештатных ситуаций =29 let result = getIntFormServer().map { summ(a:

    3, b: $0) } result.map { print($0) } result.failure { print("Что то пошло не так") }
  30. Данные Шредингера ➡ ➡ ➡ ➡ ➡ ❌ =30

  31. Функции с несколькими аргументами =31 let result = getIntFormServer().map {

    summ(a: 3, b: $0) }
  32. Разделение функции =32 func summ(_ a: Int) -> ((Int) ->

    Int) { return { b in return a + b } }
  33. Вычисление в несколько шагов =33 let summWith3 = summ(3) let

    result = getIntFormServer().map { summWith3($0) } let summWith3 = summ(3) let result = getIntFormServer().map(summWith3)
  34. Каррирование =34 func curry<A, B, C>(_ function: @escaping ((A, B))

    -> C) -> (A) -> (B) -> C { return { (a: A) -> (B) -> C in { (b: B) -> C in function((a, b)) } } }
  35. Каррирование =35 let summWith3 = curry(+)(3) let result = getIntFormServer().map(summWith3)

  36. Апликативный функтор =36 let a: Int? = 3 let b:

    Int? = 7 let c = a.map { $0 + b } !!!
  37. Апликативный функтор =37 let a: Int? = 3 let b:

    Int? = 7 let summWithA = a.map(curry(+)) let result = b.map(summWithA!)
  38. Апликативный функтор =38 extension Optional { func map<U>(_ f: ((Wrapped)

    -> U)?) -> U? { guard let f = f else { return nil } return self.map(f) } }
  39. Апликативный функтор =39 let a: Int? = 3 let b:

    Int? = 7 let summWithA = a.map(curry(+)) let result = b.map(summWithA)
  40. Апликативный функтор =40 let a: Int? = 3 let b:

    Int? = 7 let result = b.map(a.map(curry(+)))
  41. Апликативный функтор =41 let a: Int? = 3 let b:

    Int? = 7 let c = curry(+) <*> a <*> b
  42. Без кастомных операторов =42 @discardableResult func zip<U, Z, Result>( _

    optional1: U?, _ optional2: Z?, _ transform: (U, Z) throws -> Result) rethrows -> Result? { guard let optional1 = optional1, let optional2 = optional2 else { return nil } return try transform(optional1, optional2) }
  43. Без кастомных операторов =43 let a: Int? = 3 let

    b: Int? = 7 let result = zip(a, b, +)
  44. Монада =44 let z = Optional("1").map { Int($0) } if

    let one = z, let two = one { print(two) } type(of: z) // Optional<Optional<Int>>
  45. Монада =45 ➡ ➡

  46. Монада =46 let z = Optional("1").flatMap { Int($0) } if

    let one = z { print(one) } type(of: z) // Optional<Int>
  47. Плюсы/минусы монад и функторов ➕ Лаконичный синтаксис ➕ Декларативный подход

    ➕ Фокус на работе с данными, а не ошибками ➕ Безопасность ➖ Сложнее отлаживать =47
  48. Работа с UI ▪ UI - изменяемое состояние ▪ Много

    изменяемых состояний ▪ Все методы работы с UI грязные =48
  49. Форма входа =49

  50. Текст в полях изменился @IBAction func textFieldTextDidChange() { guard let

    login = loginView.text, let password = passwordView.text else { loginButton.isEnabled = false return } let loginIsValid = login.count > constants.loginMinLenght if loginIsValid { loginView.layer.borderColor = constants.normalColor } let passwordIsValid = password.count > constants.passwordMinLenght if passwordIsValid { passwordView.layer.borderColor = constants.normalColor } loginButton.isEnabled = loginIsValid && passwordIsValid } =50
  51. Закончили ввод логина @IBAction func loginDidEndEdit() { let color: CGColor

    if let login = loginView.text, login.count > 3 { color = constants.normalColor } else { color = constants.errorColor } loginView.layer.borderColor = color } =51
  52. Закончили ввод пароля @IBAction func passwordDidEndEdit() { let color: CGColor

    if let password = passwordView.text, password.count > 6 { color = constants.normalColor } else { color = constants.errorColor } passwordView.layer.borderColor = color } =52
  53. Нажали кнопку войти @IBAction private func loginPressed() { guard let

    login = loginView.text, let password = passwordView.text else { return } auth(login: login, password: password) { [weak self] user, error in if let user = user { /* успех */ } else if error is AuthError { guard let `self` = self else { return } self.passwordView.layer.borderColor = self.constants.errorColor self.loginView.layer.borderColor = self.constants.errorColor } else { /* Другие ошибки */ } } } =53
  54. Минусы ▪ 4 метода изменяют состояния полей ▪ 2 метод

    изменяют состояние кнопки «Войти» ▪ 4 раза производится раскрытие опционального значения каждого поля ввода ▪ Не очевидны конечные состояния ▪ Нелинейный flow =54
  55. =55 Поток данных

  56. =56 ➡ ➡ Поток данных

  57. Текущее состояние UI struct LoginOutputModel { let login: String let

    password: String var loginIsValid: Bool { return login.count > 3 } var passwordIsValid: Bool { return password.count > 6 } var isValid: Bool { return loginIsValid && passwordIsValid } } =57
  58. Модель изменений UI struct LoginInputModel { let loginBorderColor: CGColor? let

    passwordBorderColor: CGColor? let loginButtonEnable: Bool? let popupErrorMessage: String? } =58
  59. События меняющие UI enum Event { case textFieldTextDidChange case loginDidEndEdit

    case passwordDidEndEdit case loginPressed case authFailure(Error) } =59
  60. Текст в полях изменился @IBAction func textFieldTextDidChange() { updateView(.textFieldTextDidChange) }

    =60
  61. Закончили ввод логина @IBAction func loginDidEndEdit() { updateView(.loginDidEndEdit) } =61

  62. Закончили ввод пароля @IBAction func passwordDidEndEdit() { updateView(.passwordDidEndEdit) } =62

  63. Нажали кнопку войти @IBAction private func loginPressed() { updateView(.loginPressed) let

    completion: (User?, Error?) -> Void = { [weak self] user, error in user.map { _ in /* успех */ } zip(self, error) { $0.updateView(.authFailure($1)) } } outputModel.map { auth(login: $0.login, password: $0.password, completion: completion) } } =63
  64. Изменяем UI func updateView(_ event: Event) { let inputModel =

    makeInputModel(event: event, outputModel: outputModel) inputModel.loginBorderColor.map { loginView.layer.borderColor = $0 } inputModel.passwordBorderColor.map { passwordView.layer.borderColor = $0 } inputModel.loginButtonEnable.map { loginButton.isEnabled = $0 } inputModel.popupErrorMessage.map(showPop) } =64
  65. Получаем текущее состояние var outputModel: LoginOutputModel? { return zip(loginView.text, passwordView.text,

    LoginOutputModel.init) } =65
  66. Получаем модель изменений UI func makeInputModel(event: Event, outputModel: LoginOutputModel?) ->

    LoginInputModel { switch event { case .textFieldTextDidChange: let mapValidToColor: (Bool) -> CGColor? = { $0 ? normalColor : nil } return LoginInputModel(/**/) case .loginDidEndEdit: return LoginInputModel(/**/) case .passwordDidEndEdit: return LoginInputModel(/**/) case .loginPressed: return LoginInputModel(/**/) case .authFailure(let error) where error is AuthError: return LoginInputModel(/**/) case .authFailure: return LoginInputModel(/**/) } } =66
  67. Текст в полях изменился case .textFieldTextDidChange: let mapValidToColor: (Bool) ->

    CGColor? = { $0 ? normalColor : nil } return LoginInputModel( loginBorderColor: outputModel .map { $0.loginIsValid } .flatMap(mapValidToColor), passwordBorderColor: outputModel .map { $0.passwordIsValid } .flatMap(mapValidToColor), loginButtonEnable: outputModel?.passwordIsValid ) =67
  68. Плюсы/минусы ➕ Очевидный поток данных ➕ Более декларативный подход ➕

    Проще отлаживать ➕ Можно покрыть модульными тестами ➖ Обновляется весь UI сразу ➖ Больше кода =68
  69. Итог ▪ Промышленное ФП это не функции, монады, функторы и

    математику. ▪ Чистые функции упрощают код ▪ Функторы и монады помогают концентрироваться на работе с данными ▪ Нужно стараться уменьшать количество изменяемых состояний. =69
  70. @twitter Евгений, Елчев iOS разработчик Redmadrobot Вопросы? facebook www. info@mail.ru