Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение12 мин
Количество просмотров13K
Всего голосов 16: ↑13 и ↓3+15
Комментарии56

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

newtype App a = App
  { unApp :: ReaderT AppState IO a
  }
  deriving (Applicative, Functor, Monad, MonadReader AppState, MonadIO, MonadThrow, MonadCatch)

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

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

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

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

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

А как вы определяете ФП, простите, что оно у вас может быть императивным? Определение из вики (источника так себе, но для основы пойдёт):

In computer science, imperative programming is a programming paradigm of software that uses statements that change a program's state.

При этом, чистое ФП не содержит таких понятий, как statement и state. Вместо них используются referential transparent expression (function) и immutable (stateless) data.
Как вы, на основе этих понятий, планируете организовать императивное программирование?
В чистых ФП языках, тот же Haskell, даже IO - не реальные операции ввода-вывода, а декларативное описание связывания отдельных IO функций в IO pipeline, исполнение которого нарочно вынесено "за скобки" в языке.

Путь будет из ВИКИ:

In computer science, imperative programming is a programming paradigm of software that uses statements that change a program's state. In much the same way that the imperative mood in natural languages expresses commands, an imperative program consists of commands for the computer to perform. Imperative programming focuses on describing how a program operates step by step,[1] rather than on high-level descriptions of its expected results.
The term is often used in contrast to declarative programming, which focuses on what the program should accomplish without specifying all the details of how the program should achieve the result

Императи́вное программи́рование — это парадигма программирования (стиль написания исходного кода компьютерной программы), для которой характерно следующее:

  • в исходном коде программы записываются инструкции (команды);

  • инструкции должны выполняться последовательно;

  • данные, получаемые при выполнении предыдущих инструкций, могут читаться из памяти последующими инструкциями;

  • данные, полученные при выполнении инструкции, могут записываться в память.

Императивная программа похожа на приказы (англ. imperative — приказ, повелительное наклонение), выражаемые повелительным наклонением в естественных языках, то есть представляют собой последовательность команд, которые должен выполнить процессор.

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

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

Функциона́льное программи́рование — парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних
Функциональное программирование предполагает обходиться вычислением результатов функций от исходных данных и результатов других функций, и не предполагает явного хранения состояния программы.

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

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

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

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

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

Нет, в самом языке (его safe части), никаких императивных команд нету. Да и в unsafe части, программе предоставляется лишь функция unsafePerformIO, которую, к тому же, на практике нужно вызывать примерно в 0% случаев. И даже в этом проценте случаев, у вас нет контроля над тем, как именно исполняется IO, это делает компилятор и стандартная библиотека. Но если уж вы так пытаетесь натягивать императивную сову на декларативный глобус, то тогда выходит, что декларативных парадигм вообще не существует: рано или поздно всё это транслируется в набор машинных команд процессора, которые императивны по своей сути. Согласитесь, абсурд получается.

По вашему, можно из определения выкинуть существенную часть

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

Согласитесь, абсурд получается.

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

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

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

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

В определении я процитировал самую существенную часть.

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

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

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

Поэтому лучше не передергивать и не менять уровень абстракций.

Так и не передёргивайте, не меняйте уровень абстракций! Покажите, где в спецификации того же Haskell встречается понятие, эквивалентное понятию команда? Покажите, где в спецификации ленивого ФП языка что-то про последовательность исполнения этих самых команд? Нету? Вот и не надо тогда рассказывать сказки про императивность ФП!

Вот и не надо тогда рассказывать сказки про императивность ФП!

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

Конечно же можно. Если что-то на чём-то реализуемо, это вовсе не значит, что нельзя противопоставлять. С чего бы? Вот, например, тот же C вполне реализуем на языке ассемблера, но мы же их противопоставляем, говоря, что в C, в отличие от ассемблера, есть, например, типизация, структурность.

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

Опять же, ваши сходные слова:

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

Нет, нельзя ФП использовать в императивном стиле. Это уже будет не ФП, а использование нескольких парадигм!

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

Ладно. Я не вижу смысла в дальнейшей дискуссии. Вы слишком вольно пытаетесь толковать строгие определения, за которыми стоит ещё более строгая математика, а у меня нет сил проводить ликбез, в очередной раз.

