Введение
Среди особенностей моего подхода к разработке у моих заказчиков, коллег и студентов наибольшее сопротивление вызывает использование Spring Data JDBC, а не [Spring Data] JPA (де-факто стандарта работы с БД на платформе Java).
Изначально я собирался писать пост "Почему не JPA", но немного подумав понял, что ответ умещается в одно предложение: потому что JPA по своей природе (persistence context и dirty checking) не поддерживает неизменяемую модель данных - неотъемлемую часть функционального стиля программирования, который, в свою очередь, является неотъемлемой частью моего подхода к разработке. И это объективный факт.
Почему для себя я выбрал ФП, а не "нормальное" императивное программирование? На этот вопрос также можно ответить одним предложением: потому что функциональный стиль помогает мне снижать стоимость разработки для бизнеса и делать руководителей проектов счастливыми.
Уверен, многие не согласятся с истинностью утверждения "применение функционального стиля ведёт к снижению стоимости разработки". Поэтому я пока буду называть его Гипотезой и приведу факты, доказывающие её истинность.
Главный из этих фактов - эмпирическое подтверждение Гипотезы, описанное в книге Structured Design.
Структурный дизайн
В 60-ых годах прошлого века учёный Ларри Константин написал книгу Structured Design. В этой книге он впервые ввёл понятия сцепленности (coupling) и функциональной связанности (cohesion), которые до сих пор лежат в основе большинства подходов к проектированию и кодированию программ.
Помимо этого, в главе "THE MORPHOLOGY OF SIMPLE SYSTEMS" (для просмотра необходимо пройти бесплатную регистрацию и "занять" книгу), Константин приводит иллюстрацию того, что он называет моделью, ориентированной на трансформацию (выделение цветами моё):
Упрощённо, эта модель разбивает код на четыре вида - ввод, вывод, трансформации и координацию.
Затем Константин пишет:
It [transform-centered model] was derived empirically from a careful review of the morphology of systems, comparing systems that had proven to be cheap to implement, to maintain, and to modify with ones that had been expensive.
[...]
The umpteenth round in this game produced what came to be called the transform-centered model. Most of the cheap systems had it - none of the costly systems did!
---
Она [модель, ориентированная на трансформацию] была получена эмпирически на основе тщательного анализа морфологии систем со сравнением систем, которые были доказано дёшевы в разработки, обслуживании и модификации, с системами, которые были дорогостоящими.
[...]
Энный раунд этой игры породил то, что стало называться моделью, ориентированной на трансформации. Она присутствовала в большинстве дешёвых систем - ни одна из дорогостоящих систем не имела её!
— Larry Constantine, Structured Design, p. 144
И если предположить, что трансформации являются чистым функциями в терминах ФП, то в этой модели отчётливо видна функциональная архитектура:
красные афферентные модули (ввод), жёлтые эфферентные модули (вывод) и голубой модуль координации - императивная оболочка
зелёные модули центральной трансформации - чистое ядро.
Константин не пишет прямым текстом, что трансформации - это чистые функции, но если изучить книгу и её исторический контекст, то это становится очевидно.
Для того чтобы функция была чистой, она не должна иметь побочных эффектов:
Ввод-вывод (в консоль, файл, сеть и т.д.);
Выброс исключения;
Запуск нового потока;
Изменение полей структур данных, переданных ей на вход по указателю;
Чтение и изменение полей глобальных структур данных, доступных по статической ссылке (обращение к общему окружению, синглтоны).
Первый вид эффектов (ввод-вывод) в подпрограммах трансформации исключён самой моделью.
Выброс исключения, запуск нового потока и изменение структур данных по указателю исключены историческим контекстом - все эти штуки только-только начали появляться в момент разработки структурного дизайна (исключения, потоки, указатели).
Любопытный факт — с указателями Советский Союз смог догнать и перегнать Америку и придумал их в 1955 году, на 12 лет раньше западных учёных.
Касательно обращения к общему окружению, Константин вроде бы прямым текстом говорит, что этого не следует избегать любой ценой. Но он приводит такую иллюстрацию "эффекта общего окружения", которой хочется избежать любой ценой.
Whenever two or more modules interact with a common data environment, those modules are said to be common-environment coupled.
[...]
A common environment may be a shared communication region, a conceptual file in any storage medium, a physical device or file, a common data base area, and so on.
[...]
The point is not that common-environment coupling is bad, or that it should be avoided at all cost. To the contrary, there are circumstances in which this may be the method of choice. However, it should be clear that a small number of elements shared among a few modules can enormously complicate the structure of a system - from the point of view of understanding it, maintaining it, or modifying it.
— Larry Constantine, Structured Design
Всякий раз, когда два или более модуля взаимодействуют с данными в общем окружении, считается, что эти модули сцеплены через общее окружение.
[...]
Общим окружением может быть общая область обмена данными, концептуальный файл на любом носителе информации, физическое устройство или файл, общая область базы данных и так далее.
[...]
Посыл не в том, что сцепленность через общее окружение плоха или что её следует избегать любой ценой. Напротив, существуют обстоятельства, при которых этот метод может быть предпочтительным. Однако должно быть очевидно, что небольшое количество элементов, совместно используемых несколькими модулями, может чрезвычайно усложнить структуру системы с точки зрения её понимания, обслуживания или модификации.
— Larry Constantine, Structured Design
Наконец, в статье Structured Design Константин пишет:
A predictable, or well-behaved, module is one that, when given the identical inputs, operates identically each time it is called. Also, a well-behaved module operates independently of its environment.
---
Предсказуемый, или [хорошо управляемый/исправный/хорошо себя ведущий], модуль - это модуль, который при задании идентичных входных данных работает одинаково при каждом его вызове. Кроме того, исправный модуль работает независимо от своей среды.
— Larry Constantine, Structured Design
Это определение well-behaved модуля (подпрограммы) буквально является определением чистой функции.
Таким образом, мы получаем:
Трансформации физически не могли иметь никаких эффектов, кроме обращения к общему окружению;
При этом Константин иллюстрирует "эффект общего окружения" картинкой, которая повергнет в ужас любого разработчика;
И называет исправными те подпрограммы, которые обладают свойствами чистых функций - в том числе не обращаются к общему окружению.
Достаточно ли этого, для того чтобы прийти к выводу, что трансформации де-факто были чистыми функциями и, следовательно, модель, ориентированная на трансформации, является эквивалентом функциональной архитектуры? Я считаю что да.
Это, в свою очередь, значит что есть эмпирические свидетельства тому, что применение функциональной архитектуры ведёт к системам "дешёвым в разработке, обслуживании и модификации".
Это могло бы быть неопровержимым доказательством Гипотезы, если бы не одно но. Результаты этого исследования не были опубликованы в рецензируемом научном журнале, потому что "исходные данные и заметки были утеряны в беспорядочной гибели института".
Тем не менее, я считаю, что слова учёного с мировым именем заслуживают доверия. В том числе потому, что они подтверждаются другими экспертами-практиками и моим собственным опытом.
Косвенные подтверждения Гипотезы
ФП в книгах
Призывы максимум кода выделять в чистые функции встречаются во множестве книг, опубликованных начиная с 60-ых годов и публикуемых по сей день.
Functional Design: Principles, Patterns, and Practices (2023), Robert Martin
Свежая книга Роберта Мартина, пожалуй, самого цитируемого человека на собеседованиях и конференциях по дизайну и хорошему коду, называется Functional Design: Principles, Patterns, and Practices.
Unit Testing: Principles, Practices, and Patterns (2020), Vladimir Khorikov
Лучшая, на мой взгляд, книга по автоматизации тестирования включает в себя три раздела на 23 страницы, посвящённых функциональной архитектуре. Потому что она является необходимым
Patterns, Principles, and Practices of Domain-Driven Design (2015), Scott Millett
Side effects can make code harder to reason about and harder to test, and they can often be the source of bugs. In a broad programming context, avoiding side effecting functions as much as possible is generally considered good advice. You even saw in the previous chapter how being side-effect‐free and immutable were two of the main strengths of value objects. But if avoiding side effects is good advice, avoiding hidden side effects is a fundamental expectation.
---
Побочные эффекты могут усложнить анализ кода и его тестирование, а так же они часто бывают источником ошибок. Избегать, насколько это возможно, функций с побочными эффектами - обычно считается хорошим советом в программировании. Вы даже видели в предыдущей главе, что отсутствие побочных эффектов и неизменяемость были двумя основными преимуществами объектов-значений. Но если избегать побочных эффектов - хороший совет, то избегать скрытых побочных эффектов - фундаментальное ожидание.
— Scott Millett, Patterns, Principles, and Practices of Domain-Driven Design, section Favor Hidden‐Side‐Effect‐Free Functions
Clean Code (2008), Robert Martin
Side effects are lies.
Your function promises to do one thing, but it also does other hidden things. Sometimes it will make unexpected changes to the variables of its own class. Sometimes it will make them to the parameters passed into the function or to system globals. In either case they are devious and damaging mistruths that often result in strange temporal couplings and order dependencies.
---
Побочные эффекты - это ложь.
Ваша функция обещает сделать что-то одно, но она также выполняет и другие скрытые действия. Иногда она вносит неожиданные изменения в переменные своего класса. Иногда делает это с параметрами, передаваемым в функцию, или глобальными переменными системы. В любом случае это коварные и разрушительные обманы, которые часто приводят к странным временнЫм связям и зависимостям в порядке выполнения.
— Robert Martin, Clean code, section Have No Side Effects
Domain-Driven Design (2003), Eric Evans
Place as much of the logic of the program as possible into functions, operations that return results with no observable side effects. Strictly segregate commands (resulting in modifications to observable state) into very simple operations that do not return domain information. Further control side effects by moving complex logic into VALUE OBJECTS with conceptual definitions fitting the responsibility.
---
Помещайте возможный максимум логики программы в функции — операции, которые возвращают результат без наблюдаемых побочных эффектов. Строго выделяйте команды (приводящие к изменениям в наблюдаемом состоянии) в очень простые операции, которые не возвращают доменную информацию. Дальнейший контроль побочных эффектов осуществляется путём переноса сложной логики в ОБЪЕКТЫ-ЗНАЧЕНИЯ, чьё концептуальное описание подходит для включения ответственности [за выполнение этой логики].
— Eric Evans, Domain-Driven Design, section SIDE-EFFECT-FREE FUNCTION
Object-Oriented Software Construction (1997), Bertrand Meyer
The first question that we must address will have a deep effect on the style of our designs. Is it legitimate for functions — routines that return a result — also to produce a side effect, that is to say, to change something in their environment?
The gist of the answer is no, but we must first understand the role of side effects, and distinguish between good and potentially bad side effects.
---
Первый вопрос, на который мы должны ответить, окажет глубокое влияние на стиль наших дизайнов. Законно ли, чтобы функции — подпрограммы, возвращающие результат, — также производили побочный эффект, то есть изменяли что-то в своей среде?
Суть ответа - нет, но сначала мы должны понять роль побочных эффектов и провести различие между хорошими и потенциально плохими побочными эффектами.
— Bertrand Meyer, Object-Oriented Software Construction, section 23.1 SIDE EFFECTS IN FUNCTIONS
Следующим косвенным доказательством преимущества ФП-стиля является тот факт, что большинство его адептов в начале своей карьеры были сторонниками императивного стиля.
ФП-перебежчики
Наиболее яркими, на мой взгляд, представителями людей, слишком слабых духом для императивного программирования, являются Роберт Мартин и Рич Хикки.
Мартин — человек, чьё имя приходит на ум первым после слов "объектно-ориентированный дизайн" или "чистый код" - десять лет назад перешёл с C++ на Clojure и теперь пишет посты-оды функциональному стилю и книги по функциональному дизайну.
Хикки некогда был адвокатом C++, но в итоге настолько устал от проблем, вызванных императивным стилем, что сделал собственный функциональный язык с персистентными структурами данных и примитивами безопасного конкурентного программирования, собственную функциональную СУБД и собственную модель функционального программирования. И это всё не академические изыскания - Clojure активно используется в коммерческой разработке (хотя масштаб использования не сопоставим с Java/C#/Kotlin и т.д., конечно же).
ФП в технологиях
Технологии, в частности языки программирования и фреймворки, призваны сделать разработку программ дешевле. И сейчас наблюдается отчётливый тренд увеличения процента технологий, базирующихся на принципах ФП. На основании чего можно предположить, что эти принципы помогают авторам удешевить разработку программ на базе их технологий.
Единственный популярный известный мне язык родом из этого века, который не поддерживает ФП - Go. Все остальные - Kotlin, Swift, Rust - поддерживают. Естественно, я не говорю о целом ворохе чисто функциональных языков программирования.
Сейчас даже Java семимильными шагами идёт в сторону ФП стиля — записи, закрытые иерархии, паттерн мэтчинг, лямбды в конце концов.
А GUI-фреймворки, некогда бывшие безраздельной вотчиной ООП, в XXI веке все как один - React, SwiftUI, Jetpack Compose (ну ладно, основные для наиболее используемых платформ - Web, Android, iOS) — предлагают функциональную модель.
Вероятная причина низкой стоимости ФП-программ
Я полагаю, что при прочих равных, в разработке дешевле те программы, которые проще понять.
Потому что при работе с понятной программой, разработчику требуется меньше времени (=денег) на её изучение, для того чтобы внести требуемые правки. И при изменении понятной программы меньше вероятность внести ошибку, которая потребует дополнительного времени (=денег) на её исправление.
И я хотя понятность кода во многом является субъективно метрикой, программы, написанные в функциональном стиле, объективно более понятны.
Это можно продемонстрировать на двух областях, которые строят точные модели программ - формальная верификация программ и оптимизирующие компиляторы. Для обеих этих областей подпрограммы, написанные в функциональном стиле, являются объективно более понятными:
Оптимизирующие компиляторы не могут применять многие из оптимизаций к программам с эффектами из-за того, что не могут предсказать последствия этих оптимизаций ([1]
Наличие оператора присваивания в программе существенно усложняет задачу её формальной верификации ([2]).
Есть и более близкая большинству разработчиков область, в которой большая простота функционального стиля на фоне императивного не вызывает сомнений, - многопоточное программирование.
Главная сложность написания многопоточных программ в императивном стиле (с разделяемыми изменяемыми структурами данных) в том, что в любой момент ваш код могут остановить и поменять данные, с которыми вы только что работали. Это как будто вы подносите ко рту конфетку, в последнюю секунду моргаете, открываете глаза, а там уже шпинат.
По сути та же ситуация может возникнуть и в однопоточной программе в момент вызова функции с передачей в неё изменяемой структуры данных. Да, тут всё значительно проще - вы точно знаете когда и примерно знаете кто (в случае полиморфного вызова в закрытой системе) может подменить вам конфетку. Но для того чтобы быть уверенным, что вы съедите именно конфетку, вам надо перед каждым использованием функции (и после каждого её изменения) заглядывать внутрь и изучать, что она делает с вашей конфеткой.
Эту проблему императивного стиля можно проиллюстрировать на следующем примере:
fun main() {
val els: ArrayList<Int> = arrayListOf(2, 2)
val sum = sum(els)
println("Сумма ${els[0]} + ${els[1]} = $sum")
}
fun sum(els: ArrayList<Int>): Int = TODO()
Что мы можем сказать про поведение этой программы? Если вынести за скобки то, что прямо сейчас она вылетит с исключением (так реализована функция TODO в Kotlin) - ничего.
Потому что sum
может быть реализована, например, так:
fun sum(els: ArrayList<Int>): Int {
var sum = 0
while (els.isNotEmpty()) {
sum += els.remove(0)
}
return sum
}
С такой реализацией вызов этой программы завершится выбросом IndexOutOfBoundsException. Это хоть и синтетический пример, но он основан на реальных событиях - из-за подобного кода я лично вносил баг, который нашли только в проде.
Для того чтобы снизить стоимость разработки, не надо знать теорию категорий
Пуристы во главе с Эриком Мейером со мной не согласятся, но прагматики во главе в анкл Бобом меня поддержат в том, что для получения пользы от ФП-стиля не обязательно уходить в идеально чистое функциональное программирование.
Для этого достаточно с помощью функциональной архитектуры разделить ввод-вывод и логику и сделать модель данных неизменяемой. А императивную оболочку, как это ни странно, намного удобнее писать в императивном стиле. И если локальная переменная не утекает за пределы функции - она вполне может быть изменяемой (см. примеры кода в конце поста).
Я не знаю единого хорошего источника по пролетарскому прагматичному ФП, но могу порекомендовать трек его изучения.
-
Сначала стоит прочитать пост Владимира Хорикова - Иммутабельная архитектура.
В этом посте можно быстро схватить основную идею функциональной архитектуры информационных систем. Но в реализации информационных систем в функциональном стиле слишком много нюансов для одного поста, поэтому одним постом ограничиться не получится.
-
Затем можно так же быстро посмотреть ещё пару небольших примеров решения "реальных" задач в ФП-стиле в моих постах:
-
После этого можно прочитать книгу того же Хорикова Unit Testing Principles, Practices, and Patterns (на русском)
Тут больше деталей, но так как фокус книги всё-таки на тестировании, не хватает главной части — моделирования неизменяемых данных.
-
Моделирование незименяемых данных хорошо раскрыто в книге Domain Modeling Made Functional.
Тут уже прям ФП-ФП с монадами, но после материалов Хорикова у вас будет выбор - идти в эту кроличью нору или нет.
-
Наконец, всё это можно полернуть свежей книгой анкл Боба Functional Design: Principles, Patterns, and Practices.
В этой книге нет ни одного сложного эффекта (всё эффекты в памяти) и примеры на Clojure, но она хорошо иллюстрирует, тот самый
пролетарскийпрагматичный стиль ФП.
Заключение
Итого мы имеем следующие факты:
Учёный с мировым именем утверждает, что у него были эмпирические данные, свидетельствующие о том, что дешёвые программы имеют структуру, которая чрезвычайно напоминает функциональную архитектуру;
Множество экспертов-практиков со страниц своих книг призывают возможный максимум кода выносить в чистые функции;
Множество экспертов-практиков переходят с императивного стиля на функциональный;
Множество вендеров включают принципы функционального подхода в основу своих технологий;
Программы, написанные в функциональном стиле, проще верифицировать и оптимизировать;
Писать многопоточные программы намного проще в функциональном стиле;
При этом код в функциональном стиле может быть вполне понятен человеку без степени доктора математических наук.
Достаточно ли этих фактов, для доказательства того, что функциональный стиль снижает стоимость разработки? Для меня, особенно с учётом того, что они согласуются с моим опытом, - да.
Комментарии (36)
panzerfaust
09.11.2023 04:50+2Среди особенностей моего подхода к разработке у моих заказчиков, коллег и
студентов наибольшее сопротивление вызывает использование Spring Data
JDBC, а не [Spring Data] JPAС общем посылом статьи согласен, но в вопросе выбора между этими двумя технологиями шо то черная магия, шо то черная магия. На мой вкус прям трушная иммутабельность - это MyBatis. Закидываешь иммутабельный объект в метод, получаешь иммутабельный результат из метода - всё, никаких сессий, состояний, актив рекордов и т.п. Плюс удобно писать сколь угодно безумные запросы с CTE, оконками и любыми причудами. Более хипстерский и приятный в использовании - JOOQ. Да, там можно работать с мутабельными Record, но можно и в чистом стиле. В этом плане он мультипарадигменный.
jdev Автор
09.11.2023 04:50и шо то черная магия, шо то черная магия
Я согласен, что SDJ - тоже чёрная магия. Но, для меня по крайней мере, несопоставимо более простая чем JPA. С SDJ, я могу работать практически не заглядывая в доки и гугл (после прочтения этой самой доки и сбора базовых грабель), а с JPA постоянно приходилось гуглить заклинания, которые надо скастовать для получения требуемого результата.
Плюс я сейчас в петпроекте эксперементирую с отказом от генерации репозов в пользу самописанных на базе jdbcAggregateTemplate - это ещё пласт магии уберёт
Закидываешь иммутабельный объект в метод, получаешь иммутабельный
результат из метода - всё, никаких сессий, состояний, актив рекордов и
т.п. Плюс удобно писать сколь угодно безумные запросы с CTE, оконками и любыми причудамиВ SDJ всё точно так же.
Плюс SDJ обещает в будущих версиях "решить проблему N+1 раз и навсегда" - это выглядит любопытно для меня.
Мне MyBatis, jooq (+ Exposed) - тоже симпатичны. Но чего мне в них нехватает - возможности сохранить дерево объектов "автомагически".
jdev Автор
09.11.2023 04:50А и самое главное забыл - SDJ интегрируется с MyBatis из коробки:)
То есть вы можете писать в БД через SDJ, со всеми его плюшками, а читать через MyBatis со всеми плюшками ещё и MyBatis.
kovserg
09.11.2023 04:50+1разработчику требуется меньше времени (=денег) на её изучение, для того чтобы внести требуемые правки.
Вот удивительно все это понимают, но до сих пор нет языка запросов к исходникам программы (как sql) что бы её изучать(исследовать), модифицировать и проверять нарушения инвариантов.
rsashka
09.11.2023 04:50-1А почему вы противопоставляете функциональное и императивное программирование?
Это же сравнение кислого с мягким, так как ФП может быть императивно.GospodinKolhoznik
09.11.2023 04:50+3Что такое ФП каждый понимает по-своему, но как правило принято считать, что ФП обладает свойством иммутабульности данных, отсутствием побочных эффектов и referential transparency (Шишков прости: не знаю как перевести)
По сути все преимущества, которые даёт ФП, оно даёт их за счёт этих 3 свойств. И разделение на ФП/не ФП имеет смысл проводить не по границе имеративный/не императивный, а про критерию выполняются или не выполняются эти 3 свойства.
includedlibrary
09.11.2023 04:50+1В ФП есть побочные эффекты, и для меня главным преимуществом ФП является возможность их контролировать. Я пишу функцию и в сигнатуре указываю, какие именно эффекты могут быть использованы в этой функции. Что-то вроде:
class Monad m => MonadInterpreter m where setVariable :: Text -> Int -> m () getVariable :: Text -> m Int interpreter :: MonadInterpreter m => Expr -> m Int interpreter = undefined
Соответственно по сигнатуре ясно, что в функции есть лишь один побочный эффект - установка значения переменной, печатать в консоль или запускать ракеты в ней не получится. При этом переменные можно сделать изменяемыми:
newtype InterpretM a = InterpretM { runInterpretM :: ReaderT Context IO a } deriving newtype ( Functor , Applicative , Monad , MonadIO , MonadReader Context ) data Context = Context { ctxVariables :: HashTable Text Int } data InterpreterError = UndefinedVariable Text instance MonadInterpreter InterpretM where setVariable var val = do vars <- asks ctxVariables liftIO $ HashTable.insert vars var val getVariable var = do vars <- asks ctxVariables liftIO (HashTable.lookup vars var) >>= \case Nothing -> liftIO $ throwIO $ UndefinedVariable var Just val -> pure val
GospodinKolhoznik
09.11.2023 04:50+2Я же говорю, что такое ФП каждый по-своему понмиает. Ок, неправда, что в ФП нет побочных эффектов,там нет побочных эффектов "по умолчанию", чтобы они появились надо прилагать усилия в виде оборачивания кода в какую ни будь специальную монаду и соотвествующие изменения в сигнатурах. например.
А ещё в ФП есть мутабельные данные, просто по умолчанию данные иммутабельные, а чтобы сделать их мутабельными опять таки надо их во что то оборачивать и снова указывать это в сигнатуре.
Кстати, пример, который вы привели, как я понимаю, сейчас стал архитектурным паттерном в Хаскеле. У меня, например, в моем собственном проекте, который я уже давольно давно ваяю, тип самого приложения выглядит вот так:
newtype App a = App { unApp :: ReaderT AppState IO a } deriving (Applicative, Functor, Monad, MonadReader AppState, MonadIO, MonadThrow, MonadCatch)
includedlibrary
09.11.2023 04:50Да, так и называется ReaderT pattern. В моём проекте все модули компилятора на нём построены
GospodinKolhoznik
09.11.2023 04:50Не знал )) просто сам к нему пришел после череды проб и ошибок. Сидел и игрался в песочнице, пытаясь то так то этак выстроить архитектуру упрощённого приложения, состоящего из сплошных mock-ов. И через несколько дней изобрёл велосипед.
kpmy
09.11.2023 04:50-1ФП подвид декларативного подхода, декларативный подход противопоставляется императивному. Возможно, это автор подразумевает.
rsashka
09.11.2023 04:50Я понимаю, что автор ошибочно считает ФП чисто декларативным подходом, поэтому и противопоставляет его императивному. Вот только ФП к данным парадигмам программирования не имеет вообще никакого отношения и его можно использовать как в декларативном, так и в императивном стиле программирования.
Не говоря уже про то, что многие императивные языки программирования часто имеют в своей семантике декларативные конструкции, превращая чисто императивное программирование в комбинацию императивных/декларативных стилей, в зависимости от пожеланий разработчика.
kipar
09.11.2023 04:50+2насчёт оптимизации я бы поспорил. Большинство придуманных людьми алгоритмов и структур данных подразумевают мутабельность. Как с помощью ФП сделать inplace сортировку? хеш-таблицу с открытой адресацией? LU разложение матрицы? Так что ФП, в моем понимании, работает на уровне архитектуры, а низкоуровневые функции все равно будут работать с мутабельными данными. Ну а компилятор, по большей части, оптимизирует как раз низкоуровневые функции.
jdev Автор
09.11.2023 04:50Я ни в коем случае не утверждаю, что ФП-программы быстрее императивных программ (хорошо написанных).
Этот пример не про то, что ФП-программы быстрее, а про то, что компилятор лучше "понимает" ФП программы и я это использую как аргумент в пользу того, что ФП-программы "понятнее".
Vladimirsencov
09.11.2023 04:50Часто работа с IO потребляет в десятки раз больше ресурсов, чем работа процессора и памяти. В таких случаях нет смысла оптимизировать код программы. А так да работа с иммутабильными объектами намного медленнее.
dyadyaSerezha
09.11.2023 04:50Тоже с трудом представляю, как функционально работать, например, с очередью неких данных/заданий, например, которая пополняется и обрабатывается несколькими потоками.
jdev Автор
09.11.2023 04:50С реальным миром (с состоянием) невозможно работать в чистом функциональном стиле. Именно эту проблему и решает функциональная архитектура - разделяет императивный код, который работает с реальным миром и чистый код, который работает с красивыми моделями.
Соответственно у вас должен быть какой-то инфраструктурный код, который занимается работой с очередью - с одной стороны помещает данные в очередь, а с другой стороны - достаёт.
А вот что будет перед и после этого кода - зависит от задачи.
Как вариант, у вас на обеих сторонах может быть по координатору
Координатор на входе получает запрос по хттп идёт в БД, достаёт оттуда неизменяемую структуру данных, передаёт её в чистое ядро, получает результат обратно и перекладывает его в инфраструктурный модуль для публикации в очередь.
Координатор на выходе получает запрос от инфраструктурного кода и делает всё тоже самое, вплоть до публикации в следующую очередь.
Lewigh
09.11.2023 04:50+10Учёный с мировым именем утверждает, что у него были эмпирические данные, свидетельствующие о том, что дешёвые программы имеют структуру, которая чрезвычайно напоминает функциональную архитектуру;
Ученые с мировым именем десятилетиями до этого продвигали ООП подход как богоизбранный. Тогда не существовало ФП? Просто пришла новая мода и пришел новый богоизбранный подход, при этом то что за эти десятилетия так и не смогли определится что же такое ООП и как на нем писать видимо не имеет значения.
Множество экспертов-практиков со страниц своих книг призывают возможный максимум кода выносить в чистые функции;
Множество экспертов-практиков переходят с императивного стиля на функциональный;
При этом мы не наблюдаем взрыва популярности языков программирования которые хоть как-то поддерживают ФП парадигму более менее достойно, вроде Scala, F#. Мы наблюдаем как множество экспертов-практиков пишут все в тех же языках с императивной парадигмой вроде Java, C# и т.д., у нас популярны современные ЯП такие как Kotlin и Go, в первом открыто заявлялось разработчиками что, не смотря на поддержку налета ФП, - это не про ФП, про Go я вообще молчу где он и где ФП. Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?
Множество вендеров включают принципы функционального подхода в основу своих технологий;
Да и все потому что религиозная догма под название ООП дала трещину и оказалось что есть другие подходы и у них есть преимущества и даже можно их совмещать.
Программы, написанные в функциональном стиле, проще верифицировать и оптимизировать;
Ну с первым пунктом я соглашусь а вот по повода второго, я бы с интересом посмотрел как бы Вы написали в функциональном стиле что-то требующее производительности, к примеру игру, а потом героически просто ее оптимизировали.
При этом код в функциональном стиле может быть вполне понятен человеку без степени доктора математических наук
Ну да ну да. Не нужно быть доктором наук чтобы изучать китайский язык, видимо это означает что китайский и английским можно изучить плюс минус.
Достаточно ли этих фактов, для доказательства того, что функциональный стиль снижает стоимость разработки?
Нет. В качестве доказательств нужна статистика, сколько что и за сколько производится с описанием почему. Делать выводы что ФП ускоряет разработку на основании что эксперты что-то там в своих книгах пишут - не очень серьезный подход
Мне тоже очень нравится ФП подход но не нужно из него делать очередную религию.GospodinKolhoznik
09.11.2023 04:50+3Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?
Не знаю, сам об этом думал. Какая то загадка дыры. Ведь Haskell идеален для создания именно масштабных и нетривиальных проектов. По своему опыту могу сказать, что он для этих целей лучше даже чем Java (а значит и её клон C#, ну и Kotlin) и уж тем более лучше, чем такие "опасные" языки, как Python или C++. Потому что большой и сложный проект всегда создаётся "по спирали" от крошечного и примитивного, до все больше и больше сложного, путем бесконечного рефакторинга и дополнения уже существующего кода. Невозможно написать что то большое и значимое просто с нуля все продумав, составив ТЗ, придерживаться его и сразу всё сделать хорошо. А преимущества Haskell раскрываются именно на рефакторинге кода, из всех языков, которые я знаю на нем удобнее и безопаснее всего проводить рефакторинг.
Наверное ответ в том, что сейчас почти никто не делает больших и значимых проектов. Рыночек порешал, и все компании заняты написанием стандартизированных и однотипных приложений с минимальными модификациями. И в таком подходе Хаскель действительно нафиг не нужон. Шлепать однотипные приложения по одному и тому же лекалу-фреймворку удобнее на JS или Python.
murkin-kot
09.11.2023 04:50Ведь Haskell идеален для создания именно масштабных и нетривиальных проектов
Что бы понять эту "идеальность" требуется глубокое понимание гораздо большего количества смыслов, нежели при использовании императива. Но углубление в смыслы точно так же требуется и в императиве, если проект становится сложным. В результате нарабатывается некий качественный набор, который во многом эквивалентен "идеальности" ФП. И зачем от него уходить на непривычную стезю, затрачивая немалое время на запоминание разного рода нечитаемых конструкций, которыми в ФП решили обозначить абстрактные операции (лифты и тому подобное)?
Качественное освоение хаскеля требует реально много времени. Может потом и станут видны сияющие перспективы, но суть работы программиста подсказывает, что ничего кардинально меняющего парадигму программирования в ФП нет, а потому вероятность встречи именно сияющих перспектив оценивается как околонулевая. Прагматичный подход, и ничего личного.
Поэтому остаётся лишь эмоциональное нравится/ненравится.
jdev Автор
09.11.2023 04:50Ученые с мировым именем десятилетиями до этого продвигали ООП подход как
богоизбранный. Тогда не существовало ФП? Просто пришла новая мода и
пришел новый богоизбранный подход, при этом то что за эти десятилетия
так и не смогли определится что же такое ООП и как на нем писать видимо
не имеет значения.Не совсем понял ваш посыл.
Константин, проводил свои исследования в 60-ых - лет за 40-50 до хайпа ФП.
При этом мы не наблюдаем взрыва популярности языков программирования
которые хоть как-то поддерживают ФП парадигму более менее достойно,
вроде Scala, F#. Мы наблюдаем как множество экспертов-практиков пишут все в тех же языках с императивной парадигмой вроде Java, C# и т.д.Я бы не стал связывать языки с поддержкой парадигмы и разработку в соответствии с парадигмой - на любом (Тьюринг полном) языке можно писать в соответствии с любой парадигмой, вопрос только в бойлерплейте. При том ФП парадигма + более-менее подходящий более-менее мейнстримный язык - это дешевле, на мой взгляд, чем ФП парадигма + чисто функциональный язык под который сложно найти разработчиков, библиотеки, документацию ко всему этому и решения не самых тривиальных проблем.
Но тут мы уходим в холивар, что такое ФП. В котором даже кложуристы с хаскеллистами не могут решить кто из них Труъ ФП.
Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?
Полагаю, основная причина - потому что в ВУЗах учат C/Java/Python etc.
а вот по повода второго, я бы с интересом посмотрел как бы Вы написали
в функциональном стиле что-то требующее производительностиНенене, я в своём уме, я бы никогда не стал этого делать:) Этот пример не про то, что ФП быстрее, а про то, что ФП "понятнее" для компилятора, из чего я делаю предположение, что оно и для человека понятнее.
Ну да ну да. Не нужно быть доктором наук чтобы изучать китайский язык,
видимо это означает что китайский и английским можно изучить плюс минус.Ну тут мне кажется мы снова упираемся в вопрос, что такое ФП.
Например
// написано в браузере fun calculateSalary(e: Employee): Int = TODO() // тут чистая математика fun main () { val e = db.getEmployee(readLine().toInt()) val salary = calculateSalary(e) paymentGateway.pay(e, salary) }
Для меня - ФП. Нужна ли для докторская для чтения этого кода? Нет
Нет. В качестве доказательств нужна статистика, сколько что и за сколько производится с описанием почему
Так Константин ровно то и проделал. Взял кучу программ с известной стоимостью, посмотрел что общего между дешёвыми программи и в чём разница с дорогими. Увидел что разница - в функциональной архитектуре.
Другое дело, что исходных данных нет - это да. Но так я и говорил о Гипотезе:)
Делать выводы что ФП ускоряет разработку на основании что эксперты что-то там в своих книгах пишут - не очень серьезный подход
Вы меня не верно поняли:) Я делаю вывод на основании эмпирического исследования Константина и собственного опыта. А эксперты и книги - это шло в разделе "Косвенные доказательства".
Lewigh
09.11.2023 04:50Я бы не стал связывать языки с поддержкой парадигмы и разработку в соответствии с парадигмой - на любом (Тьюринг полном) языке можно писать в соответствии с любой парадигмой, вопрос только в бойлерплейте. При том ФП парадигма + более-менее подходящий более-менее мейнстримный язык - это дешевле, на мой взгляд, чем ФП парадигма + чисто функциональный язык под который сложно найти разработчиков, библиотеки, документацию ко всему этому и решения не самых тривиальных проблем.
Но тут мы уходим в холивар, что такое ФП. В котором даже кложуристы с хаскеллистами не могут решить кто из них Труъ ФП.
Вот тут Вы в точку попали, но я с Вами не соглашусь ибо считаю все-таки что ЯП должен давать поддержку парадигмы, потому что помимо бойлерплейта есть еще надежность/гарантии, производительность, читаемость/выразительность. Я же должен использовать ФП потому что оно мне что-то дает, если я пишу на C в стиле ФП, оно конечно можно но от этого будет больше вреда чем пользы.
Приведу пару примеров.
Возьмем к примеру Rust которые реализует серьезные элементы ФП (к примеру типы суммы) используя Zero Cost Abstractions. Т.е. использования замыканий, ADT, ленивых вычислений на коллекциях для нас бесплатно по производительности. Для большинства других языков это имеет стоимость, причем зачастую не маленькую. Есть удобные средства типа pattern matching для работы с контейнерами и.т.д. Но это не FP язык.
Другой контрпример JS где фп сейчас очень популярно, при этом со стороны языка никак не поддерживается:
- это не производительно
- нет поддержки синтаксиса для композиции, карирования, нет PM, нет поддержки контроля чистых функций (как впрочем почти в любом яп)
- нет никаких гарантий со стороны рантайма что ктонибудь следующим коммитом не сломает все дописав в функцию не кошерную логику.
По итогу это не производительно, некрасиво и не надежно.
Вот выше Вы привели пример на Kotlin в виде функции calculateSalary(), лично для меня тут нет никакого ФП потому что вызывая эту функцию ЯП не предоставляет мне ни гарантий ни информации о том что она сделает. Завтра, кто-нибудь перепишет код и добавит туда побочные эффекты и она перестанет быть чистой, при этом остальной код не упадет, никто вообще не заметит этого. В Kotlin нет модификатора pure чтобы на уровне компиляции проверять чистоту функций, нет поддержки контроля эффектов функции. Т.е. это по сути все тот же процедурный код который можно "виртуально" пытаться делать functional like насколько это возможно, когда на проекте 1 человек.
Для меня все-таки ФП это про гарантии, про выразительность, а остальное пусть будет
FP like.Вы меня не верно поняли:) Я делаю вывод на основании эмпирического исследования Константина и собственного опыта. А эксперты и книги - это шло в разделе "Косвенные доказательства".
Спасибо за уточнение, видимо да не совсем.
Полагаю, основная причина - потому что в ВУЗах учат C/Java/Python etc.
Ну тут я бы с Вами поспорил. Тот же Kotlin набрал популярность без ВУЗов. Потому что была потребность.
includedlibrary
09.11.2023 04:50Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?
Имхо, потому что у него высокий порог вхождения. Плюс императивные языки существуют намного дольше, на них написано большая часть софта, специалистов с опытом, которые пишут на императивных языках больше, ещё и как правило первый язык, который начинающий программист изучает, - императивный. После этого может быть достаточно трудно перестроится на чистый функциональный язык, ну во всяком случае, мне было очень сложно
Vladimirsencov
09.11.2023 04:50Я бы не сказал, что императивные языки дольше существуют. Просто на протяжении многих десятилетий функциональные языки не обеспечивали нужной производительности.
murkin-kot
09.11.2023 04:50+1ФП хорош тем, что бьёт по рукам молодым разработчикам, заставляя вырабатывать привычку, которая сама по себе при работе с императивом выработается гораздо позже.
Но опытным разработчикам ФП тоже бьёт по рукам, не смотря на то, что они и без ФП отлично понимают, где можно и где нельзя все эти мутабельности с эффектами.
Ну и насильственное вырезание эффективных в широком диапазоне подходов (та же мутабельность) сильно влияет на производительность кода. Плюс императив действительно во многих случаях проще. Для такого же уровня простоты с ФП нужно тренировать извилины на запоминание ряда шаблонов, чего в императиве делать просто не нужно.
В итоге - молодёжь получает ряд полезных привычек, но может не получить понимания, в каких случаях эти привычки становятся вредными. Ну а опытные и без ФП получают хорошие результаты в части контроля сложности ПО, а вот на ограничения по оптимизации смотрят криво, ибо не дело языка решать за разработчика, как должна работать программа.
ЗЫ. Примеры переусложнённых решений от называющих себя опытными не являются доказательством преимуществ ФП.
alexeykuzmin0
09.11.2023 04:50+1Любопытный факт — с указателями Советский Союз смог догнать и перегнать Америку и придумал их в 1955 году, на 12 лет раньше западных учёных.
А как же люди в США программировали до 1967 года, без адресации данных-то? Что-то мне подсказывает, что этот любопытный факт неверен
trix
09.11.2023 04:50главная проблема JPA - 99.9% разработчиков не читали документацию и не понимают базовых вещей, сначала я в это не верил, и тщетно пытался на собеседованиях дождаться ответа от людей, которые сообщали, что работали с JPA/Hibernate 1-2-3 года, спрашивал всегда то, с чего документация начинается -- life cycle of entity objects.
jdbc чтения документации не требует, сам интерфейс исчерпывающе описывает суть работы, нужно только понимать основы реляционных баз и sql.breninsul
09.11.2023 04:50это явно не плюс JPA. Если большинство с ней не разбираются - значит дело в ней.
А вообще пытаясь добиться в ней тот запрос, который знаешь как руками написать - ну это очень странно.
Пример - несколько oneToMany полей-entity. Получаем декартово произведение и невозможность определить уникальность объектов.
Когда эта проблема элементарно решается group by и массивом
breninsul
09.11.2023 04:50Согласен, правда я предпочитаю просто использовать JdbcTemplate (вернее кастомный врапер вынесенный в библиотеку) и голый SQL.
Любой объект очень легко можно смапить через json(b), включая oneToMany в виде поля-массива.
dmitrii-bu
09.11.2023 04:50Как разработчик, потративший больше 5 лет на разработку бэкэнда на Scala typelevel могу сказать что там были свои недостатки с которыми было достаточно тяжело жить:
Сложность реализации функциональных библиотек довольно высокая. В случае бага разобраться в том, что происходит как по мне на порядок сложнее чем в классическом Java стеке. Вот пример, на который сам когда-то наступил.
Крайне запутанные хипдампы. Иногда возникают задачи по исследованию утечек памяти и вот тогда без хипдампа обойтись довольно трудно. Рантайм cats-effect, fs2 сильно усложняет этот процесс
-
Функциональный рантайм cats-effect не дружит с java.util.concurrent. Казалось бы, завернем все в Sync[F] и все у нас будет прекрасно. Нет, не будет. Блокировка нативных потоков влияет на экзекутор который используется рантаймом cats-effect и можно легко схлопотать дедлок так что даже Blocker[F] не всегда спасает. А если еще и fs2 стримы замешаны то дедлок может возникнуть в одном месте, а взорваться может совсем в другом (проверено на практике)
Да, в случае Scala ФП плюсы тоже имеются, но это не серебренная пуля которую нужно всегда и везде использовать по умолчанию просто потомучто кто-то сказал, что это круто.
nimishin
Спасибо, а можно пример проекта на github?
jdev Автор
Нет, к сожалению, публичного кода, который я бы сейчас стал приводить в пример у меня нет.
Могу предложить посмотреть на https://git.codemonsters.team/guides/ddd-code-toolkit/-/tree/dev - у Макса подход очень близок к моему, но у него структура функции-координатора на монадах.