Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть