четверг, 8 сентября 2016 г.

[prog.flame] Давно что-то не писал про ООП vs ФП, надо исправиться... ;)

Довелось опять пересечься с приверженцами функционального подхода к программированию (для которых отстой все -- начиная от ООП и заканчивая исключениями). Поймал себя на давно забытом ощущении: у функциональщиков большинство примеров сильно оторванны от прикладных целей. Во времена, когда ООП завоевывало себе место под Солнцем апологеты ООП расказывали о достоинствах ООП на простых и понятных примерах. Вот, типа, вам нужны средства для рисования кругов, квадратов, треугольников и т.д. Или вот вам нужны средства для показа менюшек и диалогов. И эти примеры нормально заходили, т.к. без ООП все это приходилось делать на процедурных языках вроде C или Pascal. Это было сложно, плохо расширяемо, трудоемко в поддержке и т.д. Тогда как с ООП все оказывалось ощутимо проще. Ну, если хотите, не проще, а управляемее. Появлялась более удобная структура в коде, больше устойчивости (т.е. правки в одной части вовсе не обязательно сказывались в остальных частях), расширяемость улучшалась и т.д.

Понятно, что ООП -- это никакая не серебрянная пуля. И даже на заре проникновения ООП в мейнстрим (по крайней мере в наших Палестинах) было очевидно, что на ряде задач выигрыш от ООП может быть только если задача объемная и предполагает развитие в течении длительного времени. Помнится, некоторые аппологеты, вроде Гради Буча, даже предупреждали, что с ООП придется потратить гораздо больше сил и времени по сравнению с процедурным подходом на начальном этапе разработки. Т.е. на C или на Pascal-е вы бы уже могли написать худо-бедно работающий прототип, а на C++, Eiffel или SmallTalk еще бы только-только завершали фазу ОО-анализа и, возможно, фазу ОО-проектирования. И, скорее всего, не написали бы еще никакого кода.

Тем не менее, при всех своих проблемах ООП всегда (по крайней мере так было в начале 90-х) крутился вокруг практических вещей. Вот здесь у нас будет класс Строка. Ok, понятно что это и зачем. Вот здесь -- класс Файл. Снова Ok, снова понятно что и зачем. Вот здесь -- класс COM-порт. И снова Ok, снова все понятно. Аналогично с классами Меню, Диалог, Конфиг, Таймер, Нить и т.д. И вот уже из понятных частей собирается GUI-приложение для работы с неким внешним устройством через COM-порт.

Поэтому ООП было более-менее просто объяснять людям. Мол, смотри, у тебя будет COM-порт. Мы сейчас точно не знаем, что именно будет у него внутри, но более-менее понимаем, как с ним работать. Поэтому давай сделаем набросок класса COM-порт с несколькими очевидными публичными методами и пойдем дальше... На какой-то N-ой итерации мы возвращаемся к нашему классу и уточняем еще чуть больше. А потом еще и еще раз.

Т.е. ООП можно было объяснять "на пальцах", отталкиваясь от практических нужд конкретного разработчика.

Тогда как с ФП лично мне таких объяснений, которые бы отталкивались от практики, особо не попадается. Если говорить совсем грубо, то получается что-то вроде:

-- Вот смотри, у нас есть функция f a b -> c...
-- А что это за функция? Для чего она?
-- Это не важно. У нас просто есть функция f a b -> c. У нее два аргумента. Но, на самом деле, ее можно представить как последовательность функций с одним аргументом f a -> b -> c...
-- И что?
-- И мы может делать каррирование, когда мы из f a b -> c, получаем g b -> c с зафиксированным значением аргумента a. А все месте нам это дает возможность композиции функций!
-- Композиции функций для чего?
-- Для чего угодно. Вот смотри, допустим, нам нужно применить к данным функции x, y и z...
-- К каким именно данным и что именно делают функции x, y и z?
-- Это не важно, важно то, что через композицию мы можем сделать так, что разультат x идет в y, а разультат y идет в z. Мы просто описываем композицию функций декларативно в коде и все.
-- Что все? Какие у нас данные? Как именно мы их обрабатываем? Зачем мы это делаем?
-- Это не важно, я тебе принцип объясняю.

Вот, скажем, как вам такая статья на Хабре: Монады и do-нотация в C++? Но не просто как статья, как демонстрация принципа, на котором можно связать в цепочку последовательность операций. Например, B*x + C, где B и C -- это матрицы, а операции умножения и сложения могут завершаться неудачно.

Собственно, сей пост не является попыткой бросить камень в само ФП. Как по мне, так изрядная часть фишек ФП оказалась в императивном мире давным давно (например, если посмотреть на языки SmallTalk и Ruby, в которых блоки кода есть ни что иное, как лямбды, а изрядная часть методов в стандартной библиотеке базируется на использовании функций высших порядков). И использовалась программистами задолго до возникновения хайпа вокруг ФП, причем многие до сих пор не подозревают об этом, дергая Enumerable#map. Ну и вообще, очень многие вещи из ФП вполне себе просты и понятны, если их объяснять без чрезмерного использования математических формул :)

Речь о том, что рекламе ФП и раньше недоставало приземленности. И сейчас ничего особо не изменилось, как по мне. Поэтому если человек не фанат изучения новых парадигм и языков, и его не прет от реализации моноидов на шаблонах C++, он не тащится от того, насколько генерики+трейты в Rust-е удобнее генериков в Java, а ему тупо нужно взять набор байтиков отсюда и переложить их вот туда, то осознать полезность фишек ФП ему сейчас ничуть не проще, чем 10 лет назад. Имхо, конечно.

PS. Кстати говоря, ничуть не удивлюсь, если для современной молодежи, которая начала учиться программировать сейчас или даже лет пять назад, ситуация видится прямо противоположной. Вероятно, для нонишних новичков в программировании базисом, с которого они стартуют, являются такие вещи, как функции высших порядков, алгоритмы вроде foldr/foldl/zip, алгебраические типы данных и т.д. А вовсе не правила структурного и модульного программирования, с осознания важности которых довелось начинать моему поколению.

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