Могу только посоветовать почитать про классификацию парадигм программирования, и почему они друг другу противопоставляются. Начать можете с той же википедии https://en.wikipedia.org/wiki/Declarative_programming, может поймёте, почему ФП в императивном стиле - абсурд.

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

sum :: Num a => [a] -> IO a
sum xs = do
    sumVar <- newIORef 0
    forM_ xs $ \x -> modifyIORef' sumVar (+x)
    readIORef sumVar

По мне, так это чистой воды императивщина. Можно вместо IO использовать ST, тогда функция будет возвращать чистый результат, но код останется таким же, просто вмето IORef будет STRef

Да, этот код похож на императивный, но таковым не является (do нотация - всего лишь синтаксический сахар поверх функкций >>= и >>).
То есть, ваша функция лишь декларирует, какую композицию побочных эффектов вы хотите, а не то, какие команды будут исполняться при её вызове.
Считать это императивным кодом - большая ошибка, которая может привести к некорректным выводам относительно того, как это вычисляется. Самый простой пример - монадическая функция return. В do нотации выглядит похоже на императивную команду возврата из процедуры, но работает иначе, в частности, функции, идущие на следующих строчках за ней, будут вычисляться, в отличие от команд, идущих на следующих строчках после команды return в императивных языках.

Что же до ST монады, то да, она, вместе с сахаром do нотации, позволяет декларировать чистые функции похоже на императивный код, но лишь похоже.

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

  • в исходном коде программы записываются инструкции (команды);

  • инструкции должны выполняться последовательно;

  • данные, получаемые при выполнении предыдущих инструкций, могут читаться из памяти последующими инструкциями;

  • данные, полученные при выполнении инструкции, могут записываться в память.

И да, про то, что return для монад отличается от return в императивных языках, я в курсе, однако одно это отличие не делает код декларативным. В haskell, я для раннего выхода могу те же исключения, например, использовать

Давайте разберёмся с вашим кодом.
Чтобы получить в Haskell хоть что-то похожее на императивное исполнение, вам понадобилось:

  1. Использовать монады, ведь без них не работает синтаксический сахар do нотации. Если что, подобный синтаксис просто приколочен сбоку в компиляторе, в чистом ФП, базирующемся на лямбда исчислении, никаких вам do.

  2. Использовать IORef, ведь в Haskell нет изменяемых переменных.

  3. Использовать специфическую функцию forM_ для организации чего-то, вроде цикла, ведь в Haskell нет циклов.

  4. Использовать специфическую функцию modifyIORef' для организации присвоения нового значения переменной.

  5. Даже после всего этого, синтаксис оказался довольно далёк от типичного императивного варианта (сравните с кодом на C или Java).

То есть, я бы сказал, что вы реализовали на основе ФП что-то похожее на императивный стиль, хотя и с оговорками.

Вот, кстати, как примерно выглядит ваш код после того, как из него убирается не ФП сахар (do нотация)

sum :: Num a => [a] -> IO a
sum xs =
    newIORef 0 >>=
    \sumVar -> (forM_ xs $ \x -> modifyIORef' sumVar (+x)) >>
    readIORef sumVar

Ну как? Сильно похоже на императивный стиль?

Использовать монады, ведь без них не работает синтаксический сахар do нотации.

Без do натации код остаётся императивным - всё так же задаётся последовательность команд и точно так же идёт изменение состояния, просто изменилась текстовая запись кода.

Использовать IORef, ведь в Haskell нет изменяемых переменных.

IORef и есть изменяемая переменная, и IORef является частью haskell.

Использовать специфическую функцию forM_ для организации чего-то, вроде цикла, ведь в Haskell нет циклов.

И что это меняет, если в результате получается цикл?

Даже после всего этого, синтаксис оказался довольно далёк от типичного императивного варианта (сравните с кодом на C или Java).

Не синтаксис делает код императивным

В данном примере согласен, код императивный.
Правда, посмотрев на него ещё раз, понял, что он императивный как раз потому, что не является чисто функциональным: функция modifyIORef' не является ссылочно прозрачной, так как осуществляет побочное действие по изменению значения переменной. Как следствие, лямбда \x -> modifyIORef' sumVar (+x), так же не является ссылочно прозрачной.

Более того, в случае с IO типом, сам тип возвращаемого значения говорит о том, что функция не чистая, к тому же, не существует safe способов получения из IO чистого значения, то есть, ваш подсчёт суммы, на самом деле, не совсем сумму считает, а порождает IO, в котором будет "обёрнута" сумма.

Если переделать IO на ST, то можно сделать саму функцию sum ссылочно прозрачной, но код её реализации от этого чисто функциональным не станет (используются не чистые функции и изменяемые значения).

То есть, получаем выбор: или чисто функциональный код без императивщины, или императивщина, но тогда без чистоты.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

Не совсем понял ваш посыл.

Константин, проводил свои исследования в 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)
}

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

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

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

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

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

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

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

