Вагиф Абилов — разработчик в норвежской компании 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 замечательных докладчиков из самых разных уголков мира и по самым актуальным темам.
Комментарии (83)
AlexanderByndyu
05.05.2017 13:24+3Похоже пора расчехлить свой pet project на F#. Спасибо за интервью, вдохновляет шагнуть в функциональное программирование.
vadim_ig
05.05.2017 13:48-2Я правильно понял, что на данный момент из реально завершенных «фунциональных» проектов у автора только «Жизнь» размером в 14 строк?
EngineerSpock
05.05.2017 14:05-1Вы до конца дочитали?
Небольшой группой последние полтора года на F# с помощью Akka.Net мы пишем систему, которая имеет высокие требования к быстродействию и масштабированию. Эта система разрабатывается для норвежского государственного радио-телевидения. Она оперирует многими сотнями терабайт файлов, работая с облаком. Код получается очень компактный, несмотря на сложность системы.
KirillGuzenko
05.05.2017 14:29+7из реально завершенных «фунциональных» проектов
последние полтора года на F# с помощью Akka.Net мы пишем систему
Пишут, а значит пока не написали, а значит, согласно статье, у автора нет законченных проектов на функциональных языках, кроме игры «Жизнь».
masai
05.05.2017 16:17+19Пишут, а значит пока не написали
Выходит, у Торвальдса, например, тоже нет законченных крупных проектов на Си.
artemt
05.05.2017 17:02+1Это значит написали, работет и продолжают развивать. Вот ссылка на доклад Вагифа "Опыт внедрения актёров на F#"
https://www.youtube.com/watch?v=wRxO5ky7S8g
vadim_ig
05.05.2017 16:16-5Конечно дочитал. Сам факт того, что они полтора года что-то пишут, ни о чем не говорит. Без демонстрации доведенных до ума продуктов все звучит жутко неубедительно.
Szer
05.05.2017 17:02+1Вагиф на прошлом Московском DotNext рассказывал о своём проекте:
https://www.youtube.com/watch?v=wRxO5ky7S8g
VagifAbilov
05.05.2017 17:02+7Спасибо за интересный вопрос :-) Нет, вы неправильно поняли. На проект на F# для норвежского телевидения уже скоро год как в продакшн, сейчас готовится вторая фаза. Считать ли такой проект завершенным — это уже вопрос точки зрения. Но это 100% функциональный проект, который заставил многое пересмотреть. Основная причина, по которой в этой беседе я ссылался на довольно простую программу — при всей ее простоте на ней очень многое вскрывается, по крайней мере вскрылось для меня. К тому же она всем знакома. Ну и к тому же когда принимаешь для себя решение осваивать новую парадигму, то по определению нет завершенных проектов, надо на основании ограниченного опыта решать, в какую сторону двигаться.
jetcar
05.05.2017 14:41+2Вначале я пытался с этим бороться и писал также много тестов, как на C#. Потом я понял, что в этом нет необходимости, так как большинство логических ошибок отлавливается компилятором, который гораздо менее «прощающий», чем компиляторы объектно-ориентированных языков.
с чего это? компилятор знает как бизнес логика работает?iZevs
05.05.2017 15:43+4Если использовать domain driven design то будет знать
http://fsharpforfunandprofit.com/ddd/
VagifAbilov
05.05.2017 17:05+2Многое ловится, если использовать discriminated union, особенно если включить опцию warnings as errors. Это вообще очень полезная категория типов для описания предметной области. Очень помогает и что тип record требует задания значений всех полей при декларации данных. Нет нулей, на NullReferenceException наталкиваешься лишь когда принимаешь данные из C#. И еще что существенно сказывается — нет никаких переменных, стараешься писать все в виде функциональных трансформаций, получается меньше, так сказать, движущихся частей.
При этом я мгного лет был в каком-то смысле TDD-junkie, старался писать тест на все что угодно.
GreenBee
05.05.2017 15:22+6Мне казалось, что я уже много подобных статей прочитал. Но впервые, я увидел для себя реальные причины начать изучать F#. То ли в этой статье написали что-то, чего не было в других, то ли в других было не так понятно.
kogoia
05.05.2017 16:00+3Если вы посмотрите на объектно-ориентированный код, там будут какие-то переменные, какие-то данные, потом их куда-то посылают, и ко всему этому осуществляется доступ из многих потоков.
По моему это не про ООП, а о том, как многие его неправильно используют. :)Shablonarium
06.05.2017 02:21+1Да, это не относится ни к ООП, ни к его использованию, это про императивный подход к программированию.
vasIvas
05.05.2017 16:00+1Я для себя взял за правило не слушать тех кто пытается тебя в чем-то убедить. Нормальный доктор никогда не скажет кушать только фрукты потому что они витамины. ООП там где оно нужно, это сила. ФП на своем месте — наверное тоже. Игры делают не на ооп и не на фп, а на нечто среднем. Так же личный опыт подсказывает что разные программы требуют разные подходы. Когда я пишу ui я радуюсь mvvm, когда сервер mvp, когда простую игру, я наслаждаюсь mvc, когда большую игру ecs, когда я пытаюсь связать части архитектуры, я просто тащусь от реактивных библиотек. То место где мне нужна иммутабельность, я использую иммутабельност.ь. Но так же я знаю что везде иммутабельность просто не нужна, а если пытаться использовать архитектуру не там где положено-парадигму, а лишь из-за того что это модно, то это начала серии статей о том что все это ужас.
Но самое непонятное мне, это то, что лично я бы не стал писать 20 лет на том что мне не нравится.VagifAbilov
05.05.2017 17:06«Но самое непонятное мне, это то, что лично я бы не стал писать 20 лет на том что мне не нравится.»
Если из моих слов сложилось такое впечатление, значит я совсем туманно выражаюсь.vasIvas
05.05.2017 17:28Если из моих слов сложилось такое впечатление, значит я совсем туманно выражаюсь.
нам смог ответить человек, открывший для себя преимущества функционального подхода после 20 лет ООП-разработки
Это разве не означает что 20лет Вы писали на убогом-неудобном и неправильном подходе?
И ещё вопрос — функциональные библиотеки которые Вы используете, так же не имеют переменных и не хранят состояние свойствах?VagifAbilov
05.05.2017 17:39Нет, не означает, конечно. Я и сейчас немало пишу на C#, переходил на него с Си++ с удовольствием. Что же до чужих библиотек, то речь идет непосредственно о коде, который пишем мы сами, о его прочности, о пригодности к параллелизации. Сопровождать же нужно будет свой домашний код, а не другие библиотеки.
vintage
05.05.2017 20:02+1В ООП проблема shared-state решается просто — его отсутствием: https://en.wikipedia.org/wiki/Thread-local_storage
Mishkun
05.05.2017 20:34+1Если вы переживаете по поводу производительности и иммутабельности — в f# достаточно средств оптимизации и можно втыкать mutable и кусочки ООП туда, где вам нужно.
Когда я попробовал f#, посмотрев лекцию VagifAbilov об экторах, я понял, что ФП — это именно то, как я стремился писать программы. И если в ООП языках я иногда чувствовал сопротивление инструмента, то в случае с f# все "как ножом по маслу".
ФП сейчас конечно форсится жутко, но это не делает парадигму плохой.
NaHCO3
06.05.2017 04:54-1Согласен с вами. Все парадигмы программирования уместны в некотором контексте. То же самое функциональное программирование основано на вполне императивных интерпретаторах. Императивный код отвечает за работу ThreadPool, за доставку сообщений, за синхронизацию очереди сообщений. И только поверх всего этого растёт функциональное параллельное программирование.
Адепты концепций высокого уровня любят высокомерно бросать «ну а если вам нужна производительность — можно подключить си библиотеку». Можно подумать пользователям не нужна производительность. Или что отдельная библиотека — это не часть программы. Существенная часть программы, написанная в другой парадигме. И это даже не наводит их ни на какие мысли. Слишком много развелось узкоспециализированных программистов, не только не способных построить с нуля всю программную систему, но даже не представляющих как она устроена. Отсюда и утверждения, что только их болото достойно внимания.
Опять же, можно подумать, что параллельное ФП по своей сути отличается от процедурного и магически решает все проблемы. Никакой магии нет. За гарантии работы программа платит скоростью работы. Параллельное программирование — это такая сложная тема не потому, что сложно написать корректную программу (хотя это тоже удаётся не всем), а потому что невероятно написать оптимизированную, но всё ещё корректную программу. Выкинуть все проверки и остановки, без которых можно обойтись, использовать разные виды локов с пониманием ньюансов их использования. Простота ФП параллелизма лежит в том, что вместо десятков разных параллельных примитивов, точное использование которых позволяет снизить издержки, у нас остаётся два-три железобетонных, и если твоя область применения порождает издержки, то тебе придётся их терпеть.
Вот как магически можно решить проблему общего состояния в игре? Каждый актор должен знать состояния всех соседей и поля боя в целом. Можно использовать локи, а можно копить кучу копий и обеспечивать синхронизацию этих копий. И никак нельзя облегчить задачу — объединить несколько мутаторов в общий логическую систему и разбить обновление этой системы на фазы, внутри некоторых из которых можно ослабить гарантии пареллельного доступа. Что-то наподобие идемпотентного оператора, только более сложное и в масштабах системы, а не отдельного кусочка данных.0xd34df00d
08.05.2017 19:58+2За гарантии работы программа платит скоростью работы.
Почему вы считаете, что это истинно всегда?
Выкинуть все проверки и остановки, без которых можно обойтись, использовать разные виды локов с пониманием ньюансов их использования.
Кто мешает это сделать достаточно умному компилятору?
NaHCO3
14.05.2017 05:21> Почему вы считаете, что это истинно всегда?
Потому что все низковисящие фрукты уже собрали. И уж точно это верно в случае так называемого параллельного функционального программирования
> Кто мешает это сделать достаточно умному компилятору?
Таких компилятов ещё не написали. И напишут не скоро.
CrazyFizik
05.05.2017 17:16Как там обстоит дело со скоростью например при работе с охрененно гигантскими матрицами? Ну там крутить-вертеть их туда-сюда, перемножать и т.д. и паралельно крутить мозгокрутные конечные автоматы, ну и так далее?
telhin
05.05.2017 17:36На счет F# не знаю, но в Haskell вроде как есть возможность подключать высокопроизводительный код на c++. Аналогичную черную магию практикует питон. Из тех бенчмарков что я видел производительность хуже на 0-10%.
Так же есть так называемые «Небезопасные» функции для работы напрямую с памятью, думаю будет примерно то же самое.
P.s. Сам не имею опыта в таких вещах.0xd34df00d
06.05.2017 02:41Да, в хаскеле каноничная hmatrix дергает всякие gsl, blas, lapack и тому подобные вещи.
VagifAbilov
05.05.2017 17:46Сразу оговорюсь, что опытом кручения гигантских матриц не обладаю, но судя по разным бенчмаркам, при особых требованиях к быстродействию F# уступит не только Си++, но и некоторые его модули (List, Map) проиграют по скорости структурам данных из .NET/C#. Если не ошибаюсь, Ayende (Oren Eni) делал сравнения, писал в своем блоге.
В более традиционных системах, однако, закон Амдала, указывающий на потери быстродействия залоченных данных, имхо, делает код с замками в многопоточной среде более уязвимым в плане быстродействия, чем функциональные алгоритмы без блокировки.
dimaaan
05.05.2017 17:27Так же не понятно, как реализовать кеширование без разделяемого состояния.
Отдельный кэш для каждого потока?
При обновлении кэша пересоздавать его заново?VagifAbilov
05.05.2017 17:55-1Думаю, обобщенный ответ на этот вопрос лежит вне парадигмы программирования. Хотя в рамках некоторых моделей есть свои решения. Например, мы пользуемся моделей актеров — она доступна и из ООП, и из ФП, но поскольку актеры (или акторы, тоже не уверен, как лучше по-русски выразиться) не делят состояния, то по духу ФП ближе, не даром Akka пришла из Скалы. Так вот, в рамках этой модели у нас некоторые актеры хранят разделяемое состояние, доступ к котрому осуществляется путем обмена с ними сообщениями. Это может выглядеть тяжеловесно, позволяет полностью обходиться без замков.
senpay
05.05.2017 17:43+3— Что вы можете посоветовать тем, кто решил начать изучать функциональное программирование?
Вагиф Абилов: Посоветовал бы не относиться к этому выбору как к какому-то серьезному жизненному шагу. Я заметил, что пробовать изменить основной язык у программистов считается каким-то радикальным шагом в отличие, например, от смены базы данных или какого-то фреймворка. Например, разработчики на javascript меняют библиотеки, которыми они пользуются, как перчатки. И это не выглядит каким-то серьезным изменением. Если вы перешли с реляционной базы данных на document-базу, это во многом более серьезный шаг, чем перейти с одного .NET языка на другой.
Отлично сказано!
VolCh
05.05.2017 18:00+3Вообще говоря, если ориентироваться на доминирующую архитектуру процессоров, то она императивна и мутабельна :)
А вообще делал несколько попыток освоить языки, позиционирующиеся как функциональные прежде всего, но всегда спотыкался на моменте, когда становится нужна мутабельность, ввод/вывод и прочие сайд-эффекты — красивые концепции резко становятся сложными.
VagifAbilov
05.05.2017 18:21В прошлом году Джо Армстронг (Erlang) делал доклад на NDC о принципах, заложенных в язык, и он как раз упирал на то, что в основе успеха Эрланга у телеоператоров и прочих систем, требующих масштабируемости и надежности — соответствие Эрланга (функционального языка) железу, на котором будут запускаться программы на нем. «We do not have ONE web-server handling 2 millions sessions. We have 2 million webservers handling one session each.» — сказал он же в другом месте. То есть несмотря на императивность и мутабельность архитектуры процессоров процессы на них имеет смысл запускать функциональные и неизменяемые.
Sirikid
06.05.2017 03:18Кстати, многие считают Erlang как раз ООЯ, а не ФЯ, и трактуют акторы-процессы как объекты, а обмен сообщениям как… обмен сообщениями.
madkite
05.05.2017 18:49Вообще говоря, если смотреть на доминирующую архитектуру процессоров, то она нынче мультиядерная. А распараллеливаются, как раз, лучше "чистые" функции, которые используют только иммутабельные данные.
VolCh
05.05.2017 18:55+1Так или иначе эти чистые функции изменяют состояние системы — ОЗУ и дисков, даже если про регистры не вспоминать. И делают это в императивной мутабельной манере, просто транслятор и(или) рантайм языка это скрывает.
Neftedollar
05.05.2017 19:20+1Не стоит все доводить до крайнестей. Очеидно тут говорится об одном уровне абстракции, а вы опускаетесь на другой.
roman_kashitsyn
05.05.2017 18:59+1Третий важный аспект в реализации игры «Жизнь» на F# было то, что там я не ввел ни одного типа.
Это какое-то неправильное функциональное программирование. "Правильное" современное ФП обычно начинается с типов и управляется типами.
VagifAbilov
05.05.2017 19:10Это верное замечение, мне следовало лучше обговорить, что я имею в виду под важностью аспекта. ФП действительно управляется типами, но благодаря type inference их часто можно не вводить для промежуточных вычислений. Partial application тоже помогает на них не сосредоточиваться. Но ФП именно что начинается с типов — дальше они пускаются в плавание по трансформациями. ООП и начинается, и продолжается, и заканчивается типами, собственно парадигма такая, от нее не убежать. И это приводит к тому, что в реальной практике мы часто слишком многое определяем заранее, связывая свой дизайн.
NaHCO3
05.05.2017 19:49+1> Первое, как я уже сказал, это отсутствие мутации данных — это очень важно.
Конечно важно. От этого асимптотика страдает. Частенько и коэффициенты при О-большом тоже.
LmTinyToon
05.05.2017 20:18Если честно, то немного не понимаю как функциональное программирование связано с быстродействием? (Пока вижу один плюс — простое распараллеливание участков кода — за счет отсутствия сайд эффектов) Насчет дизайна для меня очень спорно, наш мир не идеален, и работать в контексте персистентных структур очень сложно. Простой пример — работа с графами, мне сложно себе представить, как можно эффективно с точки зрения памяти и времени работы алгоритма работать с такими объектами (если учитывать, что это персистентная структура). Возможно императивный подход уже наложил сильный отпечаток, и это все предубеждения.
unabl4
05.05.2017 20:49Как мне кажется, связь скорее всего связь отрицательная. Иммутабельность существующих объектов = постоянное создание новых, что врядли может положительно сказаться на производительности.
sshikov
05.05.2017 20:59Если вы думаете, что нужно обязательно создать полную копию структуры (графа), чтобы внести небольшое изменение — вы ошибаетесь. Вот как-то так в двух словах. Совсем не хочу при этом сказать, что эти алгоритмы простые — скорее наоборот, они сложные (классическая книжка Окасаки — это просто идеальный способ сломать мозги). Но с точки зрения памяти и времени не все так мрачно.
sotnikdv
06.05.2017 01:21> Если честно, то немного не понимаю как функциональное программирование связано с быстродействием?
Напрямую — никак. Косвенно — через легкую масштабируемость и параллелизм, который достигается не руками, а языком, в обмен на определенный оверхед и лимиты, накладываемые парадигмой.
Реанимация ФП произошла когда мы уперлись в пределы вертикальной масштабируемости и начали активно осваивать горизонтальную.
И оказалось, что проще дать +10% нод на оверхед парадигмы и получить scalability factor ~=1 из коробки, чем без оверхеда иметь scalability factor 0.4 и дикий гемморой и баги на реализацию совместного доступа в других парадигмах.
фп, как и nosql оооочень старые подходы
Nakosika
05.05.2017 20:57… а у меня на ассемблере около 70 строк занимает. Как можно на С# напилить больше я не понимаю, может там фреймворк какой? По ООП языкам в принципе Оккам плачет, может из-за этого.
sotnikdv
06.05.2017 01:16+3«обычно программы на F# вдвое компактнее, чем на C#»
Какая-то феерическая глупость, которую адепты новых языков таскают друг у друга и никак не закопают в тихом месте.
Компактность исходного кода в символах — это какой-то фетиш, ценный только для абсолютно оторванных от жизни людей. И зачастую это приводит к абсолютно нечитаемым конструкциям из букв и символов, которые зато компактны.
Исходник должен быть компактным не в смысле количества слов и символов, а в смысле лаконичности при сохранении читаемости без сложностей и ошибок понимания логики. Об этом блин почему-то забывают.
Поэтому аргумент типа «код получается более читаемым и менее многословным, где программист фокусируется на бизнес-логике, а не контроле доступа к данным и блокировках и многие вещи реализуются проше» — это круть несусветная.
А код получается компактным — это обычно псевдосимвольная графика в коде, которая не читается.VagifAbilov
06.05.2017 11:42+1Ну вот возьмите async/await. Вроде бы это всего лишь синтактический сахар. Но реально уменьшает количество кода, а раз так, то и шанс сделать логическую ошибку. Я себя причисляю к той части программистов, которая чем больше пишет, тем больше ошибается, для таких, как я, компактность кода — это благо.
VagifAbilov
06.05.2017 11:47+1Если же брать примеры непосредственно из ФП, то какой-нибудь List.fold поначалу может и требует задуматься, что же там внутри происходит. Но пройдя через это и поняв, выходишь на иной уровень абстракции, выигрывая на нем в компактности.
Nakosika
07.05.2017 00:35Чем короче код тем проще его понять (при условии что это не просто переменные названы короче а сам алгоритм проще). В случае F# он проще за счет того что для того чтобы сделать одно и то же нужно как раз таки меньше логики описывать.
Сравните хелло ворлд хотя бы. На F# это одна строчка кода, на C# тебе и классы и полиморфизм, и аргументы из командной строки, и статик и синглтон — все в каше.NaHCO3
07.05.2017 01:38+1> (при условии что это не просто переменные названы короче а сам алгоритм проще).
> Сравните хелло ворлд хотя бы. На F# это одна строчка кода, на C# тебе и классы и полиморфизм, и аргументы из командной строки, и статик и синглтон — все в каше.
Так вы определитесь, алгоритм разный или строчки? Алгоритм-то hello world один и тот же во всех случаях.VolCh
07.05.2017 17:25Формально подходя, вроде к ФП понятие алгоритм в целом не применимо, алгоритм — описание шагов в определенном порядке, а ФП не задает порядок вычислений в функциях.
sshikov
08.05.2017 20:13Вы преувеличиваете. Он не задается, если вычисление не зависит от результата другой функции. Тогда можно в любом порядке, параллельно и т.п. А иначе — извините, есть зависимость по данным, извольте соблюдать.
VolCh
09.05.2017 14:14Порядок не задаётся программистом явно всё равно, он определяется по ходу дела в процессе определения того, что нужно сделать, чтобы получить результат по декларативным правилам заданным программистом. Программист пишет "чтобы получить результат функции A нужно применить такую-то трансформацию к результату функции B". Это правило не задаёт момент когда реально выполнится вычисление функции B, единственное что можно сказать, что вычисление функции A не закончится позже вычисления функции A, если результат необходим для этого вычисления. С натяжкой, конечно, можно назвать это порядком, но не в том смысле, в котором он используется в алгоритмах.
qw1
09.05.2017 14:25Функциональное программирование полно по Тьюрингу, а значит, умеет представлять алгоритмы.
Функции в ФП мощнее тех, которыми пользуются в математике. Тут главное — функция выбора
IfThen( condition, trueValue, falseValue )
В математике, при комбинировании функций, значение функции не определено, пока не определены все аргументы. У функцииIfThen
в ФП фундаментальное отличие — еслиcondition==true
, то значениеfalseValue
может быть не определено (а может и быть определено). На этом строится и рекурсия (условие выхода из рекурсии), отсюда и проблемы с вычислимостью (аналог проблемы остановки), это и даёт возможность записывать любые алгоритмы.Sirikid
09.05.2017 17:39Такие выкрутасы прощают только ленивые языки, в энергичных придется эмулировать ленивость самому.
qw1
10.05.2017 00:09Речь о том, что в ФП есть специальные функции, не вычисляющие все аргументы.
Работающие, как тернарный оператор(condition? value1 : value2)
в C/C#.
Взять классический не ленивый LISP, с его функцией (cond) — там независимо от ленивости вычислена будет только одна ветка.
qw1
07.05.2017 13:37+1Помните BASIC?
10 PRINT "Hello, World"
Тут helloworld так короткий, потому что BASIC — язык для начинающих (согласно расшифровке его аббревиатуры), т.е. язык для helloworld-ов. Дизайн языка говорит новичкам: смотрите, как просто — 1 строка и программа работает!
С другой стороны, если посмотреть на алгоритмы архивации, то доказано, что нет универсального — такого, который бы обошёл все алгоритмы на любых данных. Всегда найдётся лучше архиватор под специфические данные. С языками то же самое.
Gradarius
08.05.2017 16:05А мне показалось убедительно, конечно не так, что бы все бросить и бежать, но серьезно изучить вопрос и начать осваивать — почему бы и нет? Спасибо за статью, правда хотелось бы больше примеров и сравнений, но можно и самому поискать
lromanov
10.05.2017 10:31+1Вагиф, ФП очень даже востребовано в программировании интерфейсов, форм и прочего. Если вам интересен функциональный подход к написанию клиентских приложений, то очень интересный в этом плане язык — Elm http://elm-lang.org
Это, можно сказать, наследник Haskell для написания сложных frontend web-приложений, но при этом очень простой в освоении. Чистые функции, отсутствие null reference exceptions и вообще исключений в runtime для сложных SPA, где владычествует великий и ужасный js, — согласитесь, заманчиво
vlad_iv
10.05.2017 10:31Вагиф, поясните, если «правильно» писать на ОПП — 1. Вместо мутабл — неизменяемое состояние
2. Вместо shared state — инкапсулировать состояние
и т.д…
То какие преимущества у ФП, кроме краткости, которое часто сомнительно?
Раскройте. за счет чего выше производительность при ФП, будет, если расход памяти больше на порядок (ведь состояние неизменяемое)? Как удается быстро освобождать неиспользуемую занятую память?VagifAbilov
11.05.2017 17:46+2Влад, так в том-то и дело, что такой стиль написания на ОПП не считается однозначно правильным. Я согласен, что не надо очень напирать на краткость кода на ФП, это в конце концов не самоцель, хоть и полезно, но церемониальность многих языков ООП не связана с ООП как таковым, и при желании их тоже можно сделать более компактными. Вот здесь — набор слайдов, где предлагается синтакс языка C# Light, при использовании которого код становится сравнимым по компактности с фшарпом:
https://www.slideshare.net/ScottWlaschin/c-light
Но если брать другие, более важные моменты, то основная проблема языков ООП — это то, что они навсегда останутся языками мутирующих данных. Мы можем требовать от разработчиков команды высокой дисциплины, но нельзя заменить основы языка дисциплиной. Вот пересказ отзыва создателя Clojure Рич Хики о проблемах мутаций в ООП:
«It should also be obvious that OO languages have „no proper notion“ of values in this sense. As Rich Hickey points out, you can create a class whose instances are composed of immutable components, but there is no high-level concept of immutable value implemented as a first class construct within the class.
This is one of the main causes of headaches when doing OOP. How many times have you pulled your hair out trying to figure out how an object's attribute got changed? The fact is, in OO languages there is no built-in mechanism to ensure that the object you're dealing with is stable.
This is the big reason why concurrent programming is so difficult.»
Безусловно, современные C# и Java куда более пригодны для функционального стиля программирования, чем пятнадцать лет назад. Но такой стиль всегда будет оставаться для этих языков если не инородным, то по крайней мере не самым идиоматическим.Sirikid
12.05.2017 02:03Смотрел слайды и думал про Kotlin, он почти идеально подходит под описание C# Light.
Отсутствие неизменяемых переменных (или свойств) скорее проблема Си-подобных языков, а не объектно-ориентированных.vintage
12.05.2017 09:36D сиподобный и в нём есть неизменяемые структуры:
immutable struct Person { /// Full name string name; /// Birth day DateTime birthday; }
auto person = Person( 'Alice' , DateTime( 2000 ) )
Sirikid
12.05.2017 10:14D сравнительно поздний и богатый язык, я думал о C, C++ и Java когда писал этот комментарий, возможно стоило их явно перечислить в самом комментарии.
vintage
12.05.2017 11:09Вообще говоря, автор c#light упростил лишь синтаксис для одного конкретного кейса — иммутабельные структуры, а для этого хватило бы обычных классических структур с модификатором. Тут не нужны классы, гетеры, модификаторы доступа и прочий высший пилотаж.
NaHCO3
14.05.2017 06:32> Но если брать другие, более важные моменты, то основная проблема языков ООП — это то, что они навсегда останутся языками мутирующих данных
Так и компьютер — это механизм с мутирующими данными. И алгоритмы затачиваются именно под мутирующие данные. При переходе на немутирующие ленивые могут даже асимптотику потерять, доступную в императивном стиле.
Я как-то купился на всю эту чушь и попробовал пописать на хаскеле. Вроде даже получилось, хотя и геморно было. Но это я прак писал, никаких требований к производительности там не предъявлялось, лишь бы работало. Потом посмотрел, как большие мальчики пишут на хаскеле. Оказалось, что реальные либы включают монады памяти, IO, подключают сишный код. В общем, весьма далеки от идеалов чистоты. А иногда ещё и энергичные вычисления вместо ленивых используют, чтобы сборщик мусора не напрягать. Если кто не понял, то именно так и выглядит текущая абстракция. Когда сборщик мусора тормозит и нужно оптимизировать код не из соображений красоты и логичности, а чтобы мусор собирался.
> The fact is, in OO languages there is no built-in mechanism to ensure that the object you're dealing with is stable.
А как же константы? Очень удобная декларация, с самого начала программистам понравилась. Просто не надо все поля класс делать константными.
> Но такой стиль всегда будет оставаться для этих языков если не инородным, то по крайней мере не самым идиоматическим
Так он в любом случае будет императивным. Ну вот заменили все функции на структуры данных, запихали их во Free, получили ad-hoc синтаксическое дерево. Только вот оно само ничего не вычисляет. Его надо интерпретировать потом. И внезапно оказывается, что интерпретатор с большим энтузиазмом меняет состояния.
st210165
10.05.2017 10:34-3ребята
проблема не в том в какой парадигме суть языка программирования
а в том что суть языка есть Синтаксис
я до сих пор не могу понять Почему синтаксис все не вобрал в себе на Упрощения кода
пусть будет синтаксис совмещен от всех парадигм и от ООП и от Функций и от Прологов и всех остальных язычеств
программисту вообще нет дела до того как реализуются уже библиотеки и модули
ему есть дело Как решить ту или иную задачу за минимум времени чтобы получить максимум премию)
никто не хочет работать
так создайте Си шарп с синтаксисом Питона и Наскела))
столько не нужных скобок и типов которые в Айдле уже сами выставляются
упрощайте синтаксис и не хамите что это Сахарный десерт
и начинайте упрощать с ассемблера
и почему нельзя в ассемблере применять циклы)) по синтаксису — сами себе вы программисты хамите
вот что я думаюARG89
10.05.2017 10:36+4Я вижу, вы и в русском языке синтаксис упростили — ни одной запятой
st210165
10.05.2017 17:50-5так прибавь по моде твердые знаки)) в конце слов русского языка чтобы тебе вторить)
если ты против упрощений то пиши все на ассемблере)) и забудь вообще другие языки
ведь можно в том тебя же укорить что Упростил себе жизнь используя любой другой язык а не ассемблер)
Spiceman
10.05.2017 18:56Пример с игрой Жизнь на F# не очень показательный. На C# с помощью linq получается примерно также:
static void Main() { var rnd = new Random(); var size = 10; var grid = Enumerable.Range(0, size * size).Select(_ => rnd.Next(2)).ToArray(); Func<int, int, int> match = (v, n) => n == 3 ? 1 : n == 2 ? v : 0; var neighbours = new[] { -1 - size, -size, 1 - size, -1, 1, -1 + size, size, 1 + size }; while (true) { Console.Clear(); foreach (var s in Enumerable.Range(0, grid.Length / size) .Select(y => string.Join("", Enumerable.Range(0, size).Select(x => grid[y * size + x])))) Console.WriteLine(s); grid = grid.Select((v, i) => match(v, neighbours.Sum(o => grid[Math.Abs(i + o) % grid.Length]))).ToArray(); Thread.Sleep(300); } }
VagifAbilov
11.05.2017 17:34Переходя на LINQ, мы уже зачастую переходим на элементы функционального программирования. В вашем примере цепочка из Enumerable.Range -> Select -> Select — это ли не функцинальные трансформации? Обратите еще внимание, что вы решаете задачу для доски конечного размера, в то время как игра Конвея предполагает бесконечную доску (решение на фшарпе, о котором я говорил, не ограничивает размер доски).
TheKnight
Если не ошибаюсь, это обычно называют выводом типов (type inference).
VagifAbilov
Спасибо за поправку. Я не знал. Долго думал, как же это перевести, остановился на «внедрении».
TheKnight
Оу. Я почему то думал это оригинальный текст на русском и замечание оставил что бы уточнить сопоставимость терминов. Мало ли, вдруг что то новое.
VagifAbilov
Так это и есть оригинальный текст, мы беседовали по-русски.
VolCh
Так зачем тогда переводить? )