Функциональный Swift

в 10:40, , рубрики: appscast, appsconf, swift, Блог компании Конференции Олега Бунина (Онтико), разработка мобильных приложений, Разработка под android, разработка под iOS, функциональное программирование

Что объединяет «каррирование», «монады», «алгебраические типы данных»? Не только тот факт, что часть разработчиков старается обходить эти слова стороной, но еще и функциональное программирование. Под заботливым руководством Евгения Елчева мы погрузились в функциональную парадигму и почти все поняли. Не пугайтесь раньше времени, смело читайте расшифровку десятого выпуска подкаста AppsCast.

Функциональный Swift - 1

Даниил Попов: Всем привет. Сегодня у нас в гостях Евгений Елчев из солнечного Красноярска. Женя, расскажи, чем ты занимаешься и как пришел в функциональное программирование?

Евгений Елчев: Всем привет. Я iOS-разработчик в Redmadrobot, как и все остальные крашу кнопки, иногда пишу бизнес-логику.

С функциональным программированием я сначала познакомился через статьи. Я, не совсем понимая суть, думал, что это вроде процедурного программирования, без классов. Когда прочел одну из статей повнимательнее, осознал свою неправоту и начал копать. Нельзя сказать, что я прямо-таки пришел в функциональное программирование, так как настоящие адепты костьми за него лягут и пишут на Haskell, используя монады где только можно. Я же просто погрузился и использую только в продакшене.

Даниил Попов: Так, уже монады пошли.

Евгений Елчев: Уже сложно?

Даниил Попов: Я пытался пройти такой же дорогой, но открывал статью, видел слова «каррирование», «монады» и сразу закрывал, думая, что пока не достоин. У меня вообще есть шансы?

Евгений Елчев: Конечно. Этого можно вообще не знать.

Простыми словами про функциональщину

Даниил Попов: Давай для тех, кто никогда не слышал о функциональной парадигме, дадим простое определение.

Евгений Елчев: Парадигмы каждый понимает по-своему. Если брать объяснение из Википедии, то это использование математических функций, где вся программа трактуется как математическая функция.

Функциональный подход (ФП) — это когда ты используешь в своей работе функции, в которых есть только входные аргументы и выходное значение. Если вся программа состоит из таких функций, то это функциональная программа.

Даниил Попов: ООП было логическим продолжением обычного процедурного программирования и решало вопрос инкапсулирования данных в классы. Какие проблемы призвано решить функциональное программирование?

Евгений Елчев: Функциональное программирование изобрели математики. Собрались ребята и решили создать парадигму, где все можно доказывать. Есть код, он еще не запущен, но уже весь доказуем. Любую точку программы можно рассчитать, понимая, куда мы придем, когда допустим какое-либо действие.

Это звучит абстрактно, поэтому разберем на примере чистой функции. Пишем функцию sum, которая принимает два аргумента, передаем ей 2 и 3, получаем 5 и можем это доказать. Это всегда истина. Если вся наша программа состоит из таких функций, то она вся доказуема.

При создании языков, базовых функций стало не хватать, и появились дополнительные возможности: лямбды, функции высшего порядка, монады, моноиды.

Функциональная парадигма не решает отдельную проблему, это все то же желание написать хороший код как можно проще, чтобы программы были стабильными и их было легко поддерживать.

Если присмотреться, многие вещи, которые мы используем в ООП, нашли свое отражение в функциональном подходе. В ОПП есть классы, которые инкапсулируют набор полей. В ФП тоже можно так сделать с помощью type-классов. Как любит говорить Виталий Брагилевский: «Если ты смотришь на табличку, где по строкам идут данные, а по колонкам функции, то ФП идет по колонкам, ООП по строкам». Вот и все.

Даниил Попов: Как ФП соотносится с другими парадигмами? Могу ли я на ООП-языке писать функционально? Как миксовать парадигмы, и есть ли в этом смысл?

Евгений Елчев: Парадигма ограничивается тем, что ты пишешь функции с данными. Одна из особенностей ФП — отсутствие изменяемых состояний. Если твои данные — это класс, то проблем нет. Если класс полностью иммутабельный, то его можно использовать. Класс — это просто тип, как строка или число, только более сложный, состоящий из нескольких значений.

Даниил Попов: Ты ранее сказал, что можно доказать математическую корректность программы, если писать исключительно функционально. Тогда шутка про «скомпилировалось — работает» для функциональных языков перестает быть шуткой, так?

Евгений Елчев: Если смотреть на ошибки ввода/вывода, то да. Раньше программисты боролись с проблемой: подключился к сети, сети нет, вернулся nill, и все упало. Для решения проще всего было проверить, что пришло — nill/не nill, но, так как оставался риск, что не все учтено, программа могла скомпилироваться и упасть.

В современных языках это решено. В Haskell можно написать программу, которая будет работать и не падать, но насколько корректно она работает, никто не скажет. Конечно, там строгие типы, и нельзя совершить ошибку, сложив число со строкой, но всегда можно оставить в приложении баги, а оно будет работать.

Место функционального подхода в Swift

Алексей Кудрявцев: Насколько Swift можно назвать функциональным языком?

Евгений Елчев: Можно. Функциональщина позиционируется как stateless, но на Swift можно писать, избегая таких состояний. При этом Swift — это не то же самое, что писать под iOS, где везде есть состояния. Конечно, в Swift нет специальных инструкций как в Haskell, где все функции чистые по умолчанию и компилятор не разрешит обратиться к состоянию и изменить его. Если же пометить функцию как «грязную», то изменения становятся доступны.

Алексей Кудрявцев: Во втором или третьем Swift был модификатор pure, но он действовал только на уровне компиляции, чтобы глобальные значения не изменялись. Ты в них что-то записывал, но компилятор все вырезал.

Евгений Елчев: Да, в iOS компилятор за таким следить не будет. Все целиком на нашей совести: как напишешь, так и будет.

Алексей Кудрявцев: Ты говоришь, что в iOS-приложениях много состояний, а где какие и что с ними делать, если ты пишешь в функциональном стиле?

Евгений Елчев: Самое главное состояние — это UI, например, поля ввода. Сделать с ними практически ничего нельзя. Можно попробовать от них абстрагироваться, собрать в одном месте и писать как можно больше кода без их учета. Например, пишешь одну грязную функцию, которая получает все данные из UI.

В своей статье я приводил пример формы авторизации, где важно, чтобы пользователь ввел логин/пароль. Пишем одну грязную функцию, которая возвращает структуру с авторизационными данными, а затем пишем на нее чистый код. Получили эти данные, провалидировали, если результат валиден, отправляем запрос на сервер. Запрос на сервер — тоже грязная функция, а его обработка целиком может быть чистой. «Получили, распарсили» — это линейная функция: на вход data, на выход — наша структура. Дальше преобразовали, отфильтровали и можно снова показать на экране.

Алексей Кудрявцев: В Haskell компилятор сильно помогает. Если откуда-то приходит state, вся цепочка вызовов будет считаться грязной и нужно оборачивать все в монады. Если же функция чистая, то работает кэширование результатов — на одни и те же данные всегда один и тот же выход. В Swift приходится самостоятельно реализовывать мапы и пытаться возвращать результат, если он уже закэширован.

Даниил Попов: Большинство современных языков считаются мультипарадигменными и во многих есть функциональные особенности. Например, в Java есть специальная аннотация для интерфейса — @FunctionalInterface, которая обязывает разработчика определить в интерфейсе только один метод, чтобы затем этот интерфейс в виде лямбд использовался во всем коде. При добавлении второго метода или удалении существующего, компилятор начнет ругаться, что это перестало быть функциональным интерфейсом. Есть ли в Swift, в отрыве от iOS-платформы, такие функциональные фишки?

Евгений Елчев: Мне сложно понять, что делает такая аннотация в Java. Если ты имеешь в виду, что имплементируешь этот интерфейс к классу, а потом реализуешь всего один метод, то в Swift таких ограничений нет. Можно создать typealias, назвать его и как тип функции использовать в качестве типа аргументов, типа переменной для того, чтобы присвоить замыкание. Можно определить ограничения — входные и выходные аргументы замыкания. Сами функции высшего порядка, которые могут принимать замыкания — это полиморфизм, и в Swift можно построить полиморфизм на типах, не ограничиваясь объектами.

А вот конкретных функциональных вещей я не знаю. Когда-то в первом Swift было каррирование, но его выпилили. Сейчас мы можем сами написать функцию для каррирования, либо написать функцию так, чтобы она возвращала замыкания одно в другом, но это не совсем то.

У нас нет никаких коробочных функторов или монад. Их даже нельзя написать. Новые фишки в Swift 5.1 должны помочь это сделать, но я пробовал написать такой код, и xCode упал.

В принципе, в Swift при желании несложно все сделать самому. Есть уже из коробки монада optional (в Haskell — maybe). У нее есть map и flatmap для построения линейного вычисления.

В Swift мощный pattern matching. Switch, который есть почти в каждом языке и в большинство случаев сопоставляет integer с единицей, может сопоставлять переменную с конкретным образцом, диапазонами, типами, извлекать значения из связанных типов. Есть carthage — составляешь новый тип, передавая в него несколько других. На их основе тоже можно делать pattern matching. Есть перечисление, которое может ограничивать типы, подвязывать к ним связанные типы.

Алексей Кудрявцев: Уточню, что связанные типы похожи на котлиновские sealed-классы. Это enum внутри кейса, в который можно положить связанное значение. В switch можно написать: вот case, разверни, внутри объект. Например, кейсы user и company с соответствующими объектами могут быть enum и можно свитчиться. Только sealed-классы расширяемы, а switch конечен.

Зачем мобильщику функциональщина?

Даниил Попов: Чем же функциональный подход полезен для мобильной разработки? Есть ли проблемы, которые он решает?

Евгений Елчев: Нет конкретной проблемы, которую можно решить именно с помощью функционального программирования.

Самое важное, что следуя этим принципам, даже если не получается, мы должны отказываться от состояний, потому что именно они — главная боль.

Отказываясь от них, ты делаешь свой код более понятным. Я не говорю, что будет меньше ошибок, потому что это надо как минимум замерять. Тем не менее, когда ты начинаешь что-то внедрять, код меняется. Часто бывает, что смотришь на код и в нем все по делу, а начинаешь переписывать, менять местами, убирать лишнее и читается легче.

Следуя функциональной парадигме, получаешь дополнительный источник вдохновения.

Даниил Попов: Если я начну писать в ООП-языке такие иммутабельные классы и использовать иммутабельные методы, можно будет сказать, что я пишу функционально?

Евгений Елчев: Да, при этом ты начинаешь видеть плюсы. Становится проще тестировать методы из-за отсутствия глобального состояния, проще составлять из методов цепочки вычислений.

Даниил Попов: В своей статье ты объясняешь, что такое чистая функция и side-эффекты. Ты приводишь пример с суммированием, где функция еще и модифицирует внешнее состояние. Проблема в том, что когда ты читаешь такой код, сложно держать в голове все изменения: нужно посмотреть на эту глобальную переменную, кто еще в нее читает, кто еще в нее пишет, что может произойти. Зато функциональный подход позволяет тебе держаться в потоке, не ходить в соседние классы, ты просто читаешь код.

Алексей Кудрявцев: Если ты в функциональном языке, то с одной стороны тебе легче код писать, но с другой, приходится понимать, в какой ты сейчас монаде.

Евгений Елчев: Да, но когда ты начинаешь писать все на чистых функциях, возникают другие проблемы. Например, как выстроить длинную цепочку вычислений. В обычном стиле ты, не задумываясь об этом, легко докидываешь данные, которых не было изначально. В функциональном подходе этого делать нельзя: приходится дробить цепочки, все вычисления, используемые в нескольких методах, соединять в состояния. К этому нужно привыкать.

С другой стороны, в отличие от классов в ОПП, которые делают код закостенелым и мало поддающимся композиции, функции могут быть более гибкими. Можно написать одну функцию, добавить свободы с помощью замыкания, накидать таких функций и объединять их в цепочки.

Алексей Кудрявцев: Это похоже на идеологию Unix: есть bash, terminal и можно передавать данные из маленьких программ, которые делают одно небольшое действие, в другие.

Даниил Попов: Мне это напомнило Rx-подход, где пишут гигантские цепочки.

Евгений Елчев: Вы оба правы. И Unix-way про это, а Rx — это сплав идеи биндинга и реактивщины. В ФП мы биндимся на источник события и в цепочке вычислений изменяем его, подвязывая результат на конечное состояние.

Даниил Попов: Хороши ли вообще мультипарадигменные языки, насколько это удобно и полезно, что язык умеет и так, и сяк?

Евгений Елчев: Если четко следовать какой-то парадигме, всегда будут вещи, которые делать неудобно. Есть вещи, которых сложно добиться в функциональном стиле, например, хранить state и сделать cache.

Когда есть возможность выбрать инструмент, который больше подходит под конкретную задачу — это круто.

Можно создать класс, внутри него сделать несколько методов в функциональном стиле и организовать код лаконично цепочками, или отказаться полностью от класса, наделать нужных функций и использовать их.

Минус в том, что возникает дилемма выбора и чем больше вариантов, тем сложнее выбирать. Разобраться тоже становится сложнее: чем больше вариантов, тем сложнее читать код.

Про варенье из монад

Алексей Кудрявцев: Вернемся к функциональщине, что такое монада?

Евгений Елчев: Я бы назвал это контейнером, в который можно объединять цепочки вычислений. Самый просто способ — это контейнер, к которому можно применить функцию и преобразовать в новый контейнер с измененным значением.

Представьте коробку, в которой лежит клубника, и есть прибор, которая позволяет из клубники делать варенье, но ты не можешь в него положить коробку с клубникой, ее надо высыпать. Монады — эта та самая вещь, которая позволяет засунуть в прибор коробку.

Это не state в прямом понимании, так как state хранится отдельно, а здесь контекст (коробка) со значением и ты передаешь из одного в другое. Это передача информации от одного вычисления к другому.

Алексей Кудрявцев: Получается, что в функциональном подходе для того, чтобы сделать варенье, нужно залезть внутрь коробки…

Евгений Елчев: Прелесть в том, что в коробку лезть не надо. Можно кидать коробку.

Функциональщина для избранных?

Даниил Попов: Бытует мнение, что функциональным программированием нельзя заниматься без докторской степени по математике. Правда ли это?

Евгений Елчев: Это неправда. Знание математики, конечно, делает все лучше, но я забыл математику после окончания университета и нормально живу. По сути все это инструменты, которые воплотились в языках в решение конкретных задач. Их можно использовать, не пытаясь доказать математически. Пока ты будешь составлять уравнение с математической точки зрений, быстрее и проще будет накидать методом тыка парочку строк кода, и они будут работать.

Алексей Кудрявцев: Насколько увлечение функциональным подходом может мешать продуктовой разработке? Если часть кода уже написана функционально, нет ли сложности в работе с ним?

Евгений Елчев: Вообще нет. Если ты не маньяк и не будешь писать огромную экосистему с декораторами, то можно использовать тот же pattern matching.

Сложнее будет, если захочешь перейти на новый элемент функциональщины. Например, недавно появился пятый Swift и монада result, раньше ты ей не пользовался, а теперь решил, что все будет на ней. Берешь функцию запроса в сеть и пишешь, что ее результат теперь result (либо данные, либо ошибка), и решаешь объединить со следующим запросом, а там у тебя отдельно замыкание со значением и error, и это нужно переписать. Начал так писать в одном месте, очнулся через два дня, когда переписал половину кода, еще и новые обертки для библиотек сделал, чтобы красиво объединялось.

С чего начать?

Даниил Попов: Что почитать новичку, чтобы понять функциональное программирование?

Евгений Елчев: Надо взять чисто функциональный язык, например, Haskell и попробовать на практике. Берешь учебник и делаешь самые простые примеры. Тут ты и понимаешь подход — когда нет for, нельзя создать переменную, в которой можно поменять значение. Лично я в свое время взял книжку «Изучай Haskell во имя добра», где все описано простым языком. После можно начать читать статьи в интернете: про то, как выглядят монады в Swift, про алгебраические типы данных. Пара статей, и становится понятно, что этого не стоит бояться.

Даниил Попов: Самое сложное, это сломать парадигму в собственной голове.

Евгений Елчев: Не надо резко погружаться в функциональное программирование. Многие считают, что как сядут, так и начнут функционально писать — это неправильно.

Алексей Кудрявцев: Самое классное, что я видел — это курс на Stepic по Haskell от Дениса Москвина. Начинаешь с того, что пару чисел складываешь, а заканчиваешь тем, что монады в монады заворачиваешь. А если хочется совсем сломать голову, то есть книжка «Структура интерпретации компьютерных программ» — это курс на Lisp от простых примеров до того, что ты пишешь интерпретатор Lisp на Lisp.

Если первичный страх перед функциональщиной прошел, то гляньте еще доклад Виталия Брагилевского с весеннего AppsConf. Впрочем, в осеннем сезоне AppsConf мы затронем темы не менее интересные — iOS-сообщество с нетерпением ждет доклад Даниила Гончарова по реверсинженирингу Bluetooth, а android-разработчики вместе с Александром Смирновым обсудят актуальные подходы к построению анимаций

Автор: Алексей Кудрявцев

Источник


* - обязательные к заполнению поля