Но тут мы уходим в холивар, что такое ФП. В котором даже кложуристы с хаскеллистами не могут решить кто из них Труъ ФП.

Вот тут Вы в точку попали, но я с Вами не соглашусь ибо считаю все-таки что ЯП должен давать поддержку парадигмы, потому что помимо бойлерплейта есть еще надежность/гарантии, производительность, читаемость/выразительность. Я же должен использовать ФП потому что оно мне что-то дает, если я пишу на 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 набрал популярность без ВУЗов. Потому что была потребность.

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

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

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

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

Для меня 2 причины:

1) плохие библиотеки и тулзы. Извините, но скажем библиотеку Blazer.HTML5 поставить и использовать не смог из-за непоняток с зависимостями и версиями. Угробил день.

2) когда начинаешь копать вглубь то Iceberg.jpg

ОТВЕЧАЕТ ChatGPT:

СЛОЖНОСТИ КОНЦЕПЦИИ

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

  1. Ленивость: Haskell использует ленивую (отложенную) вычислительную стратегию, что может быть непривычным для разработчиков, привыкших к строгим вычислениям. Ленивость может привести к неожиданным эффектам, таким как бесконечные структуры данных или нежелательное И НЕПРЕДСКАЗУЕМОЕ потребление памяти. Понимание и эффективное использование ленивости требует опыта и привыкания к особенностям языка.

  2. Строгая типизация может потребовать более детального и точного определения типов данных, что может занимать больше времени на этапе проектирования.

  3. Монады: Монады являются ключевым аспектом Haskell и позволяют работать с побочными эффектами, такими как ввод-вывод, и обеспечивают контекст выполнения. Понимание и использование монад может быть сложным для новичков. В частности, монадический синтаксис (do-нотация) может быть запутанным и требует понимания операторов связывания и функциональных композиций.

  4. Категории и функторы: Haskell базируется на категорной теории, и понимание категорий и функторов может быть сложным для разработчиков без математического фона. Функторы и другие абстрактные концепции, такие как аппликативные функторы и монады, могут потребовать времени и упорства в изучении.

  5. Рекурсия: Haskell активно использует рекурсию для обработки списков и других структур данных. Правильное проектирование и использование рекурсии может быть сложным для новичков, особенно при работе с большими данными или при решении сложных задач.

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

И СЛОЖНОСТИ ИНСТРУМЕНТАРИЯ (МОЗГ ТАК НЕ МЫСЛИТ)

Действительно, в Haskell существуют некоторые инструменты и библиотеки, которые могут быть сложными для понимания или использования, особенно для новичков. Вот несколько примеров:

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

  2. Стрелки (Arrows): Стрелки представляют собой расширение функционального программирования в Haskell, которое обеспечивает комбинирование и композицию функций в более общем контексте. Они могут быть использованы для описания и комбинирования потоков данных и вычислений. Однако, стрелки могут быть сложными для понимания, особенно для новичков, и их использование требует более глубокого понимания функциональных концепций и типовых классов.

  3. Монадический стек (Monad Transformers): Монадический стек позволяет комбинировать различные монады в Haskell, что позволяет элегантно работать с несколькими эффектами и контекстами выполнения. Однако, понимание и использование монадического стека может быть сложным, особенно при работе с более сложными комбинациями монад и управлении эффектами.

  4. Глубокие типовые классы (Deep Type Classes): Глубокие типовые классы - это техника, которая позволяет определять более сложные и мощные типовые классы с дополнительными операциями и ограничениями. Примерами таких классов являются Traversable, Foldable и Monoid. Понимание и использование глубоких типовых классов может быть сложным, особенно для новичков, и требует более глубокого понимания системы типов и типовых классов в Haskell.

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

  1. плохие библиотеки и тулзы. Извините, но скажем библиотеку Blazer.HTML5 поставить и использовать не смог из-за непоняток с зависимостями и версиями. Угробил день.

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

