В чём сложность "перехода" на функциональное программирование?

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


В чём сложность "перехода" на функциональное программирование? Например, вы кодите на C# и думаете, что переход на F# будет примерно таким же, как переход на Java. Нет, переход на ФП — это совершенно не то же самое, что изучение нового языка программирования, когда вы тратите основное время на изучение нового синтаксиса уже известных вам фич, после чего отшлифовываете горсткой новых понятий другого языка. Приступить к программированию на новом языке вы можете очень быстро, просто изучив синтаксис циклов, условий, функций и классов.

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

Я в СильныхИдеях и в Hard Work стараюсь давать мягкое введение в различные подходы ФП на массовых языках (Java, Python, ...) — чтобы курсанты сразу могли начать применять это всё в своей повседневной работе. В частности, всегда полезно a) писать чистые функции, b) данные по возможности делать иммутабельными, c) отказываться от циклов (да и от условий)... Ваш привычный мир программирования стремительно разваливается, и вы потихонечку начинаете применять рекурсию, композицию, list comprehensions... Поначалу это даже опьяняет, когда вы внезапно понимаете, как вам сжать сотню строк вашего вчерашнего кода на C# до трёх строк на F#.

Однако по мере того, как вы расширяете вашу систему типов, оказывается, что типы начинают то тут, то там не состыковываться. И оказывается, что как-то надо работать с побочными эффектами хотя бы на уровне I/O, и надо обрабатывать ошибки, и часто возникает потребность в работе с состоянием... А как это всё отлаживать? В результате ваш ФП-проект запутывается в 10 раз быстрее, чем если вы делали бы его в классическом ООП.

=

В ФП есть и концепции, и принципы, и инструменты для решения всех этих проблем, но если изучить технику функционального программирования на уровне programming in small вы можете по разным курсам и потом без проблем успешно нарешать сотню литкодовских задачек на хаскеле, то уже даже на уровне проектирования, с которым вы легко справлялись, например, в проекте на сто классов ООП, в аналогичном ФП проекте вы оказываетесь в полном тупике. Вы написали 256 чистых функций, которые делают 4096 разных вещей, и что теперь?

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

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

=

Недавно вышла хорошая книга "A Skeptic’s Guide to Functional Programming with JavaScript?" в которой автор James Sinclair наглядно и выразительно объясняет многие принципы ФП на примерах JavaScript, которые легко адаптируются к другим языкам.

В частности, следующим шагом после первичного ознакомления с вышеупомянутой троицей James Sinclair рекомендует такие три темы: алгебраические структуры, тайп-классы и алгебраические типы данных.

Вот очень краткая выжимка/пересказ =>

1. Алгебраические структуры.

Когда функциональные программисты щеголяют терминами вроде "монада (это моноид...)" или "аппликативный функтор", то они говорят именно об алгебраических структурах — математических понятиях, которые весьма универсальны и определены формально.

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

map — это метод, который получает на вход функцию f, тип результата которой T, и применяет её к содержимому своего владельца. Если содержимое —- перечислимый тип, то f применяется к каждому элементу.

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

[1, 2, 3] .map(sqrt) = [1.0, 1.414, 1.732]

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

1) существует функция f(x)=x, и u.map(f)=u — идентичность,

2) u.map(f(g)) = u.map(g).map(f) — композиция.

Подобных алгебраических структур в целом существует огромное количество. Ну и что? :) Сами по себе алгебраические структуры ничего особенного не дают, это просто строгие абстрактные описания. В нашей программистской практике нам не нужны формальные спецификации, чтобы использовать map. Любой джуниор может случайно наткнуться на стандартную функцию map (или аналогичную по возможностям с другим именем) и по одному-двум примерам сразу поймёт, как она работает со списками.

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

Это не я и не Sinclair придумал, об этом многие великие говорят, например:

— "Плохие программисты думают о коде. Хорошие программисты думают о структурах данных и взаимосвязях между ними". — Линус Торвальдс

— 'Type structure is a syntactic discipline for enforcing levels of abstraction.' — J. C. Reynolds

=

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

1. Абстракции всегда скрывают некоторые детали и дают достаточно ясную общую картину. Как только вы изучите несколько экземпляров алгебраических структур, часто используемых в функциональном программировании (Array, Maybe, Effect и т. п.), вы постепенно начнёте видеть более глубокие закономерности, которые ранее никогда бы не заметили, оставаясь на уровне обычных классов. А затем и код станете писать, опираясь уже на более универсальные сущности.

2. Так как алгебраические структуры основываются на известных математических понятиях, мы вполне можем заставить эту математику работать на нас. Компиляторы используют формальные спецификации для такой важной задачи, как вывод типов значений, который может быть весьма сложным. Впервые type inference был реализован для ML-языков (OCaml, F#...), когда компилятор стал выводить самый общий полиморфный тип для любого выражения, причём он делает это с математической гарантией.

Что хорошего это нам даст? В простейшем случае, зная, что

u.map(g).map(f) = u.map(f(g))

(поверьте, что это далеко не очевидно даже миддлам :)

мы можем учесть размер входного списка: если он очень большой, то цепочка map(g).map(f) будет работать скорее всего медленнее, потому что в ней потребуется создавать промежуточный список.

Самые разные алгебраические структуры успешно (и, главное, строго) применяются для работы с тем, что в "обычных" языках представлено кривоватой прииляпкой nullable, для ленивых/отложенных вычислений, для контроля за побочными эффектами и т. д. и т. п.

=

Но если алгебраические структуры настолько прекрасны, то почему же они почти не используются в нашей повседневной практике? Где все эти программисты, которые заявляют, что очень здорово применять алгебраические структуры? Их хоть кто-нибудь видал живьём? :)

Ну на самом деле они используются весьма продуктивно, просто те, кто с ними работают, обычно имеют опыт разработки на функциональных языках (F#, Haskell, Scala, PureScript...), где есть особый способ создания алгебраический структур — не с помощью классов из ООП, а с помощью т.н. тайп-классов. Вкратце про них, а также про третью темку — алгебраические типы данных, в следующем посте.


Источник: vk.com

Комментарии: