Введение

Среди особенностей моего подхода к разработке у моих заказчиков, коллег и студентов наибольшее сопротивление вызывает использование 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" (для просмотра необходимо пройти бесплатную регистрацию и "занять" книгу), Константин приводит иллюстрацию того, что он называет моделью, ориентированной на трансформацию (выделение цветами моё):

why fp 286c9
why fp 286c9

Упрощённо, эта модель разбивает код на четыре вида - ввод, вывод, трансформации и координацию.

Затем Константин пишет:

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 модуля (подпрограммы) буквально является определением чистой функции.


Таким образом, мы получаем:

  1. Трансформации физически не могли иметь никаких эффектов, кроме обращения к общему окружению;

  2. При этом Константин иллюстрирует "эффект общего окружения" картинкой, которая повергнет в ужас любого разработчика;

  3. И называет исправными те подпрограммы, которые обладают свойствами чистых функций - в том числе не обращаются к общему окружению.

Достаточно ли этого, для того чтобы прийти к выводу, что трансформации де-факто были чистыми функциями и, следовательно, модель, ориентированная на трансформации, является эквивалентом функциональной архитектуры? Я считаю что да.

Это, в свою очередь, значит что есть эмпирические свидетельства тому, что применение функциональной архитектуры ведёт к системам "дешёвым в разработке, обслуживании и модификации".

Это могло бы быть неопровержимым доказательством Гипотезы, если бы не одно но. Результаты этого исследования не были опубликованы в рецензируемом научном журнале, потому что "исходные данные и заметки были утеряны в беспорядочной гибели института".

Тем не менее, я считаю, что слова учёного с мировым именем заслуживают доверия. В том числе потому, что они подтверждаются другими экспертами-практиками и моим собственным опытом.

Косвенные подтверждения Гипотезы

ФП в книгах

Призывы максимум кода выделять в чистые функции встречаются во множестве книг, опубликованных начиная с 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. Оптимизирующие компиляторы не могут применять многие из оптимизаций к программам с эффектами из-за того, что не могут предсказать последствия этих оптимизаций ([1]

  2. Наличие оператора присваивания в программе существенно усложняет задачу её формальной верификации ([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. Это хоть и синтетический пример, но он основан на реальных событиях - из-за подобного кода я лично вносил баг, который нашли только в проде.

Для того чтобы снизить стоимость разработки, не надо знать теорию категорий

Пуристы во главе с Эриком Мейером со мной не согласятся, но прагматики во главе в анкл Бобом меня поддержат в том, что для получения пользы от ФП-стиля не обязательно уходить в идеально чистое функциональное программирование.

Для этого достаточно с помощью функциональной архитектуры разделить ввод-вывод и логику и сделать модель данных неизменяемой. А императивную оболочку, как это ни странно, намного удобнее писать в императивном стиле. И если локальная переменная не утекает за пределы функции - она вполне может быть изменяемой (см. примеры кода в конце поста).

Я не знаю единого хорошего источника по пролетарскому прагматичному ФП, но могу порекомендовать трек его изучения.

  1. Сначала стоит прочитать пост Владимира Хорикова - Иммутабельная архитектура.

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

  2. Затем можно так же быстро посмотреть ещё пару небольших примеров решения "реальных" задач в ФП-стиле в моих постах:

    1. Агрегаты;

    2. Ленивые вычисления для реализации функциональной архитектуры.

  3. После этого можно прочитать книгу того же Хорикова Unit Testing Principles, Practices, and Patterns (на русском)

    Тут больше деталей, но так как фокус книги всё-таки на тестировании, не хватает главной части — моделирования неизменяемых данных.

  4. Моделирование незименяемых данных хорошо раскрыто в книге Domain Modeling Made Functional.

    Тут уже прям ФП-ФП с монадами, но после материалов Хорикова у вас будет выбор - идти в эту кроличью нору или нет.

  5. Наконец, всё это можно полернуть свежей книгой анкл Боба Functional Design: Principles, Patterns, and Practices.

    В этой книге нет ни одного сложного эффекта (всё эффекты в памяти) и примеры на Clojure, но она хорошо иллюстрирует, тот самый пролетарский прагматичный стиль ФП.

Заключение

Итого мы имеем следующие факты:

  1. Учёный с мировым именем утверждает, что у него были эмпирические данные, свидетельствующие о том, что дешёвые программы имеют структуру, которая чрезвычайно напоминает функциональную архитектуру;

  2. Множество экспертов-практиков со страниц своих книг призывают возможный максимум кода выносить в чистые функции;

  3. Множество экспертов-практиков переходят с императивного стиля на функциональный;

  4. Множество вендеров включают принципы функционального подхода в основу своих технологий;

  5. Программы, написанные в функциональном стиле, проще верифицировать и оптимизировать;

  6. Писать многопоточные программы намного проще в функциональном стиле;

  7. При этом код в функциональном стиле может быть вполне понятен человеку без степени доктора математических наук.

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

Комментарии (36)


  1. nimishin
    09.11.2023 04:50
    +1

    Спасибо, а можно пример проекта на github?


    1. jdev Автор
      09.11.2023 04:50
      -5

      Нет, к сожалению, публичного кода, который я бы сейчас стал приводить в пример у меня нет.

      Могу предложить посмотреть на https://git.codemonsters.team/guides/ddd-code-toolkit/-/tree/dev - у Макса подход очень близок к моему, но у него структура функции-координатора на монадах.


  1. panzerfaust
    09.11.2023 04:50
    +2

    Среди особенностей моего подхода к разработке у моих заказчиков, коллег и
    студентов наибольшее сопротивление вызывает использование Spring Data
    JDBC, а не [Spring Data] JPA

    С общем посылом статьи согласен, но в вопросе выбора между этими двумя технологиями шо то черная магия, шо то черная магия. На мой вкус прям трушная иммутабельность - это MyBatis. Закидываешь иммутабельный объект в метод, получаешь иммутабельный результат из метода - всё, никаких сессий, состояний, актив рекордов и т.п. Плюс удобно писать сколь угодно безумные запросы с CTE, оконками и любыми причудами. Более хипстерский и приятный в использовании - JOOQ. Да, там можно работать с мутабельными Record, но можно и в чистом стиле. В этом плане он мультипарадигменный.


    1. sshikov
      09.11.2023 04:50

      И гдеж вы видите в Spring Data JDBC магию?


    1. jdev Автор
      09.11.2023 04:50

      и шо то черная магия, шо то черная магия

      Я согласен, что SDJ - тоже чёрная магия. Но, для меня по крайней мере, несопоставимо более простая чем JPA. С SDJ, я могу работать практически не заглядывая в доки и гугл (после прочтения этой самой доки и сбора базовых грабель), а с JPA постоянно приходилось гуглить заклинания, которые надо скастовать для получения требуемого результата.

      Плюс я сейчас в петпроекте эксперементирую с отказом от генерации репозов в пользу самописанных на базе jdbcAggregateTemplate - это ещё пласт магии уберёт

      Закидываешь иммутабельный объект в метод, получаешь иммутабельный
      результат из метода - всё, никаких сессий, состояний, актив рекордов и
      т.п. Плюс удобно писать сколь угодно безумные запросы с CTE, оконками и любыми причудами

      В SDJ всё точно так же.

      Плюс SDJ обещает в будущих версиях "решить проблему N+1 раз и навсегда" - это выглядит любопытно для меня.

      Мне MyBatis, jooq (+ Exposed) - тоже симпатичны. Но чего мне в них нехватает - возможности сохранить дерево объектов "автомагически".


      1. jdev Автор
        09.11.2023 04:50

        А и самое главное забыл - SDJ интегрируется с MyBatis из коробки:)

        То есть вы можете писать в БД через SDJ, со всеми его плюшками, а читать через MyBatis со всеми плюшками ещё и MyBatis.


  1. kovserg
    09.11.2023 04:50
    +1

    разработчику требуется меньше времени (=денег) на её изучение, для того чтобы внести требуемые правки.

    Вот удивительно все это понимают, но до сих пор нет языка запросов к исходникам программы (как sql) что бы её изучать(исследовать), модифицировать и проверять нарушения инвариантов.


    1. iliazeus
      09.11.2023 04:50

      Есть CodeQL, например.


  1. rsashka
    09.11.2023 04:50
    -1

    А почему вы противопоставляете функциональное и императивное программирование?
    Это же сравнение кислого с мягким, так как ФП может быть императивно.


    1. GospodinKolhoznik
      09.11.2023 04:50
      +3

      Что такое ФП каждый понимает по-своему, но как правило принято считать, что ФП обладает свойством иммутабульности данных, отсутствием побочных эффектов и referential transparency (Шишков прости: не знаю как перевести)

      По сути все преимущества, которые даёт ФП, оно даёт их за счёт этих 3 свойств. И разделение на ФП/не ФП имеет смысл проводить не по границе имеративный/не императивный, а про критерию выполняются или не выполняются эти 3 свойства.


      1. 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


        1. 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)


          1. includedlibrary
            09.11.2023 04:50

            Да, так и называется ReaderT pattern. В моём проекте все модули компилятора на нём построены


            1. GospodinKolhoznik
              09.11.2023 04:50

              Не знал )) просто сам к нему пришел после череды проб и ошибок. Сидел и игрался в песочнице, пытаясь то так то этак выстроить архитектуру упрощённого приложения, состоящего из сплошных mock-ов. И через несколько дней изобрёл велосипед.


    1. kpmy
      09.11.2023 04:50
      -1

      ФП подвид декларативного подхода, декларативный подход противопоставляется императивному. Возможно, это автор подразумевает.


      1. rsashka
        09.11.2023 04:50

        Я понимаю, что автор ошибочно считает ФП чисто декларативным подходом, поэтому и противопоставляет его императивному. Вот только ФП к данным парадигмам программирования не имеет вообще никакого отношения и его можно использовать как в декларативном, так и в императивном стиле программирования.

        Не говоря уже про то, что многие императивные языки программирования часто имеют в своей семантике декларативные конструкции, превращая чисто императивное программирование в комбинацию императивных/декларативных стилей, в зависимости от пожеланий разработчика.


  1. kipar
    09.11.2023 04:50
    +2

    насчёт оптимизации я бы поспорил. Большинство придуманных людьми алгоритмов и структур данных подразумевают мутабельность. Как с помощью ФП сделать inplace сортировку? хеш-таблицу с открытой адресацией? LU разложение матрицы? Так что ФП, в моем понимании, работает на уровне архитектуры, а низкоуровневые функции все равно будут работать с мутабельными данными. Ну а компилятор, по большей части, оптимизирует как раз низкоуровневые функции.


    1. jdev Автор
      09.11.2023 04:50

      Я ни в коем случае не утверждаю, что ФП-программы быстрее императивных программ (хорошо написанных).

      Этот пример не про то, что ФП-программы быстрее, а про то, что компилятор лучше "понимает" ФП программы и я это использую как аргумент в пользу того, что ФП-программы "понятнее".


      1. Vladimirsencov
        09.11.2023 04:50

        Часто работа с IO потребляет в десятки раз больше ресурсов, чем работа процессора и памяти. В таких случаях нет смысла оптимизировать код программы. А так да работа с иммутабильными объектами намного медленнее.


    1. dyadyaSerezha
      09.11.2023 04:50

      Тоже с трудом представляю, как функционально работать, например, с очередью неких данных/заданий, например, которая пополняется и обрабатывается несколькими потоками.


      1. jdev Автор
        09.11.2023 04:50

        С реальным миром (с состоянием) невозможно работать в чистом функциональном стиле. Именно эту проблему и решает функциональная архитектура - разделяет императивный код, который работает с реальным миром и чистый код, который работает с красивыми моделями.

        Соответственно у вас должен быть какой-то инфраструктурный код, который занимается работой с очередью - с одной стороны помещает данные в очередь, а с другой стороны - достаёт.

        А вот что будет перед и после этого кода - зависит от задачи.

        Как вариант, у вас на обеих сторонах может быть по координатору

        Координатор на входе получает запрос по хттп идёт в БД, достаёт оттуда неизменяемую структуру данных, передаёт её в чистое ядро, получает результат обратно и перекладывает его в инфраструктурный модуль для публикации в очередь.

        Координатор на выходе получает запрос от инфраструктурного кода и делает всё тоже самое, вплоть до публикации в следующую очередь.


  1. Lewigh
    09.11.2023 04:50
    +10

    Учёный с мировым именем утверждает, что у него были эмпирические данные, свидетельствующие о том, что дешёвые программы имеют структуру, которая чрезвычайно напоминает функциональную архитектуру;

    Ученые с мировым именем десятилетиями до этого продвигали ООП подход как богоизбранный. Тогда не существовало ФП? Просто пришла новая мода и пришел новый богоизбранный подход, при этом то что за эти десятилетия так и не смогли определится что же такое ООП и как на нем писать видимо не имеет значения.

    Множество экспертов-практиков со страниц своих книг призывают возможный максимум кода выносить в чистые функции;

    Множество экспертов-практиков переходят с императивного стиля на функциональный;

    При этом мы не наблюдаем взрыва популярности языков программирования которые хоть как-то поддерживают ФП парадигму более менее достойно, вроде Scala, F#. Мы наблюдаем как множество экспертов-практиков пишут все в тех же языках с императивной парадигмой вроде Java, C# и т.д., у нас популярны современные ЯП такие как Kotlin и Go, в первом открыто заявлялось разработчиками что, не смотря на поддержку налета ФП, - это не про ФП, про Go я вообще молчу где он и где ФП. Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?

    Множество вендеров включают принципы функционального подхода в основу своих технологий;

    Да и все потому что религиозная догма под название ООП дала трещину и оказалось что есть другие подходы и у них есть преимущества и даже можно их совмещать.

    Программы, написанные в функциональном стиле, проще верифицировать и оптимизировать;

    Ну с первым пунктом я соглашусь а вот по повода второго, я бы с интересом посмотрел как бы Вы написали в функциональном стиле что-то требующее производительности, к примеру игру, а потом героически просто ее оптимизировали.

    При этом код в функциональном стиле может быть вполне понятен человеку без степени доктора математических наук

    Ну да ну да. Не нужно быть доктором наук чтобы изучать китайский язык, видимо это означает что китайский и английским можно изучить плюс минус.

    Достаточно ли этих фактов, для доказательства того, что функциональный стиль снижает стоимость разработки?

    Нет. В качестве доказательств нужна статистика, сколько что и за сколько производится с описанием почему. Делать выводы что ФП ускоряет разработку на основании что эксперты что-то там в своих книгах пишут - не очень серьезный подход


    Мне тоже очень нравится ФП подход но не нужно из него делать очередную религию.


    1. GospodinKolhoznik
      09.11.2023 04:50
      +3

      Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?

      Не знаю, сам об этом думал. Какая то загадка дыры. Ведь Haskell идеален для создания именно масштабных и нетривиальных проектов. По своему опыту могу сказать, что он для этих целей лучше даже чем Java (а значит и её клон C#, ну и Kotlin) и уж тем более лучше, чем такие "опасные" языки, как Python или C++. Потому что большой и сложный проект всегда создаётся "по спирали" от крошечного и примитивного, до все больше и больше сложного, путем бесконечного рефакторинга и дополнения уже существующего кода. Невозможно написать что то большое и значимое просто с нуля все продумав, составив ТЗ, придерживаться его и сразу всё сделать хорошо. А преимущества Haskell раскрываются именно на рефакторинге кода, из всех языков, которые я знаю на нем удобнее и безопаснее всего проводить рефакторинг.

      Наверное ответ в том, что сейчас почти никто не делает больших и значимых проектов. Рыночек порешал, и все компании заняты написанием стандартизированных и однотипных приложений с минимальными модификациями. И в таком подходе Хаскель действительно нафиг не нужон. Шлепать однотипные приложения по одному и тому же лекалу-фреймворку удобнее на JS или Python.


      1. murkin-kot
        09.11.2023 04:50

        Ведь Haskell идеален для создания именно масштабных и нетривиальных проектов

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

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

        Поэтому остаётся лишь эмоциональное нравится/ненравится.


    1. 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)
      }

      Для меня - ФП. Нужна ли для докторская для чтения этого кода? Нет

      Нет. В качестве доказательств нужна статистика, сколько что и за сколько производится с описанием почему

      Так Константин ровно то и проделал. Взял кучу программ с известной стоимостью, посмотрел что общего между дешёвыми программи и в чём разница с дорогими. Увидел что разница - в функциональной архитектуре.

      Другое дело, что исходных данных нет - это да. Но так я и говорил о Гипотезе:)

      Делать выводы что ФП ускоряет разработку на основании что эксперты что-то там в своих книгах пишут - не очень серьезный подход

      Вы меня не верно поняли:) Я делаю вывод на основании эмпирического исследования Константина и собственного опыта. А эксперты и книги - это шло в разделе "Косвенные доказательства".


      1. 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 набрал популярность без ВУЗов. Потому что была потребность.


    1. includedlibrary
      09.11.2023 04:50

      Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?

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


      1. Vladimirsencov
        09.11.2023 04:50

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


  1. murkin-kot
    09.11.2023 04:50
    +1

    ФП хорош тем, что бьёт по рукам молодым разработчикам, заставляя вырабатывать привычку, которая сама по себе при работе с императивом выработается гораздо позже.

    Но опытным разработчикам ФП тоже бьёт по рукам, не смотря на то, что они и без ФП отлично понимают, где можно и где нельзя все эти мутабельности с эффектами.

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

    В итоге - молодёжь получает ряд полезных привычек, но может не получить понимания, в каких случаях эти привычки становятся вредными. Ну а опытные и без ФП получают хорошие результаты в части контроля сложности ПО, а вот на ограничения по оптимизации смотрят криво, ибо не дело языка решать за разработчика, как должна работать программа.

    ЗЫ. Примеры переусложнённых решений от называющих себя опытными не являются доказательством преимуществ ФП.


  1. alexeykuzmin0
    09.11.2023 04:50
    +1

    Любопытный факт — с указателями Советский Союз смог догнать и перегнать Америку и придумал их в 1955 году, на 12 лет раньше западных учёных.

    А как же люди в США программировали до 1967 года, без адресации данных-то? Что-то мне подсказывает, что этот любопытный факт неверен


  1. trix
    09.11.2023 04:50

    главная проблема JPA - 99.9% разработчиков не читали документацию и не понимают базовых вещей, сначала я в это не верил, и тщетно пытался на собеседованиях дождаться ответа от людей, которые сообщали, что работали с JPA/Hibernate 1-2-3 года, спрашивал всегда то, с чего документация начинается -- life cycle of entity objects.
    jdbc чтения документации не требует, сам интерфейс исчерпывающе описывает суть работы, нужно только понимать основы реляционных баз и sql.


    1. breninsul
      09.11.2023 04:50

      это явно не плюс JPA. Если большинство с ней не разбираются - значит дело в ней.

      А вообще пытаясь добиться в ней тот запрос, который знаешь как руками написать - ну это очень странно.

      Пример - несколько oneToMany полей-entity. Получаем декартово произведение и невозможность определить уникальность объектов.

      Когда эта проблема элементарно решается group by и массивом


  1. nApoBo3
    09.11.2023 04:50

    Чудовищно далеко все это от народа.

    ЗЫ: у кого-нибудь в проектах по основному месту работы есть формальная верификация?


    1. kpmy
      09.11.2023 04:50

      Да, верификация проекта для нас чистая формальность.


  1. breninsul
    09.11.2023 04:50

    Согласен, правда я предпочитаю просто использовать JdbcTemplate (вернее кастомный врапер вынесенный в библиотеку) и голый SQL.

    Любой объект очень легко можно смапить через json(b), включая oneToMany в виде поля-массива.


  1. dmitrii-bu
    09.11.2023 04:50

    Как разработчик, потративший больше 5 лет на разработку бэкэнда на Scala typelevel могу сказать что там были свои недостатки с которыми было достаточно тяжело жить:

    1. Сложность реализации функциональных библиотек довольно высокая. В случае бага разобраться в том, что происходит как по мне на порядок сложнее чем в классическом Java стеке. Вот пример, на который сам когда-то наступил.

    2. Крайне запутанные хипдампы. Иногда возникают задачи по исследованию утечек памяти и вот тогда без хипдампа обойтись довольно трудно. Рантайм cats-effect, fs2 сильно усложняет этот процесс

    3. Функциональный рантайм cats-effect не дружит с java.util.concurrent. Казалось бы, завернем все в Sync[F] и все у нас будет прекрасно. Нет, не будет. Блокировка нативных потоков влияет на экзекутор который используется рантаймом cats-effect и можно легко схлопотать дедлок так что даже Blocker[F] не всегда спасает. А если еще и fs2 стримы замешаны то дедлок может возникнуть в одном месте, а взорваться может совсем в другом (проверено на практике)

      Да, в случае Scala ФП плюсы тоже имеются, но это не серебренная пуля которую нужно всегда и везде использовать по умолчанию просто потомучто кто-то сказал, что это круто.