ОТВЕЧАЕТ ChatGPT:

Такой себе источник.

Ленивость: Haskell использует ленивую (отложенную) вычислительную
стратегию, что может быть непривычным для разработчиков, привыкших к
строгим вычислениям. Ленивость может привести к неожиданным эффектам,
таким как бесконечные структуры данных или нежелательное И
НЕПРЕДСКАЗУЕМОЕ потребление памяти. Понимание и эффективное
использование ленивости требует опыта и привыкания к особенностям языка.

Тут справедливо, в некоторых случаях придётся пользоваться профилировщиком и, возможно, устранять ленивость.

Строгая типизация может потребовать более детального и точного
определения типов данных, что может занимать больше времени на этапе
проектирования

Это наоборот плюс, меньше ошибок будете в рантайме и с помощью тестов отлавливать. Тесты, конечно, всё ещё будут нужны, но какие-то ошибки можно устранить чисто типами.

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

Как раз теория категорий необязательна для написания кода. Для общего развития можно почитать, но для написания и понимания совсем не требуется. Я теоркат стал читать после того, как научился работать с функторами, аппликативными функторами и монадами, в т.ч. трансформерами. И математического фона у меня тогда никакого не было, только математика с 1 по 9 класс, я тогда только начинал в 10 учиться. Но я согласен, основная сложность была именно в монадах, после легче пошло.

Рекурсия: Haskell активно использует рекурсию для обработки списков и
других структур данных. Правильное проектирование и использование
рекурсии может быть сложным для новичков, особенно при работе с большими
данными или при решении сложных задач.

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

И СЛОЖНОСТИ ИНСТРУМЕНТАРИЯ (МОЗГ ТАК НЕ МЫСЛИТ).

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

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

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

Deep Type Classes

Первый раз о таком термине слышу.

Примерами таких классов являются Traversable, Foldable и Monoid.
Понимание и использование глубоких типовых классов может быть сложным,
особенно для новичков, и требует более глубокого понимания системы типов
и типовых классов в Haskell.

Traversable, Foldable и Monoid не требуют глубокого погружения в типы. Эти классы типов можно спокойно, как интерфейсы воспринимать, в них вообще ничего сложно нет. Глубокое понимание системы типов нужно, когда пишешь что-то сложное на уровне типов, например, сортировку, но новички этим не занимаются, а при постепенном изучении и магия на типах становится доступной. При этом не сказал бы, что программирование на типах так уж часто нужно, особенно на начальных этапах. Со временем, конечно, начинаешь использовать, но поначалу можно легко и без этого обойтись.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

наибольшее сопротивление вызывает использование Spring Data JDBC, а не [Spring Data] JPA (де-факто стандарта работы с БД на платформе Java).

А есть какие-то исследования, подтверждающие, что JPA это стандарт?

Тинькофф, например, не использует JPA и ORM в Java сервисах. Revolut не использует Spring и JPA в принципе. У обеих компаний десятки миллионов клиентов.

А есть какие-то исследования, подтверждающие, что JPA это стандарт?

Я сам ограничился этим :)

У обеих компаний десятки миллионов клиентов.

Мне кажется тут надо смотреть не на количество клиентов, а на количество компаний/разработчиков.

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

А вот ещё вспомнил пост

"ФП виновно"... Заголовок у вас конечно отстой. ФП это что? "Финское правительство" что ли? Исправьте пожалуйста.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории