Многие, наверняка, слышали о функциональном программировании, некоторые пробовали написать свой Hello World, а кто-то даже завел свой «функциональный» pet-проект. Но многие ли пользовались функциональными языками в продакшене? Какие у ФП преимущества и недостатки? Оправдывает ли парадигма функционального программирования ожидания разработчиков? На эти и многие другие вопросы нам смог ответить человек, открывший для себя преимущества функционального подхода после 20 лет ООП-разработки.
Вагиф Абилов — разработчик в норвежской компании Miles. Активно использует функциональное программирование в реальных проектах, которые предъявляют высокие требования к быстродействию и масштабированию.
В ООП мы себя заранее заставляем работать в узких рамках
— Вагиф, Вы пришли в функциональное программирование после процедурного и объектно-ориентированного? Или все-таки первым было именно функциональное? Что стало для вас отправной точкой?
Вагиф Абилов: Когда я начал смотреть в сторону функционального программирования, у меня за плечами уже было около 20 лет опыта объектно-ориентированного программирования. В 90-е годы это был C++, а потом, с появлением .NET, я программировал на C#. Я типичный backend-разработчик. Работал с сервисами и, в основном, с проектами, где была важна масштабируемость и быстродействие.
Можно выделить одну из главных причин, по которой я стал присматриваться к чему-то другому. Если посмотреть на то, как пишутся с помощью ООП такого рода системы, то там большой проблемой является так называемое shared state или состояние общего доступа. То есть, если у вас многопоточная система, то она должна иметь доступ к общим данным. Таким образом, необходимо вручную управлять потоками, необходимо закрывать состояние общего доступа от того, чтобы его случайно не сломали. Значительная часть человеческих ресурсов уходит как раз на то, чтобы правильно это запрограммировать. Вообще говоря, это не является частью основной предметной области, основного функционала.
Неким сопутствующим фактором является то, что было сформулировано как «закон Амдала», который устанавливает зависимость быстродействия параллельной системы от доступности ресурсов для параллельной обработки. На конкретном примере он звучит так: «если у вас появляется 10 процессоров, но вы распараллеливаете лишь 40%, то быстродействие увеличивается в 1,56 раза». Таким образом, для распараллеливания систем, в которых большой фокус идет на ограничения доступа к данным со стороны различных потоков, есть не так много возможностей. Это меня в какой-то момент перестало устраивать, и я стал больше посматривать на возможность решать это такими средствами, которые позволили бы избавиться от shared state. Преимуществом многих функциональных языков как раз и является то, что у них по умолчанию все данные не мутируют, их нельзя изменять. Это и была первая причина, по которой я стал смотреть в сторону функционального программирования.
Где-то лет шесть назад я получил приглашение выступить на достаточно большой международной конференции NDC. К тому времени я уже начал работать в хобби-проектах с ФП и представлял там свой опыт F#. Это был романтический период, когда на доклады о функциональном программировании приходили в основном разработчики C#, с удовольствием слушали, а потом спрашивали: «ну где это в реальных системах применяется? Возможно ли это вообще применить?». Нередко докладчики сами говорили, что в реальных системах не используют ФП, но собираются. Я был примерно в таком же состоянии, то есть работал во всех проектах на C#, но ради удовольствия знакомился с F#.
Мой доклад назывался так: «Playing functional Conway's game of life», то есть реализация игры Конвея «Жизнь» методом функционального программирования. Достаточно известная игра. Я показал, как написать её на F#, сделал и, начав разбираться, сам удивился. Надо сказать, перед этим я нашел проект реализации этой игры на C# на CodeProject. Этот проект состоял из пяти классов, пяти свойств и методов, там было больше 300 строк кода, при этом эффективного. Даже если убрать все скобки, оставалось около 100 строк. Когда я написал «Жизнь» на F#, у меня получилось исполняемого кода 14 строк: семь функций в среднем по две строки (если вы думаете, что это предел — посмотрите код английского программиста Фила Трелфолда, который уместил решение игры Конвея на F# в 140 символов и выложил в твиттер). Компактность разработки на F# меня потрясла. Это первое, что меня впечатлило. Затем я начал рассматривать свой код. Я стал думать, а где вообще в этом коде говорится о том, что решение сделано для двумерной доски? Я обнаружил, что лишь одна функция, которая вычисляет соседей клетки, говорит о том, что это 2D доска. Если эту функцию заменить на работу с трехмерной или даже многомерной доской, то все будет также работать.
Во многих функциональных языках, в частности в F#, существует так называемое внедрение типов: когда вы не задаете напрямую тип ваших данных, а компилятор, в зависимости о того как вы их используете, сам определяет что подставить. Благодаря этому вы пишете сразу обобщенный код. Если в Java или C# нужно специально идти к тому, чтобы обобщать ваши классы, то в F# это получается по-умолчанию. Это дает очень большие преимущества, в чем я смог убедится лично, работая над различными проектами.
Когда я в итоге приготовил свой доклад и выступал с ним на конференциях, то обращался к залу и спрашивал, с чего следует начать написание игры Конвея. Практически все предлагали с определения классов и свойств, например: надо ввести класс «Доска», «Клетка», определить свойства «Клетки». То есть все привыкли к тому, что нужно начать с определения типов и их взаимосвязей. Мы так приучены. Но на самом деле введение заранее такого большого количества определений накладывает существенное ограничение на дальнейшую работу с ними. Третий важный аспект в реализации игры «Жизнь» на F# было то, что там я не ввел ни одного типа. Все делалось путем задания функций. Это дает полную свободу того, как «играться» с исходными данными. Я осознал, что при ООП мы заранее «заставляем» себя работать в рамках тех определений, которые ввели.
Подходящий для описаний этой ситуации твит я нашел, когда работал с презентацией своего доклада. Какой-то англоязычный программист написал: «программировать на Java — все равно что заниматься русской литературой: вам нужно определить сотню имен, прежде чем начнут происходить какие-то события». Этот комментарий неплохо определяет подход ООП. Мы должны изначально хорошо все описать, и только потом начнут происходить какие-то события, а мы сможем на наши определения «нанизывать» какие-то методы. И, зачастую, наш дизайн уже нас ограничивает.
Возвращаясь к первоначальному вопросу, надо сказать, что именно попытка уйти от мутирующих данных стала отправной точкой для меня в мир функционального программирования.
В Java и C# слишком много «церемоний»
— На ваш взгляд, следует ли человеку, который долгое время занимается объектно-ориентированным программированием, знакомиться/переходить на функциональное?
Вагиф Абилов: Вопрос полного перехода — это вопрос довольно прагматичный. А знакомиться, да, конечно же стоит.
Если мы посмотрим на объектно-ориентированные языки типа Java или C#, то они претерпели достаточно большие изменения в последние годы. Если я ничего не путаю, в C# версии 3.0, когда появился LINQ, появились лямбда-выражения, это был уже заметный ход в сторону внедрения элементов функционального программирования.
Возникает такой аргумент: «а зачем мне изучать сами функциональные языки, если мы многое можем сделать и в C# с элементами функциональных языков?». По крайней мере один из ответов на это уходит в область изменяемости структур данных, поскольку и C#, и Java всегда останутся языками с мутациями. Когда данные, которые вы определяете по-умолчанию, доступны для изменении, то, какие бы элементы функционального программирования вы не вносили, принципиальной сущности этих языков это не изменит. В последних версиях C# вы можете «играться» с элементами ФП, но, конечно же, имеет смысл попробовать поработать с настоящим функциональным языком, таким как Erlang, Haskel или F#. Последний я бы особенно рекомендовал, поскольку это язык очень хорошо встраивается в .NET. Достаточно разобрать какие-то примеры, посмотреть насколько лаконичным получается код. Это, на мой взгляд, серьезный аргумент — компактность кода. Чем более опытен программист, тем больше он должен осознавать, что в таких языках как Java и C# слишком много «церемоний». Избежать их можно если уменьшить код вдвое, потому что обычно программы на F# вдвое компактнее, чем на C#.
— Какие преимущества дает ФП по сравнению с ООП?
Вагиф Абилов: Первое, как я уже сказал, это отсутствие мутации данных — это очень важно. В программах, которые пишутся на функциональном языке, нет переменных. В каком-то смысле они сразу получаются «жесткими».
Если вы посмотрите на объектно-ориентированный код, там будут какие-то переменные, какие-то данные, потом их куда-то посылают, и ко всему этому осуществляется доступ из многих потоков. В функциональных языках, поначалу, это немного сбивает с толку: как вообще можно работать, не вводя переменные? Но все реализуется с помощью методов «функциональных трансформаций». Это как раз и создает основы параллелизма. Когда вы получаете какие-то данные, вам не нужно отвечать на вопрос: «это thread-safe или не thread-safe? Переживет ли это доступ из многих потоков или нет?». Вы по определению знаете, что переживет. Вам даже не надо делать никаких тестов для проверки с доступом из многих потоков.
В связи с отсутствием переменных и с тем, что вы все пропускаете через функциональные трансформации, очень упрощаются тесты. Как следствие, логические ошибки гораздо чаще отлавливаются компилятором. Одна из «хороших» проблем при работе с F# такая: я могу несколько часов потратить просто на то, чтобы программа скомпилировалась, но когда это произойдет, она будет работать без ошибок. Это настолько «убаюкивает», что начинаешь меньше писать тесты. Вначале я пытался с этим бороться и писал также много тестов, как на C#. Потом я понял, что в этом нет необходимости, так как большинство логических ошибок отлавливается компилятором, который гораздо менее «прощающий», чем компиляторы объектно-ориентированных языков.
Пожалуй, это и есть основные преимущества: параллелизм, отсутствие мутаций, большее взаимодействие с компилятором, который восприимчив к логическим ошибкам. Кроме этого меняется стиль работы. Если я работаю на C#, то часто использую классическую TDD методологию. С F# я работаю в режиме REPL (read-eval-print loop). Такая работа очень эффективна.
— Есть ли что-нибудь, что не под силу ФП в сравнении с ООП? Какие у него (ФП) недостатки?
Вагиф Абилов: Для каждой задачи должны применяться свои средства. Что касается преимущества функциональных языков при разработке масштабируемых систем с большим быстродействием это понятно, общеизвестно. Но для меня не очевидны преимущества функционального программирования при работе с визуальными интерфейсами. Если у вас программа однопоточная и сводится к редактированию форм, то, в общем-то, здесь будет естественно применять объектно-ориентированный подход, так как формы легче ложатся на модели данных. F#, Clojure, Erlang используют и для разработки user-interface, но преимущества мне кажутся неочевидными.
Еще можно сказать, что, обращаясь к функциональным языкам, разработчик может решить, что проблемы параллелизма и быстродействия решатся сами собой, но это никак не заменяет анализа проблем, влияющих на быстродействие. Например, разработчику нужно думать об этом, если он работает с многоядерными процессорами. Программу нужно писать так, чтобы она пользовалась преимуществами кэша процессора. Производительность можно потерять из-за того, что кэш будет постоянно обновляться. Это, вообще говоря, задача, которая не имеет никакого отношения ни к функциональному программированию, ни к объектно-ориентированному. В любом случае, при разработке быстрых масштабируемых проектов необходимо понимать внутреннюю архитектуру систем, на которых они будут работать. Другими словами, это не «серебряная пуля», нельзя рассчитывать, что обращение к функциональному языку сразу решит все эти проблемы.
— Если обобщить, на решение каких задач ориентировано ФП?
Вагиф Абилов: На решение задач, которые требуют параллелизма, высокого быстродействия. В общем-то весь back-end успешно может писаться на функциональных языках.
— Вагиф, какую инфраструктуру (tooling) необходимо собрать для реализации проекта на языке ФП?
Вагиф Абилов: Поскольку я работаю с C# и F#, то для меня Visual Studio — это наиболее часто используемый инструмент. Но, все чаще я замечаю, что пользуюсь другими, менее «тяжелыми», средствами. Например, что касается языка F#, если речь идет о .NET-разработке, то есть Visual Studio Code с плагином Ionide. Это потрясающая среда для работы с F#.
Я бы порекомендовал использовать такие редакторы, как Atom + Ionide, VS Code + Ionide, Paket, Fake. Для тестов есть F#-дружественные фреймворки: FsUnit и библиотека Expecto, которая очень хорошо встраивается в работу с функциональными языками. И вот буквально на днях появилась информация, что новый IDE JetBrains Rider, который пока находится в бете, будет поддерживать F#. Это примечательное событие, поскольку JetBrains — вообще практичные ребята, и они долго отнекивались, когда их спрашивали о поддержке F#, мотивируя это сложностью встраивания принципов языка в платформу Resharper (как я понимаю, сложности относятся к внедрению типов, чего нет в объектно-ориентированных языках). Но лед тронулся, F# стал слишком важным в среде .NET языком, чтобы его можно было и дальше игнорировать.
Если вам необходимо написать веб-приложение, то есть замечательный фреймворк Suave. Он позволяет очень компактно, буквально в несколько строк, написать веб-аппликацию или веб-сервис. Если же говорить о реализации микросервисов, то очень хорошо функциональные языки работают вместе с моделью актеров (Actor-model). Я последние полтора года занимаюсь разработкой системы на F# с использованием Akka.Net, в которой эта модель реализована.
Кроме всего прочего, важными составляющими будут провайдеры типов (type providers), которые на F# реализованы и позволяют очень эффективно работать с базами данных. Они заменяют такие тяжеловесные библиотеки, как Entity Framework.
Кстати, интересный пример. Есть на F# open-source библиотека SQLProvider, которая необычна тем, что включает в один модуль сразу семь драйверов: MSSQL, Oracle, SQLite, PostgreSQL, MySQL, MsAccess, ODBC. И все это весит лишь 1,3 мегабайта. И драйвер каждой из баз данных составляет примерно от 600 до 800 строк кода. Это, к слову, о том, насколько компактно можно писать многие вещи на F#.
— Если ли на вашем личном счету большие и серьезные проекты, реализованные с помощью функционального программирования?
Вагиф Абилов: Да. Небольшой группой последние полтора года на F# с помощью Akka.Net мы пишем систему, которая имеет высокие требования к быстродействию и масштабированию. Эта система разрабатывается для норвежского государственного радио-телевидения. Она оперирует многими сотнями терабайт файлов, работая с облаком. Код получается очень компактный, несмотря на сложность системы.
— Как вы считаете, станет ли ФП популярным настолько, чтобы конкурировать с ООП?
Вагиф Абилов: Что касается конкуренции, то уже сейчас функциональное программирование успешно конкурирует с объектно-ориентированным и во многих проектах заменяет его. Если же говорить о количественном сравнении, то надо понять, о какой временной перспективе мы говорим. Наверно, в ближайшие пять лет ФП не выйдет на сравнимое с ООП количество проектов по разным причинам. Одна из них заключается в том, что начинать обучение программированию гораздо легче с объектно-ориентированных языков. Плюс, имеется большое количество задач с пользовательским интерфейсом, где, как я уже сказал, преимущества ФП не очевидны.
Мне кажется, что большие масштабируемые системы все больше и больше будут писаться на функциональных языках. Одна из причин этого в том, что перестает работать закон Мура. Если раньше можно было просто дождаться, когда выйдут более мощные процессоры, и все само по себе станет быстрее, то сейчас этого делать нельзя. Нужно переделывать архитектуры под имеющееся быстродействие, зная и имея в виду то, что оно не увеличится. Это очень большой козырь в пользу функционального программирования.
— Что вы можете посоветовать тем, кто решил начать изучать функциональное программирование?
Вагиф Абилов: Посоветовал бы не относиться к этому выбору как к какому-то серьезному жизненному шагу. Я заметил, что пробовать изменить основной язык у программистов считается каким-то радикальным шагом в отличие, например, от смены базы данных или какого-то фреймворка. Например, разработчики на javascript меняют библиотеки, которыми они пользуются, как перчатки. И это не выглядит каким-то серьезным изменением. Если вы перешли с реляционной базы данных на document-базу, это во многом более серьезный шаг, чем перейти с одного .NET языка на другой.
Мне довелось однажды поговорить с ребятами, которые писали систему для одного из заказчиков на F#. Я спросил, насколько было легко убедить заказчика, что вы будете делать проект на F#. Они сказали, что заказчику об этом не говорили. В контракте было написано, что система должна работать под .NET. В этом подходе, на самом деле, что-то есть. Если вы пишете для данной операционной среды, то мой совет: как можно активнее и больше пробовать. Пробовать другие языки, библиотеки и модели программирования. От всего этого будет только польза.
— О чем будет ваш доклад на питерской конференции DotNext в мае?
Вагиф Абилов: Нынешний мой доклад не будет иметь непосредственного отношения к функциональному программированию, но он будет, в каком-то смысле, связан со сменой парадигм. Я буду рассказывать о том, как разработчику сделать API таким образом, чтобы он был в равной степени легко используемым как теми, кто предпочитает типизированное программирование, так и теми, кто использует программирование с динамическими типами. Как известно, с появлением .NET 4.0, появилась возможность встраивать динамические типы в C# (тип dynamic). В каком-то смысле я буду говорить о том, что нужно быть готовым к смене парадигмы. И это роднит мой доклад с темой нашего сегодняшнего разговора.
Полностью тема доклада Вагифа Абилова, который будет выступать на нашей конференции DotNext в Питере 20 мая, звучит как Typed or dynamic API? Both! Мы будем рады видеть вас на этом мероприятии, где нам удалось собрать около 30 замечательных докладчиков из самых разных уголков мира и по самым актуальным темам.
Автор: sinnerspinner