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

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


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

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

С SQL тут все-таки переборщили: это и правда неудобный язык. Расположение SELECT перед FROM приводит к постоянным использованием имён таблиц перед их объявлением, а это ломает контекстные подсказки в IDE. Не говорю уже о том, что мне вообще неизвестны редакторы SQL, которые бы не тупили и не тормозили.


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

делегатов/лямбд/стрелочных функций

Так это все делалось для приближения ООП к ФП.

Но от "приближения" оно не перестаёт быть ООП. Однако, оно перестаёт содержать страшные классы, введенные ради всего одного метода.

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

Если вы не утверждали, что ООП перестало быть ООП — то непонятно с чем вы спорите.

Это больше карго-культ, если честно. Смысл вышек и самолетов… Простите, лямбд и стрелочных функций не в том, чтобы просто быть.

Но это не нечто новое, привнесенно-ФПшное. Комбинирование ООП подхода с самыми обычными императивными функциями/процедурами без нужды держать классы ради одного метода без сохранения контекста есть, скажем, в том же Object Pascal-Delphi…
если ты сказал про селект. То куда удобнее, компактнее и читабольшее исользовать синтаксис ФП например стрима:
select(actor, id, last_name -> text) -> filter(text -> text.startswith('A')).
При этом фича ФП будет также присвоение функции имени переменной, то есть с возможностью использовать опять:
aactor_queuery = select(actor, id, last_name -> text) -> filter(text -> text.startswith('A'))
aactor_queuery -> filter(...)

также рекурсия что то вроде:
select_rec = select(table, id,last_name, next_id) -> filter(next_id in select_rec.id)
я это понял вот на этом абзаце:

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


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

Функциональное программирование полно недочётов, оно не подходит для реальных проектов


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

Но вот после математики которая не находит применения в реальном мире, становится ясно, что либо автор глубоко и безнеадежно болен, либо он имеет университетский диплом юмориста-затейника, но уже много лет не может получить работу по специальности — чтобы определить точнее, нужно доситать статью, а мне уже лень
Я после первого примера ФП кода понял, что что-то не так O_o
Астрологи объявили неделю жЫрных набросов. Объемы сальной прослойки в районе пуза увеличиваются вдвое.

P.S. Годно, очень годно! Ждем часть 2.
Хороший стеб однако.

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

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


  • ФП работает с "чистыми" функциями. Но реальное приложение работает с файлами, БД, внешними сервисами, которые нарушают требования "чистоты", в итоге приходится придумывать костыли
  • в ФП переменные иммутабельны. Если у вас есть массив из 100 пользователей, и вы должны одному обновить рейтинг, вы должны сделать полную копию массива. И так на каждое изменение. Это негативно влияет на производительность и потребление памяти.(пример: реакт, где на любой чих пересоздается стейт заново и хорошо, если он у вас маленький).
  • в ФП переменные иммутабельны, а структуры это не объекты и они передаются по значению, а не по ссылке. Там, где вы в PHP пишете $user->updateKarma(), в ФП вы должны писать user2 = updateKarma(user), и не забыть заменить старые копии user на user2 во всех местах кода, во всех коллекциях и списках. Удачи!
  • в ФП нет исключений. В нормальном программировании вы просто пишете действия подряд, если произойдет ошибка, выбросится исключение и оставшиеся не будут выполняться. В ФП вам приходится лепить костыли, делая типы вроде Maybe и делая "пропуск" функции, если ей передано Maybe с ошибкой внутри.
  • в ФП нет ООП, которым удобно представлять объекты реального мира. Вместо этого там приходится делать разрозненные структуры и функции для работы с ними — то, для замены чего и придуман ООП. А ведь в ФП вы еще не можете модифицировать объект, с которым работаете.
  • в ООП функуция это просто последовательность шагов: 1) проверь, что такого емайла нет 2) добавь запись в БД 3) отправь письмо для подтверждения почты. В ФП же так не принято, а принято комбинировать функции в составные функции, из-за чего разбор кода превращается в кошмар (registrator = (formData) => combine(formValiadtor(rules), fieldExtractor('email'), emailNotInDbChecker(db), emailToDbAdder(db), emailSender(db, sendService)).

Любимый паттерн разработчиков ФП. и JS — это сделать в одном файле определения функций (причем к которым нельзя перейти по клику или найти поиском), а в другом — их вызвать. Типичный пример (похожий код есть в jQuery):


var attrs = ['id', 'name', 'age'];
attrs.forEach((attr) => {
root['get' + attr] = (x) => root[x];
}


Удачи вам найти определение функции getName поиском.


А ФП вроде Хаскелл позволяет создать 10 вариантов функции с одинаковым именем, но разным типом аргументов. Удачи вам найти при рефакторинге, какая из функций (раскиданных по разным файлам) вызывается в том или ином случае.


Но, конечно, в программах для вычисления чисел Фибоначчи ФП не знает себе равных.

в ФП нет исключений

Вообще-то есть, аж в двух видах — в виде, собственно, исключения (которое условно типизируется как ⊥) и в виде Either-подобной монады.


Любимый паттерн разработчиков ФП. и JS

Первый раз такое вижу.


Удачи вам найти определение функции getName поиском.

Если речь идет о JS — то пишем getName в консоли и дальше браузер сам найдет эту функцию.


А ФП вроде Хаскелл позволяет создать 10 вариантов функции с одинаковым именем, но разным типом аргументов

Мало чем отличается от полиморфизма в ООП. Да, в ООП функция лежит всегда рядом с объектом — зато тип этого объекта будет известен только в рантайме.

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

Запускаем ноду с ключом --inspect и подключаемся через chrome://inspect...

Если речь идет о JS — то пишем getName в консоли и дальше браузер сам найдет эту функцию.
О чём вы? Она ведь в замыкании. Никакой функции в глобальном скоупе нету. И что нам это даст? Ну найдём мы её в консоли, что дальше?
О чём вы? Она ведь в замыкании. Никакой функции в глобальном скоупе нету.

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


И что нам это даст? Ну найдём мы её в консоли, что дальше?

А дальше можно перейти к её определению кликнув на неё левой кнопкой мыши. А также при необходимости посмотреть все замкнутые переменные.

Но ведь откуда-то мы её получили, раз уж вопрос о поиске определения вообще возник? Вот там где получили, ставим точку останова, и теперь она в текущем скоупе есть.
Звучит как безумно удобно, класс! Не то, что ctrl+click в IDE, гадость какая

Напомню, что это функция может вызываться только раз в 3 месяца, когда полная Луна находится в фазе льва. Будете ждать фазу льва, чтобы отдебажить или просто поменяете время на компе?
Звучит как безумно удобно, класс! Не то, что ctrl+click в IDE, гадость какая

С другой стороны, ctrl+click в IDE хорошо работает только в пределах проекта, а в javascript можно точно так же увидеть исходники многих библиотек.


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

Напомню, что нам не нужно ждать пока функция будет вызвана, нам нужно ждать пока она станет видима.

в javascript можно точно так же увидеть исходники многих библиотек.
В которые тоже можно прыгнуть по ctrl+click. Если они нормально написаны, конечно.

Напомню, что нам не нужно ждать пока функция будет вызвана, нам нужно ждать пока она станет видима.

1. Что тоже может зависеть от фазы Луны
2. Если функция вызывается только через строковый параметр, то мы можем банально не знать, где она вызывается, пока она не будет вызвана.

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

Если мы не знаем ни где функция вызывается, ни где она определена — то мы вообще не знаем про её существование. Так откуда в таком случае исходная задача взялась?

Если в проекте принято вызывать функции через конкатенацию строк, то у нас появляется проклятая медаль с двумя сторонами:
1. Мы не знаем все функции, которые можем вызвать из данного места.
2. Мы не знаем все места, из которых может быть вызвана данная функция.

Это два стула, на которые нужно садиться по очереди.

А где вы увидели вызов функции через конкатенацию строк? Я вижу лишь объявление трёх шаблонных функций-акцессоров.

У Вас получилось чуть потоньше, чем у автора, но всё еще есть куда стремится. Я бы поменял про исключения. Все-таки ```catch(AnyException e) { log(e); }``` – это говнокод даже по меркам современного индус-триального программирования.

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

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

ООП работает с "объектами". Но реальное приложение работает с файлами, БД, внешними сервисами, которые реализованны на чистом С, в итоге приходится придумывать костыли


в ФП переменные иммутабельны. Если у вас есть массив из 100 пользователей, и вы должны одному обновить рейтинг, вы должны сделать полную копию массива.

Приложение на хаскелле который делает "копию на каждый чих" скорее всего будет производительнее вашего мутабельного варианта на Java/C#/… И да, copy elision, даже С++ умеет.


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

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


в ФП нет исключений.

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


в ФП нет ООП, которым удобно представлять объекты реального мира.

Это шутка?


в ООП функуция это просто последовательность шагов

Да, только в ООП у вас 100500 зависимостей IFooBaz у которых тоже миллион зависимостей, что-то куда-то как-то передается, и проследить флоу становится нереально. Особенно если мы какой-нибудь DI заюзаем, с сессионным резолвом зависимостей и т.п. Ух как весело становится.


Любимый паттерн разработчиков ФП. и JS — это сделать в одном файле определения функций (причем к которым нельзя перейти по клику или найти поиском), а в другом — их вызвать. Типичный пример (похожий код есть в jQuery):

А где тут ООП? Обычный императивный код, который грязно мутирует глобальный стейт. Как вам такой ООП код:


var attrs = ['id', 'name', 'age'];
attrs.forEach((attr) => {
root['get' + attr] = (x) => root[x];
}

Удачи вам найти определение класса реализующего обновление рута поиском.


А ООП вроде Java позволяет создать 10 классов наследующихся от одного интерфейса, но разными имплементациями. Удачи вам найти при рефакторинге, какой из классов (раскиданных по разным файлам) вызывается в том или ином случае.
Приложение на хаскелле который делает "копию на каждый чих" скорее всего будет производительнее вашего мутабельного варианта на Java/C#/…

Приведите бенчмарк что ли в подтверждение своих слов.


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

А что вы делаете, когда выясняете, что приложение тормозит? Или скажете, что приложения написанные на ФП языке не тормозят?


Одно время в дотнете помню ненаблюдаемое исключение в асинхронной операции могло уронить рантайм.

Как видим, асинхронное программирование — та ещё ерунда.

А что вы делаете, когда выясняете, что приложение тормозит?

Профилирую, как это делается в любых языках.

Зависит. Чаще всего меняю алгоритм, реже добавляю/убираю {-# LANGUAGE Strict #-} в каком-нибудь из модулей, ещё реже начинаю играться с аннотациями строгости конкретных функций и байндингов. Пару раз за всю практику переписывал код в ST.

То есть вам нужно знать как ваш код компилируется, чтобы подшаманить и он компилировался во что-то более эффективное. И не факт, что эти шаманства не дадут обратный эффект при обновлении компилятора. В JS это постоянная беда. JIT компилятор даже между запусками может всё по разному соптимизировать исходя из кучи эвристик. Идиомы типа "передача по ссылке" хотя бы детерминированы.

Нет, зачем мне знать, как он компилируется? Речь ведь о семантике выполнения. Компилятор может сделать лучше, если он сам выведет более эффективную strictness annotation, но хуже он не сделает точно.

ООП работает с "объектами". Но реальное приложение работает с файлами, БД, внешними сервисами, которые реализованны на чистом С, в итоге приходится придумывать костыли

"Чистый C" часто очень ООПный. Обычно есть "create"/"open"/"new", "delete"/"remove"/"close" и зоопарк функций, принимающих первым аргументом хэндл. Различие с ООП исключительно синтаксическое. Перекладывается подобный API из сишного в ООПный и обратно очень тонкими синтаксическими прослойками.


Приложение на хаскелле который делает "копию на каждый чих" скорее всего будет производительнее вашего мутабельного варианта на Java/C#/… И да, copy elision, даже С++ умеет.

Есть разные алгоритмы, и где-то выгоднее иммутабельность, а где-то она убивает производительность. Если нужна максимальная скорость, алгоритм всё равно будут писать на C++ или подобном языке.

  1. Любая работа с ресурсами — это костыли. Вообще все функции кроме чистых — это костыли. Либо это dispodsble, либо это RAII — все одно. Вот только ФП не создает иллюзии, что ты работаешь безопастно.
  2. Точно. Это позволяет программисту почаще задумываться о структурах данных, которые он использует — например в вашем примере с пользователями это был бы связный список или хеш, которые были бы практически столь же эффективны в io нагруженных приложениях. Но вот для складывания матриц в GPU ФП скорее всего не подходит.
  3. Благодаря function as first class, сопоставлению с образцом и грамотному написанию кода в ФП в большинстве случаев не используется операция присваивания (=) — "переменные" не нужны.
  4. Исключения — это костыль. В отличие от монады. И это математически доказал Дейкстра в своих основах структурного программирования, которые исключения жестко нарушают. Кроме того, "костыли" в виде Maybe уже написаны за нас. Использовать их — не сложнее чем написать ключевое слово raise.
  5. ФП не противоречит ООП. Кроме того, вызовы u.foo() и foo(u) ничем ни отличаются. Это хорошо видно к примеру в питоне, где метод первый аргументом принимает в обязательном порядке self. Модификация объекта — это головная боль с того момента, как программе появляется многопоточность. А она появляется во всех "серьезных" программах.
  6. Это называется декларативное программирование — вершина грамотной архитектуры и композиции программных систем. Приучить мозг, который ходит по шагам, к понимаю декларативных описаний занимает несколько недель — если конечно захотеть.
Любая работа с ресурсами — это костыли.

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


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

Сколько матриц на ООП вы сегодня сложили? Сириузли, когда у вас проблемы с производительностью, вы и ООП использовать не будете, потому что известная AoS vs SoA проблема.


Благодаря function as first class, сопоставлению с образцом и грамотному написанию кода в ФП в большинстве случаев не используется операция присваивания (=) — "переменные" не нужны.

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


4

По этому пункту нет возражений, спасибо за уточнение.


ФП не противоречит ООП.

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


6

Не понял, к чему это. Декларативное описание, конечно, всегда лучше, потому что позволяет думать о бизнес-логике, а не бойлерплейте. Спасибо за еще одно уточнение.

когда у вас проблемы с производительностью, вы и ООП использовать не будете, потому что известная AoS vs SoA проблема.

Если я поравильно понял о чём вы, то это проблема лишь языков типа Java. В нормальных языках объекты можно размещать прямо в массиве. Не без ограничений, конечно, но всё же.

Нет, это прежде всего проблема для языков типа Си. В Java с этим все настолько плохо, по умолчанию, что проблема AoS vs SoA уходит на второй план. Но годится в качестве примера важности порядка размещения элементов в памяти.

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

Ну массив из полиморфных объектов в C++ сделать не получится. А если не делать их полифорными, то ООП тут не при чём.

  1. Можно и полиморфный массив сделать.
  2. ООП и полиморфизм ортогональные понятия.
Можно и полиморфный массив сделать.

Как это сделать в C++?


ООП и полиморфизм ортогональные понятия.

Нет, это не так. Без полиморфизма не будет ООП.

Как это сделать в C++?

Через Variant или как он там в C++ называется.


Нет, это не так. Без полиморфизма не будет ООП.

Вполне себе будет. Суть ООП в инкапсуляции, а не морфизмах.

Через Variant или как он там в C++ называется.

Покажите код, очень интересно.


Суть ООП в инкапсуляции, а не морфизмах.

Если убрать инкапсуляцию, то можно договориться, что поля, которые начинаются с _ или кончаются на _, трогать нельзя, как это делалось в php в джаваскрипте и по моему в питоне. И будет ООП.


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

Покажите код, очень интересно.

https://dlang.org/phobos/std_variant.html#VariantN


Если убрать инкапсуляцию, то можно договориться, что поля, которые начинаются с или кончаются на , трогать нельзя, как это делалось в php в джаваскрипте и по моему в питоне. И будет ООП.

Это вы говорите про сокрытие. Инкапсуляции она ортогональна. https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%BA%D0%B0%D0%BF%D1%81%D1%83%D0%BB%D1%8F%D1%86%D0%B8%D1%8F_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)


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

Не кончится. Кончится лишь полиморфизм.

Мало того, что в ответ на просьбу показать код на C++ вы показали.код на D, так он ещё и не создаёт массив полиморфных объектов ))


Это вы говорите про сокрытие. Инкапсуляции она ортогональна.

Не вопрос, можно сделать объект, в котором вообще нет данных. И, если он полиморфный, то это будет всё ещё ООП.


Не кончится. Кончится лишь полиморфизм.

Вы знаете ООП системы в которых не используется полиморфизм?

на просьбу показать код на C++ вы показали.код на D

Аналогичный код на C++ вы можете написать самостоятельно. Шаблоны это позволяют.


он ещё и не создаёт массив полиморфных объектов

Он создаёт полиморфный контейнер. Можете сделать массив этих контейнеров — получится полиморфный массив.


можно сделать объект, в котором вообще нет данных. И, если он полиморфный, то это будет всё ещё ООП

А я говорил, что существование объектов без поведения или без состояния — это не будет ооп?


Вы знаете ООП системы в которых не используется полиморфизм?

1C :-D

Аналогичный код на C++ вы можете написать самостоятельно. Шаблоны это позволяют.

Мог бы, я бы не спрашивал )). Можно пример создания массива полиморфных объектов, которые наследуются от абстрактного класса Shape?


А я говорил, что существование объектов без поведения или без состояния — это не будет ооп?

Вы говорили, что суть ООП инкапсуляции. Значит если её убрать — будет уже не ООП?


1C :-D

С козырей зашли? ))

Вы правда думаете, что я сейчас попрусь вспоминать этот убогий C++, чтобы вам что-то доказать?


А если вам ножки отрезать, вы перестанете быть человеком? Так же и инкапсуляция никуда не девается, если объекту пока что не нужно состояние.

Вы правда думаете, что я сейчас попрусь вспоминать этот убогий C++, чтобы вам что-то доказать?

Действительно. Тогда можно пример создания массива полиморфных объектов, которые наследуются от абстрактного класса Shape, только на D?


Так же и инкапсуляция никуда не девается, если объекту пока что не нужно состояние.

А как тогда продемонстрировать, что суть ООП в инкапсуляции?

Спасибо за пример. Вот эта строчка всё портит.


alias AnyShape = Algebraic!(Cyrcle,Square,Rect);

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


shapes[0].x.writeln; 

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


Это следует из определения объекта.

Объект это набор методов, которые чем-то манипулируют. Поля для объекта не обязательны.

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

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

Не соглашусь — если у объекта нет внутренних данных, сохраняемого контекста, то такой объект практически бессмысленен. Это просто набор функций с namespace'ом. КМК.
Я недаром сказал «практически» (в значении «почти»). Да, есть случаи, как упомянутый вами, когда такие объекты нужны из чисто технических соображений и подобный объект является базовым, используется полиморфизм; но, пмсм, то же может быть достигнуто без объектов, на функциях в большинстве случаев. Особенно учитывая ход дискуссий в комментариях о том, нужно ли наследование как обязательный признак, или нет.
Получается, что при добавлении ещё одного потомка нужно будет её поправить.

Ужас-то какой..


Смысл полиморфизма в том, чтобы можно было добавлять реализации не внося такие правки.

Это уже ваши фантазии.


написать shapes[0].x.writeln; не получается

https://run.dlang.io/is/asBYYV


вызвать методы тоже не выйдет.

А давайте вы поверите мне на слово, что всё это тоже не реализуемо?


Объект это набор методов, которые чем-то манипулируют. Поля для объекта не обязательны.

Главное, что клиентскому коду не надо знать что там внутри объекта.

Ужас-то какой… [что смысл полиморфизма в том, чтобы можно было добавлять реализации не внося такие правки] Это уже ваши фантазии

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


А давайте вы поверите мне на слово, что всё это тоже реализуемо?

Охотно верю, возможности языка впечатляют.


Главное, что клиентскому коду не надо знать что там внутри объекта.

То есть объединение методов и данных в одном объекте это не самое важное? А важно, чтобы клиент не знал как реализованы методы, да? Кстати, такие методы, реализация которых выбирается в рантайме, как раз называюся полиморфными )))

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

Я вам по секрету скажу — эти типы можно передавать библиотеке как статические параметры, а не хардкодить внутри.


То есть объединение методов и данных в одном объекте это не самое важное? А важно, чтобы клиент не знал как реализованы методы, да? Кстати, такие методы, реализация которых выбирается в рантайме, как раз называюся полиморфными )))

Не знать как устроен объект и работать с разными объектами — ортогональные вещи. У вас какая-то беда с логическим мышлением. Мне с вами не интересно общаться.

Я вам по секрету скажу — эти типы можно передавать библиотеке как статические параметры, а не хардкодить внутри.

То есть получается, что в библиотеке будет объявлен тип Shape, а потом пользователь сделает реализации, потом он каким-то образом передаст библиотеке набор реализаций и библиотека включит их в список типов, реализующих Shape и сможет вызывать методы Shape на этих новых для неё типах? Не совсем понятно, как это реализовать с помощью подхода, который вы продемонстрировали кодом на D. Например, если в библиотеке есть метод, который принимает коллекцию реализаций Shape, то как сказать, что там ещё есть наши реализации? Ведь библиотеке надо будет декларировать тип, который принимает коллекция. Можно как-то расширить описание этого типа?


Не знать как устроен объект и работать с разными объектами — ортогональные вещи. У вас какая-то беда с логическим мышлением.

Вы сначала сказали, что важно, чтобы в объекте были методы и поля и это самое важное. А потом сказали, что важно, чтобы клиентский код не знал, как устроен объект. Что важнее?

Тогда можно пример создания массива полиморфных объектов, которые наследуются от абстрактного класса Shape
Если нужно именно это, а не массив из вообще любых типов, то это типовая задача в плюсах.

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

A variant, который выше предлагалось использовать, предназначен для других случаев — когда нужно хранить несовместимые между собой типы. Например, shape, string и double.

Мы обсуждали вопрос хранения объектов единым массивом, а не массива ссылок на объекты, находящиеся где попало.

Кстати, вопрос, один элемент массива будет требовать памяти столько, сколько занимает наибольший тип? Или они каким-то чудом ещё и пакуются?

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

В примерах с variant выше само собой будет занимать как наибольший тип, плюс оверхед на идентификатор типа.

Ну вы же развели дискуссию о том, что можно сделать на С++ массив полиморфных объектов, хотя код можете показать только на D )))


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

Справедливости ради, сделать свою коллекцию с «магией» в С++/D не сложно, но придется распрощаться с произвольным доступом к элементам. Ну или городить к ней вспомогательную структуру, которая будет мапить индексы в смещения элементов, но тогда полученное будет уже не совсем массивом.
Мы обсуждали вопрос хранения объектов единым массивом, а не массива ссылок на объекты, находящиеся где попало.
Никакого уточнения про хранение по ссылкам или значению я не обнаружил, обсуждался именно сабтайп-полиморфизм для массивов. Который во всех приличных ООП языках делается через ссылки, и я не вижу никакого практического смысла реализовывать его иначе.

Но если прям очень-очень захочется, то ничего не мешает запилить собственный класс массива, который будет складывать в себя эти же объекты по значению, но итерировать их через указатель.
А как массивы ссылок противоречат проблеме AoS vs SoA? Она ведь подразумевает не только выбор между буквальными расшифровками аббревиатур, она куда шире.

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

Что важнее, под AoS очень редко рассматривается массив variant, потому что это максимально неэффективный способ хранить объекты разного размера в массиве. Если уж отказываться от ссылочности, то в сторону множества однотипных массивов, как в ECS.

По второй ссылке нет ничего про «in-place». Зачем вы меня отправили по ссылкам в другом сообщении я вообще не понял, я и так по ним ходил.
Через Variant или как он там в C++ называется.

Через variant не получится, это ж не тот полиморфизм (и вообще не полиморфизм, а ADT на костылях).

Один и тот же код работает работает с разными типами. Это полиморфизм по определению.

Во-первых, не один и тот же. Чаще всего при обработке variant'а у вас по отдельной ветке на каждый, собственно, вариант (а если и нет, то это параметрический полиморфизм).


Во-вторых, если у меня есть код на хаскеле типа


data Shape = Circle | Rectangle | Triangle

describe :: Shape -> String
describe Circle = "it's so round!"
describe Rectangle = "yay rectangle"
describe Triangle = "yay triangle"

то это что, полиморфизм, по-вашему?

Чаще всего при обработке variant'а у вас по отдельной ветке на каждый

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


У вас больше одного, у меня другого — бывает. Но даже если полиморфного кода у вас мало, он от этого не перестаёт быть полиморфным.


это параметрический полиморфизм

А параметрический полиморфизм — это внезапно уже не полиморфизм? Или как это работает?


у меня есть код на хаскеле

А я не понимаю хаскель.

А параметрический полиморфизм — это внезапно уже не полиморфизм? Или как это работает?

Контекст дискуссии намекал (по крайней мере, мне), что речь о сабтайпинг-полиморфизме.

Речь шла о создании массива, который может содержать разные типы данных.

Во-вторых, если у меня есть код на хаскеле типа

В хаскеле же объединения размеченные, по-этому варианты типами не являются. Вы же не можете создать значение типа Circle или ф-ю, которая принимает только Circle.

Это вопрос писанины на клавиатуре:


data Shape = Circle CircleData | Rectangle RectData | Triangle TriData

для соответствующих типов CircleData, RectData, TriData. Хранит ли массив [Shape] разные типы данных?

Это вопрос писанины на клавиатуре:

Нет, это вопрос семантики. В хаскеле у вас нет соответствующих типов. И, более того, вы их никак не сможете даже объявить или эмулировать. Размеченные объединения можно выразить через обычные объединения и пары — но не наоборот.


Хранит ли массив [Shape] разные типы данных?

Конечно же, нет. Он хранил бы разные типы данных, если бы в массиве были значения типов CircleData/ReactData/TriData. Но там вместо них значения типа Shape.
Если вы напишите ф-ю, которая возвращает некоторый CircleData, то вы результат этой ф-и в массив ваш положить не сможете. Вам надо будет этот CircleData дата скормить конструктору, который вернет Shape.

Но на том же С++ std::variant точно так же является типом-суммой, а не типом-объединением.

Но на том же С++ std::variant точно так же является типом-суммой, а не типом-объединением.

Для суммы int + int != int, разве это выполняется для variant?

Конечно же выполняется, тип std::variant<int,int> отличается от просто int.


Более того, его можно создать как std::variant<int,int>( std::in_place_index<0>, 10), а можно как std::variant<int,int>( std::in_place_index<1>, 10) — и эти два значения будут различимыми.

Конечно же выполняется, тип std::variant<int,int> отличается от просто int.

А, ну ок. Вообще я перепутал и думал, что мы про union говорим.

А вас не смущает, что та же статья в Википедии приводит примеры из Си?

О той, которая на том самом сайте, которая идёт после какой-то.


Собственно мы о разных проблемах говорили.

Но вот для складывания матриц в GPU ФП скорее всего не подходит.

Кстати, внезапно подходит, см. фреймворк accelerate.

Вообще-то Дейкстра изначально такого про исключения не говорил. И все-таки, они не эквивалентны goto, что бы там ни говорили.
Да я и не настаивал, что они хороши по всем пунктам. Но у них есть своя, скажем так, ниша. И по сравнению с goto это все-таки шаг вперед. Был.
Либо это dispodsble, либо это RAII — все одно. Вот только ФП не создает иллюзии, что ты работаешь безопастно.

Почему абстракции, позволяющие безопасно работать с ресурсами, вы называете иллюзией?


Исключения — это костыль. В отличие от монады. И это математически доказал Дейкстра в своих основах структурного программирования

Можно ссылку на это математическое доказательство?


Модификация объекта — это головная боль с того момента, как программе появляется многопоточность.

Нет там никакой головной боли при использовании правильных абстракций.


Это называется декларативное программирование — вершина грамотной архитектуры и композиции программных систем.

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

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

Нет, не с чистыми в смысле отсутствия эффектов. Чистота функций — это про то, что эффекты функции объявлены в её сигнатуре. IO — ни в коей мере не костыли.


Если у вас есть массив из 100 пользователей, и вы должны одному обновить рейтинг, вы должны сделать полную копию массива. И так на каждое изменение. Это негативно влияет на производительность и потребление памяти.

Если вам не нужна старая копия массива, то GC её тут же подберёт, и массив даже из nursing area (и, как следствие, из кеша процессора) выбраться не успеет.


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


Там, где вы в PHP пишете $user->updateKarma(), в ФП вы должны писать user2 = updateKarma(user), и не забыть заменить старые копии user на user2 во всех местах кода, во всех коллекциях и списках. Удачи!

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


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

В ФП с do-нотацией вы тоже пишете функции подряд, и оставшиеся не будут выполняться. Пропуск выполняется за вас.


А ФП вроде Хаскелл позволяет создать 10 вариантов функции с одинаковым именем, но разным типом аргументов.

Правда? Можно пример?


Удачи вам найти при рефакторинге, какая из функций (раскиданных по разным файлам) вызывается в том или ином случае.

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


Но, конечно, в программах для вычисления чисел Фибоначчи ФП не знает себе равных.

Или в компиляторах. Или в статических анализаторах. Или в скрейперах. Или в веб-серверах.

Ну так IO-функция каждый раз возвращает один и тот же экшн и является детерминированной. В чём расхождение?

В том, что давая неверное определение, вы вводите людей в заблуждение.

Чем оно неверное-то? Чем функция, возвращающая описание действия, не чистая?

Два разных текста с двумя разными смыслами. Дальнейших разъяснений от меня можете не ждать.

Или в компиляторах. Или в статических анализаторах. Или в скрейперах. Или в веб-серверах.

BigData еще.

Вот это уже гораздо тоньше, чуть сам не кинулся объяснять, где вы неправы :)

Судя по карме комментатора это его искренние размышления, а не троллинг, как хотелось бы думать.

Толсто.

ФП работает с «чистыми» функциями. Но реальное приложение работает с файлами, БД, внешними сервисами, которые нарушают требования «чистоты», в итоге приходится придумывать костыли

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

Лишний повод почитать структуры данных. Immutable hash trie (через который в ФП реализуются immutable массивы) решает проблему.
в ФП нет исключений. В нормальном программировании вы просто пишете действия подряд, если произойдет ошибка, выбросится исключение и оставшиеся не будут выполняться. В ФП вам приходится лепить костыли, делая типы вроде Maybe и делая «пропуск» функции, если ей передано Maybe с ошибкой внутри.

Зато есть Either (это в scala), Option и до черта всякого остального. Пропуск функции делать не нужно, монада сделает это за вас.
в ФП нет ООП, которым удобно представлять объекты реального мира. Вместо этого там приходится делать разрозненные структуры и функции для работы с ними — то, для замены чего и придуман ООП. А ведь в ФП вы еще не можете модифицировать объект, с которым работаете.

ФП плох потому, что в нем нет ООП)
Вообще есть до черта всего. Алгебраические типы данных. Coproduct-ы. Функторы. Аппликативные Фнукторы. Монады, в конце-то концов. Полугруппы, моноиды, группы. Этим всем вполне можно представлять объекты реального мира.
в ООП функуция это просто последовательность шагов: 1) проверь, что такого емайла нет 2) добавь запись в БД 3) отправь письмо для подтверждения почты. В ФП же так не принято, а принято комбинировать функции в составные функции, из-за чего разбор кода превращается в кошмар (registrator = (formData) => combine(formValiadtor(rules), fieldExtractor('email'), emailNotInDbChecker(db), emailToDbAdder(db), emailSender(db, sendService)).

Это на уровне бреда. Вполне можно и последовательные вычисления организовывать, мешает-то кто?
Immutable hash trie (через который в ФП реализуются immutable массивы) решает проблему.

Ну вообще надо быть честным, hashtables иногда даёт такую производительность, которая на чистых (unordered-)containers недостижима.

а ввод-вывод оставить грязным функциям? IO-монада, там, вот это все.

Наличие стандартной затычки красноречиво свидетельствует о протечке абстракции.


Лишний повод почитать структуры данных. Immutable hash trie (через который в ФП реализуются immutable массивы) решает проблему.

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


Зато есть Either (это в scala), Option и до черта всякого остального. Пропуск функции делать не нужно, монада сделает это за вас.

А в языках с поддержкой исключений — компилятор делает эту монаду и Either за вас. Причём в нормальных реализациях ещё и cost-free, без 100500 условных переходов, которые так любит конвейер процессора.


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

Можно и из буханки хлеба сделать троллейбус. Только зачем?

А в языках с поддержкой исключений — компилятор делает эту монаду и Either за вас.

И за меня выражение вроде «попытайся выполнить вот этот список действий и верни первое успешное» нагенерит для каждого конкретного типа ошибки? Или надо будет самому обвязки писать?

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

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

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

И так почти по всем пунктам.

И что еще характерно, почти все пункты уже аргументированно разобрали, а комментарий все же имеет +24. Это я не к тому, что он плохой — по большей части тут рассмотрены вполне осмысленные вопросы, которые стоит себе задавать. Но он односторонний — а значит в конечном счете будет как-то способствовать распространению все тех же искаженных представлений об ФП.
Ну да, иммутабельность чего-то стоит, несомненно. Но в тоже время она почти даром дает нам например такие вещи, как возможность версионирования значения

Да вполне терпимо стоит. По асимптотике функциональные структуры данных такие же, как и мутабельные. Зато параллелизм из коробки.
Терпимо — это с точки зрения производительности наверное? Но вообще эти структуры — они несколько сложнее, откровенно говоря. Так что их разработка тоже чего-то да стоила.

Что-то это уже для меня слишком тонко. Где тут шутка, а где реальное мнение? :)

А кто просит писать на чисто функциональном языке? Я например пишу на swift, кое-где применяю паттерны из ФП (без фанатизма, где реально удобно), но это не отменяет ООП-шного стиля там, где он работает лучше. Мне кажется, дискуссия на тему «ФП полностью убьет ООП» является специальной олимпиадой, так же как «коробка передач убьет двигатели, они больше не нужны»
А ФП вроде Хаскелл позволяет создать 10 вариантов функции с одинаковым именем, но разным типом аргументов.

И при чём тут ФП языки?
перегрузка функций
c++: ravesli.com/urok-102-peregruzka-funktsij
перегрузка методов
java: javarush.ru/quests/lectures/questcore.level05.lecture03
c#: metanit.com/sharp/tutorial/3.5.php

Декларация. «И сказал Бог: да будет свет. И стал свет».

А один евангелист рекурсии дополняет: "В начале было Слово, И Слово было у Бога, И Слово было Бог"

Кстати говоря, если измерять «производительность труда» в LoC за единицу времени — то заголовок вполне себе верный.
В этой статье присутствует ложная дихотомия функционального программирования и ООП. Эти подходы совершенно не исключают друг-друга.

Совсем непонятно про наследование. В каком месте функциональное программирование отменяет концепцию наследования???

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

Если вы уберете наследование из ООП вы потеряете всё ООП.
Если вы уберете требования чистоты из ФП вы потеряете всё ФП.


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


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


В итоге, что остается от ООП? Какая концепция в нем есть, которая дает какие-то ощутимые преимущества?

Если вы уберете наследование из ООП вы потеряете всё ООП.

Нет, это не так, ООП потеряется только если убрать полиморфизм. Наследование там не главное.

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

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


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


В ФП центровым понятием является функция, функция должна быть чистой. Если функции не чистые, то уже не ФП, а элементы ФП


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

В ФП центровым понятием является функция, функция должна быть чистой. Если функции не чистые, то уже не ФП, а элементы ФП

Это ИМХО бессмысленная интерпретация, если относиться к ней формально. Даже в С все функции чистые, просто все функции живут в IO.

как это все чистые? Чистота определяется тем, что на одни и те же параметры функция выдаст один и тот же результат. Что такое параметры в С, думаю, не надо здесь объяснять. Так же, думаю не надо рассказывать здесь про существование статических переменных внутри функции, а, значит, что IO тут не совсем не существенно.

Давайте немножко издалека начнём.


Чистая ли функция putStrLn :: String -> IO (), просто выводящая строку на экран?
Чистая ли функция


greet :: IO ()
greet = do
  str <- getLine
  putStrLn $ "Hello, " <> str

?


Это ведь хаскель, а в хаскеле все функции чистые, не так ли?


Чистая ли функция


stateful :: MonadState MyState m => m Int
stateful = do
  myState <- get
  pure $ if something myState then 1 else 2

?

Смысл в том, что функция putStrLn не выводит на экран ничего, а создает соответствующий IO. А println именно что выводит, она не создает никакую структуру, которую можно потом проинтерпретировать.


Натянуть на глобус си и считать его чистым при большом желании можно, но зачем? Все понимают, о чем идет речь.

А println именно что выводит, она не создает никакую структуру, которую можно потом проинтерпретировать.

Это неважно, есть очевидный изоморфизм между живущими в IO программами на хаскеле и нечистыми программами на какой-нибудь джаве (может, даже на С есть, но мне лень анализировать низкоуровневые свойства С).


Натянуть на глобус си и считать его чистым при большом желании можно, но зачем?

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

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

Это неважно, есть очевидный изоморфизм между живущими в IO программами на хаскеле и нечистыми программами на какой-нибудь джаве (может, даже на С есть, но мне лень анализировать низкоуровневые свойства С).

Изоморфизм есть только в слчае лени, а её нет.


Если вы сделаете let _ = putStrLn то ничего не произойдет. Если вы сделаете () _ = println(...) то эффект будет.


Точно так же вы можете дропнуть весь IO при желании, и его эффекты не сработают.


Отправиться в прошлое в си и отменить печать в консоль вы не сможете.

Если вы сделаете let _ = putStrLn то ничего не произойдет. Если вы сделаете () _ = println(...) то эффект будет.

Потому что это разные вещи: оттого, что я в С объявлю функцию void _() { printf(...); }, тоже ничего не произойдёт. И лень тут ни при чём, в очень «энергичном» идрисе первая строка тоже ничего не выведет.


Точно так же вы можете дропнуть весь IO при желании, и его эффекты не сработают.

Что эквивалентно невызову функции.


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


stupid = do
  str <- getLine
  print "yay"

Но я в хаскеле не объявил функцию, а вызвал её.


Что эквивалентно невызову функции.

Вызов функции в хаскеле эквивалентен невызову в си? Окей, но мне сложно назвать это "похожими вещами", не говоря про "Одно и то же".

Но я в хаскеле не объявил функцию, а вызвал её.

Нет, не вызвали. Вы объявили байндинг без имени (потому что _), который ссылается на функцию putStrLn. Чтобы вызвать, надо вот эту вот стрелочку налево писать, которая <-. Ну или что-то аналогичное, если без сахара do-нотации (но формализация этого в рамках данной дискуссии несущественна).


Вызов функции в хаскеле эквивалентен невызову в си?

Создание байндинга, который в итоге нигде не участвует в цепочке IO.

Чтобы вызвать, надо вот эту вот стрелочку налево писать, которая <-

Уточнение: на самом деле, чтобы вызвать, надо вернуть результат из main. Ну или передать в unsafePerformIO или что-то подобное.


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

Только ИО синхронно выполняется. ИО в грязном языке — это просто лямбда. А unsafePerformIO — простой функциональный вызов.

Как провести изоморфизм, если хаскель разделяет a и IO a, а си – нет?

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


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

Если строго говорить, то чистая. И возвращает она последовательность действий «1) прочитать строку; 2) вывести строку, используя результат первого действия»

Собственно main :: IO () в haskell — это просто выражение, которое описывает определенные действия из N возможных. Описание это формируется при помощи композиции (монадической композиции, скажем так), ряда функций вида a -> IO b.

Вот и все. Далее, на основании этого описания компилятор, собственно, генерирует машинный код (или код ассемблера, или код на промежуточном языке, таким, как C).

Ну так и код на С просто описывает определённые действия из N возможных. И при одном и том же исходном состоянии машины он тоже совершит одни и те же действия.


Любую функцию на С вида T foo(Arg1, ..., ArgN) вы можете представить себе как foo :: Arg1 -> ... -> ArgN -> IO T и не потерять вообще ничего.

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

В C можно написать функцию
T foo (Arg1, ..., Arg N),
которую, если вызывать с одними и теми же значениями Arg1 и ArgN, то она будет всегда возвращать разные значения. Это и есть нарушение чистоты функции. Про «состояние машины» речь не идет совсем.

На хаскеле тоже можно написать функцию foo :: Arg1 -> ... -> ArgN -> IO T, которая при вызове с одними и теми же параметрами будет возвращать разный результат.


Тут, конечно, дъявол кроется в деталях и, как обычно, в определениях: что именно значит «возвращать результат»?


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

Дело даже не в IO, а в том, что в Си можно вызвать функцию, передав туда указатель на структуру, потом изменить поле структуры, переданной ранее в функцию, потом вызвать ту же функцию, передав туда тот же указатель и получить другой результат.

Ну, собственно, аналогичный вопрос. Как насчёт хаскель-функции, живущей в MonadState, чистая ли она?

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

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


В ФП центровым понятием является функция, функция должна быть чистой. Если функции не чистые, то уже не ФП, а элементы ФП

Объекты это частные случаи функций, и наоборот. Объект А всегда можно представить как функцию () -> A.


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

foo<T> — это полиморфизм без использования парадигмы ООП.

Объект А всегда можно представить как функцию () -> A.

Надо просто почитать Пирса, где он на довольно простом срезе лямбда-куба делает полноценную объектную систему.

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

Широко известный в узких кругах Types and Programming Languages. Вроде даже pdf'ка свободно циркулирующая была (но если не найдёте — стукнитесь в личку).


Особо много матана там нет, самый хардкор там, ИМХО, в главе про метатеорию рекурсивных типов, но она для понимания ООП не обязательна.

foo — это полиморфизм без использования парадигмы ООП.

Если вы имеете в виду дженерики или шаблоны, то это не полиморфизм, потому что там нет вызовов полиморфных методов.

Я думал, что мы говорим про тот полиморфизм, который используется в ООП. То есть получается про подтиповой. Когда начинаете им пользвоаться — начинаете использовать ООП. Параметрический полиморфизм такой неотъемлемой частью ООП не является.

Полиморфизм в ООП подтиповой, можно реализовать через параметрический, в той же статье это написано.


То есть это просто более богатый языковой концепт, как паттерн матчинг против if. В итоге мы можем выразить всё то же самое используя его.

Полиморфизм в ООП подтиповой, можно реализовать через параметрический, в той же статье это написано.

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

Инкапсуляция (не в смысле сокрытия) данных и алгоритмов их обрабатывающих с привязкой их друг к другу by design. Как по мне, то именно это стало основным шагом от структур типа сишных с указателями/ссылками на функции к ООП. Не token = auth.login(auth, username, password), а token = auth.login( username, password).

Ну возьмите Раст, ООП там нет, но там тоже будет auth.login(...), ФП никак этому не противоречит.

А в чем еще ООП состоит? Я ниже спросил, никто так и не ответил.


В моем понимании единственным значимым отличие именно наследование и есть.


А в расте ни привычного, ни непривычного наследования структур нет.

Ответ вам дали в этой ветке 4 комментариями выше. Вы ходите кругами.


В моем понимании единственным значимым отличие именно наследование и есть.

А в моём — инкапсуляция и полиморфизм.


А в расте ни привычного, ни непривычного наследования структур нет.

Есть наследование интерфейсов-трейтов, и его более чем достаточно.

В Haskell есть инкапсуляция и полиморфизм. Выходит, Haskell — это тоже ООП?

В Haskell, насколько я знаю, инкапсуляция только уровня модулей. Напомню с чего началась ветка:


Инкапсуляция (не в смысле сокрытия) данных и алгоритмов их обрабатывающих с привязкой их друг к другу by design.

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

Я поддерживаю ваше начинание дать определения buzzword и разобраться по сути, но давайте сделаем тоже самое и с полиморфизмом. Вы писали:
ООП потеряется только если убрать полиморфизм. Наследование там не главное
Есть два основных вида полиморфизма: подтиповый и параметрический. Haskell и Rust на полную катушку используют параметрический полиморфизм, при этом по крайней мере Haskell точно не является ООП-языком. Подтиповый полиморфизм невозможен без поддержки наследования на уровне языка. В Delphi/Object Pascal долгое время был только подтиповый полиморфизм, что врочем не мешало этим языкам считаться «ООП».

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

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


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

Но даже в этом случае для ООП более чем достаточно тех возможностей, которые дают dyn Trait в Rust или экзистенциальные типы в Haskell
Что приводит нас к логическому умозаключение что видимо наследование в принципе не так уж необходимо, раз без него можно реализовать ключевые паттерны проектирования.

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

Ну да, так и есть. Это удобный механизм, а не необходимая часть ООП.


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

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


Не вижу причин не называть их паттернами ООП.

Получается, что с точки зрения полиморфизма ООП от не-ООП отличает как раз присутствием подтипового полиморфизма

Получается да


который в свою очередь невозможен без наследования.

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

Ребята, вы забываете, что ООП — это Объектно Ориентированное Программирование. Ничто не мешает реализовать объекты в ФП. Так же как и чистые функции в ИП. Разница лишь в том, на какой тип использования язык ориентирован.

А на счет инкапсуляции как «использования значения как пространства имен» – это ведь не более чем синтаксический сахар над Foo::method(foo). То есть явно не дотягивает до ключевой особенности, определяющей парадигму.

Сахар сахаром, но это именно что ключевая особенность!


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


Если же синтаксический сахар не учитывать, то надо записывать Си без плюсов как поддерживающий ООП язык, ведь ядро Linux и оконная подсистема WINAPI написаны именно в парадигме ООП!

Ну да, по вашему определению получается Си это ООП язык, и вы правы, это не так, но не потому, что там нет сахара вызова методов через точку.

Ну это скорее ваше собственное определение, чем общепринятое. Я его понимаю и принимаю. В таком смысле действительно Rust и Go – вполне себе ООП языки.

Тем не менее, такой подход вряд ли удовлетворит обывателей, которые жалуются что даже «ООП не смогли нормально завезти»: в первом нет наследования, а во втором – параметрического полиморфизма.

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

Ну, я тоже считаю, что в Rust «ООП не смогли нормально завезти»: вот как наконец-то появится делегирование реализации, так это мнение и переменится.


Тем не менее, "ненормальное" ООП — это вовсе не отсутствие ООП.

Можно пример, чем использование отдельных типажей или Deref/AsRef не угодили?
Я, если честно, и не хочу эмулировать ООП в Rust. Я хочу посмотреть на примеры ситуаций, где из-за отсутствия ООП (чтобы это не значило) на Rust код получается хуже.

Ну вот есть типаж о 10 методах (конкретный кейс придумывать лень, но не вижу причин ему не существовать). Нужно сделать реализацию B, которая делает всё как реализация A — но 1 метод из 10 отличается.


На том же C# я бы использовал либо наследование, либо композицию с наследованием — а в Rust я вынужден писать тривиальную реализацию для каждого из 9 методов.

Обычно это заменяется выносом 9 общих методов в один тип, и потом использование его из А и В.

Обычно это заменяется выносом 9 общих методов в один тип, и потом использование его из А и В.

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


Иначе получается, что у нас есть тип с 9 "общими" методами — и еще 18 написанных вручную методов-делегатов.

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

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


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

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

Во-первых, вызывает вопросы существование типажа с десятью не default методами. Я понимаю что это утрированный пример, но это плохой пример. Типаж – это неделимая единица абстракции, из которой нельзя выкинуть ни один метод, не нарушив контракт. Я сомневаюсь что у вас есть настолько сложная абстракция. Как правило типажи ограничиваются 1-4 методами. Так что задумайтесь как поделить типаж на 2-3 отдельных.

Теперь по поводу вашего подхода. На каком основании вы приняли решение что B наследуется от A? Потому что A существовал в вашей кодой базе раньше чем B? Почему не наоборот? Если я преобразую экземпляр B в тип его предка A, то как должен выполняться тот самый десятый метод? Что если реализация десятого метода для A находится в другой библиотеке и он не объявлен виртуальным? Если мне понадобится еще один тип C, который переопределяет третий метод, а десятый реализует как B, я должен наследовать его от B? А если потом окажется что нужен еще и D, который реализует третий метод как С, но десятый как A, от кого будем его наследовать?

Своими вопросами я хочу показать, что пока вы рассматриваете инструменты абстракции как споcоб писать поменьше кода, а не как способ реализовывать корректные контракты, вы будете получать дырявые абстракции. Они будут работать только на тех примерах, которые был у вас в голове когда вы их реализовывали.
Теперь по поводу вашего подхода. На каком основании вы приняли решение что B наследуется от A?

На том основании, что B сложнее A и расширяет его функциональность.


Если я преобразую экземпляр B в тип его предка A, то как должен выполняться тот самый десятый метод?

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


Что если реализация десятого метода для A находится в другой библиотеке и он не объявлен виртуальным?

А как давно на Rust появились виртуальные методы?


Если мне понадобится еще один тип C, который переопределяет третий метод, а десятый реализует как B, я должен наследовать его от B?

Такой вариант возможен.


А если потом окажется что нужен еще и D, который реализует третий метод как С, но десятый как A, от кого будем его наследовать?

Ну, это будет означать что пора программу рефакторить.


пока вы рассматриваете инструменты абстракции как споcоб писать поменьше кода, а не как способ реализовывать корректные контракты, вы будете получать дырявые абстракции

А вы рассматриваете контракты как что-то, зависящее от реализации, хотя должно быть наоборот.


Если так получилось, что в типаже 10 методов — в языке должен быть способ нормально с таким типажом работать. А не разбивать его на 10 типажей по 1 методу просто из-за деталей реализации.

А как давно на Rust появились виртуальные методы?

Всегда были


А вы рассматриваете контракты как что-то, зависящее от реализации, хотя должно быть наоборот.

Да нет, это ваш случай. Вы смотрите на содержимое методов, и такой "ба, да тут можно отнаследовать всё это. Пофиг, что наследуем треугольник от линии, у них обоих есть метод Draw!".

Всегда были

А мне почему-то казалось, что в Rust нет наследования, как следствие полиморфизма подтипов — а значит, и виртуальных методов.


Если же вы намекаете на dyn Trait — то покажите мне в таком случае невиртуальный метод типажа, который ну никак нельзя переопределить. Мне кажется, это оксюморон.

Так же, как и в ООП, делаете структуру Derived, определяете для неё трейт Foo, и засовываете в Vec<Box<dyn Foo>>

Это вы сейчас на какой вообще вопрос ответили?


На всякий случай процитирую ветку целиком:


Нужно сделать реализацию B, которая делает всё как реализация A — но 1 метод из 10 отличается.

Что если реализация десятого метода для A находится в другой библиотеке и он не объявлен виртуальным?

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

Так же, как и в ООП, делаете структуру Derived, определяете для неё трейт Foo, и засовываете в Vec<Box<dyn Foo>>

Извините, но я не вижу в этом диалоге вообще никакого смысла.

Я Ответил про то как работает вызов виртуальных методов в расте.


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

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

На том основании, что B сложнее A и расширяет его функциональность.
Не расширяют. Они оба реализуют один и тот же типаж/интерфейс, и все. Их функциональность идентична. Как вы решили что именно B расширяет А, а не наоборот? Потому что в B больше строчек кода? Расширение функционала – это добавление нового метода или реализация нового интерфейса.
Никак, потому что вы не сможете преобразовать экземпляр B в тип его предка A.
Разве? dotnetfiddle.net/m9gFeB
А как давно на Rust появились виртуальные методы?
Не появились, чтобы таких проблем не возникало. Появилась специализация, но это про другое.
Ну, это будет означать что пора программу рефакторить.
Потому что она была построена на изначально неверных предпосылках. Если бы ваш трейт/интерфейс был разбит на отдельные независимые интерфейсы и вы использовали композицию, этой проблемы можно было бы избежать.

А вы рассматриваете контракты как что-то, зависящее от реализации, хотя должно быть наоборот.
Как раз наоборот. Я утверждаю что A и B – сущности одного порядка, и между ними не может быть отношения наследования. Это выплывает из контрактов, которые они выполняют, а не из реализации. Если реализация скрыта от пользователя она может быть любой – A является оберткой над B или B является оберткой над А, или они оба композируют себя третью базовую сущность. Интефейсы вашей программы от этого не изменятся. В Вашем случае в интерфейсы вашего кода протекает ненужная абстракция того что B наследуется от A. Когда вы реализуете два класа, и один из них является наследником другого, вы тем самым утверждаете, что у них общий тип – тип предка. И приведение экземпляров наследника к типу предка – это осмысленная операция. Что она будет означать в вашем случае? Вы не отвечаете на этот вопрос, потому что решение наследовать A от B вы приняли мотивируя это схожестью реализации. Расширение функционала – это не «добавить еще строчку логирования» в метод.

Ну вот возьмём пример, который за ночь придумался-вспомнился. Абстрактное хранилище некоторых конкретных структур. Это типаж, у которого есть методы load, save, add, delete, и ещё куча методов-запросов, которые не сводятся друг к другу (точнее, свести-то их можно, но только с неизбежной просадкой производительности — из-за чего лучше разрешить хранилищу их все переопределять). Таких методов в сложных случаях может хоть два десятка набраться.


А дальше нужно сделать хранилище с локальным кешем, чтобы последовательные вызовы load с одним и тем же ключом не тратили лишнее время. Оно должно наследовать (или делегировать — это вообще не важно) поведение базовой реализации, но с дополнительной логикой в этих самых load, save, add, delete. В запросах никакая новая логика не нужна.


Если разделять каждый такой типаж на два — это удвоит число зависимостей в вышележащем слое. А их и так много.


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


Разве? dotnetfiddle.net/m9gFeB

Я вообще-то про Rust писал, а не про C#.


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

Ну так если его нет, то откуда вообще ваш исходный вопрос про невиртуальный метод?


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

Но если предположить, что она построена на изначально верных предпосылках — то и рефакторинг тоже не понадобится.


Если реализация скрыта от пользователя она может быть любой – A является оберткой над B или B является оберткой над А, или они оба композируют себя третью базовую сущность.

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


Вы не отвечаете на этот вопрос, потому что решение наследовать A от B вы приняли мотивируя это схожестью реализации.

Нет, это вы приписали мне это утверждение. Изначально я утверждал, что одному классу нужна часть поведения другого. Будет ли для этого использоваться наследование, делегирование или вообще копи-паст — с точки зрения постановки задачи не важно, это делать реализации.

ещё куча методов-запросов, которые не сводятся друг к другу (точнее, свести-то их можно, но только с неизбежной просадкой производительности)
Можно подробнее про просадку производительности, я не очень понимаю откуда ей взятся. Например в Iterator несколько десятков предопределенных методов, и все они сводятся к next() и size_hint(), которые как раз и должен определить реализатор итератора, и никаких просадок производительности нет.

Ситуации, в которой в очередной реализации мне придется сначала изменить один из запросов, но с обязательным кешированием — а потом изменить другой запрос, сохранив изменения в первом, но убрав кеширование, я придумать не могу.
Зато я могу придумать вам другой пример: вы решили добавить еще один слой кеширования (Redis). Или решили добавить профилирование выполнения запросов. А потом решили выполнить профилирование без кеширования. Или реализовать еще одно хранилище, сохранив логику кеширования и профилирования. Вот тут вы отгребете за наследование и пойдете рефакторить. Пример который вы описали – это прямо типичный случай, когда нужно использовать композицию. Ваша кеширующия/профилирующия логика – это обертка над базовой реализацией (которая должна быть абстрактна в том смысле, что мы знаем лишь ее интерфейс). Наследование не позволяет вам это сделать.

Более того, меня устроит любой из этих трёх вариантов! Но уж извините, выбирать между ними, я будут именно что с позиций «где меньше кода писать» — именно потому что абстракциям пофиг на эти детали реализации, пока абстракция не порушена — писать я могу как угодно.
Так выбирайте, все 3 подхода доступны вам в Rust. Обертка != наследование.

Будет ли для этого использоваться наследование, делегирование или вообще копи-паст — с точки зрения постановки задачи не важно, это делать реализации.
Важно, до тех пор пока предок A у вас торчит наружу, а вашей задачу нужно оставить и A и B. В отличии от делегирования и копипаста, наследование добавляет новый функционал – приведенеие B к A, который не несет смысла.
Можно подробнее про просадку производительности, я не очень понимаю откуда ей взятся. Например в Iterator несколько десятков предопределенных методов, и все они сводятся к next() и size_hint(), которые как раз и должен определить реализатор итератора, и никаких просадок производительности нет.

Что быстрее: SELECT с условием WHERE на стороне СУБД — или SELECT без такого условия, с последующей фильтрацией, сводящейся к вызовам next?

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

Увы, у него свои недостатки: формирование запросов к БД размазывается по всему коду. Это потом аукается на этапе оптимизации БД или, не дай бог, замены хранилища.

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


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


А вот как раз-таки при использовании вендор-специфичного чего-нибудь вместо стандартизованного интерфейса можно словить сюрпризов.

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

И всё это прекрасно живёт за фасадом репозитория.


А вот как раз-таки при использовании вендор-специфичного чего-нибудь вместо стандартизованного интерфейса можно словить сюрпризов.

Вот потому это самое вендор-специфичное и нужно собирать в одном месте, чтобы было на виду.

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

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

Если честно, не понимаю к чему это. У вас есть наиболее общий метод select с WHERE, который нужно реализовать. И есть частные методы: без условия, с выбором одного элемента по primary key и так далее. Все они вызывают базовый метод.

Это помешает заниматься оптимизацией отдельных запросов.

Он про то, что если у вас есть репозиторий FilterByDate() и FilterByName() вы не можете при реализации FilterByDateAndName() их использовать, придется писать отдельный метод.


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

Если бы там только FilterByDate и FilterByName — я бы согласился.


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


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


Вот были бы они все в одном месте — ошибка была бы заметна сразу же, а не на проде...

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

Вы принципиально не читаете того, что я вам пишу?


Так выбирайте, все 3 подхода доступны вам в Rust. Обертка != наследование.

Но все три требуют писать вручную 9 делегирующих методов.


Важно, до тех пор пока предок A у вас торчит наружу

А где я писал, что он торчит наружу?

Если A вообще нигде планируется использовать, то задача не имеет смысла – просто измените поведение A и не вводите новых сущностей.
Но все три требуют писать вручную 9 делегирующих методов.
Именно. И я вам пытаюсь объяснить почему это скорее всего правильно. Лучше архитектура не там где меньше кода.
Если A вообще нигде планируется использовать, то задача не имеет смысла – просто измените поведение A и не вводите новых сущностей.

Во-первых, а если все-таки планируется?


Во-вторых, а как же SRP?


Лучше архитектура не там где меньше кода.

А причём тут вообще архитектура, если я сравниваю два разных языка (Rust и гипотетических Rust с делегированием) при неизменной архитектуре?

Но все три требуют писать вручную 9 делегирующих методов.

Если вам лень писать 9 методов и поэтому вы ломаете архитектуру лучше повесить атрибут [DelegateCallsTo] чтобы он нагенерировал вам эти прокси.


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


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

вы ломаете архитектуру

В какой момент?


лучше повесить атрибут [DelegateCallsTo]

Не нашел такого аттрибута, ни в Rust, ни в C#

В какой момент?

В момент когда вы наследуете классы на основании того, что код в теле методов похожий, а не потому, что они по логике отвечают отношению "является".


Не нашел такого аттрибута, ни в Rust, ни в C#

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

Ну реалистичный пример — это GUI. Классический гуй где есть абстрактный класс контрола и вот это все отлично работает, а в расте как сделать нормально до сих пор не придумали.


Других примеров, правда, придумать не получается.

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

Вообще GUI это монументальная задача с точки зрения архитектуры. Я реализовывал TUI фреймворк на Rust. В принципе тоже самое что GUI но попроще. Пока реализовывал выучил Rust. Паттерн Visitor который распространяет события и выполняет прорисовку спускаясь по иерархии виджетов вполне справляется.
можно и без визитора, с присвоением функции прорисовки. Почитай тут посты людей которые ругают визитор. Собственно эта базовая фича ФП позволяет избежать флудокода и изворотов.
Я это и имел ввиду: рекурсивный вызов функции draw от предка до потомка по иерархии вложенности, если я правильно понял. Тоже самое и с распространением событий. Не тот визитор, который класс с кучей методов типа visit_foo.

Ну расскажите о своем опыте, потому что то что я видел (gtk-rs/azul/yew...) пока только экспериментирую, и никаких устоявшихся паттерном пока не изобрели.


С другой стороны иерархичный GUI всегда хорошо работал, возьмите хоть WPF, хоть Qt.


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


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

Я не эксперт по GUI чтобы рассказывать как правильно – наверняка найдутся ситуации которые я не учел и которые вызовут проблемы.

Подозреваю что проблемы с проектами типа gtk-rs/Qt связаны в первую очередь с тем что они делались под С++, и в итоге при их использовании приходится писать С++ на Rust, что мало кому понравится.

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


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

Это тот классический GUI в котором у каждого контрола сотня полей, две трети из которых не имеют смысла для каждого отдельно взятого типа контрола?

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

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

В Rust есть полиморфизм подтипов и реализуется он через типажи и типажи-объекты.

а кстати это серъёзный минус Раста. Что он не делает как все а как то через задницу
Если вы уберете наследование из ООП вы потеряете всё ООП.
[...]
Инкапсуляция
[...]
Полиморфизм


“I invented the term object oriented, and I can tell you that C++ wasn't what I had in mind.” —Alan Kay

Все эти 3 распиаренных слова не являются ключевыми для ООП.
Все эти 3 распиаренных слова не являются ключевыми для ООП.

А что для ООП ключевое? Не того, которое Алан Кей придумал, а для того, которое в Go, Java, C++, Javascript, С# и так далее.

Полиморфизм. Не считая его, все ООП-принципы можно использовать в чистом C с минимальными усилиями. Об этом можно почитать у автора принципов SOLID, Роберта Мартина, в его книге Clean Architecture. Именно на полиморфизме строится профит от подавляющей части паттернов и техник ООП.

Присоединяюсь к вопросу выше. Во-первых ООП как его задумал Кей это совсем не то, что имеется ввиду при оформлении вакансии на современные языки.
Во-вторых, если не это является основным для ООП, то что? "Данные вместе с функциями" в ФП точно так же живут в виде тайпклассов. Тогда что же отличает ООП от всего остального? С моей гипотеза, что это наследование, вы не согласны. Интересует, что же тогда?

Не согласен не nuclight, а автор ООП которого nuclight процитировал.

Гипотезы здесь не уместны, поскольку есть первоисточники.

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

Раньше был в этом вопросе на стороне Кея, но после ваших доводов задумался.
Может быть, чтобы не было путаницы и холиворов, стоит различить «объектное» (в стиле Кея) и «объектно-ориентированное» (в мейнстримном понимании) программирование? В конце концов, мы же не говорим «функционально-ориентированное» или «процедурно-ориентированное».

Программирование в стиле Алана Кея сейчас называется моделью акторов.

Единственное, что отличает ООП от ФП, — это угол зрения:



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

Вы не можете применять ФП в языках, где функции не чистые. Сам рантайм ФП языков предполагает, что функции чистые, и когда у вас компилятор заинлайнит функцию log(x); return x, и в эластик перестанут сыпаться логи, вы не обрадуетесь.

В том же Хаскеле есть seq для тех случаев, когда монад почему-то не хватает.

Какой тип у log? Для всех разумных типов ничего никто никуда не заинлайнит, на каждое вычисление по-прежнему будет по вызову. Даже если у вас будет код типа


getVal :: IO Ty
getVal = do
  log "foo"
  pure val

useVal :: IO ()
useVal = do
  v1 <- getVal
  v2 <- getVal
  ...

Семантику языка всё-таки не изверги придумывали.

Не надо, но бывает. Для примера сойдет.


Предлагаю закончить на этом.

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

Я не силен во всех этих теориях, по этому также можно сказать задам вопрос, если что меня поправьте…

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

Я не сильно силен в ФП, от сюда и вопрос, а разве нет подобия наследования в ФП, только оно вполне возможно выглядит иначе?

Наследования (реализации) как такового в ФП нет, но те задачи, которые решаются
наследованием можно решить другими путями.


важна инкапсуляция, тоесть мы забываем про иммутабельность

Какая связь то? Посмотрите модель акторов (на примере Erlang/Elixir)… Инкапсуляция такая, что мейнстрим языкам даже в самых влажных мечтах не снилось, но при этом абсолютно все данные иммутабельны.

Инкапсуляция к мутабельности отношения не имеет вроде как. У нас может быть иммутабельный объект с сильной инкапсуляцией и защитой.

Не будем убирать ничего. Наследование никак не нарушает чистоты функций и наоборот.

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

Чудесный вброс. Я почти поверил, пока не прочитал внимательнее :)
Интересно, а может кто нибудь сделать пример расчета больничного листа со всеми нюансами, если так удобно на ФП писать бизнес логику.

Если вы опишете алгоритм расчёта со всеми нюансами, то почему бы и нет.

Алгоритм расчета со всеми, ну или почти со всеми нюансами есть в 1С ЗУП.
Какой-то не очень удачный пример:
const getFruitPrice = type => fruits =>
  fruits
  |> filterByType(type)
  |> first
  |> get('price');


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

Фильтрация ленивая ведь. Мы остановимся как только встретим первый подходящий фрукт


https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8ac1a1c6de3f950f408b323492b5d063


Ну или если лайфтаймы и линейные типы напрягают, на скале: https://scastie.scala-lang.org/w6JXitIoRimV6zyfTGRzzg


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

Мы точно про JS говорим? Там никакой магии нет, как я понимаю. Пока filter() не пройдётся по всему массиву, следующие функции не начнут работать.

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


В гугле первой ссылкой находится вот такая библиотека: https://fitzgen.github.io/wu.js/


На первый взгляд выглядит достойно.

«Если ты такой умный, то почему такой бедный?»
Если функциональное программирование гораздо лучше чем ООП, то почему он не используется более широко чем ООП?
  1. Потому что в универах обычно его не преподают (по инерции, тянущейся еще с семидесятых, паскалей и си)
  2. Потому что когда человека отучили от математического определения функции и заменили императивной "процедурой", обратный переход неприятен
  3. Потому что до недавних пор компиляторы ФП не всегда хорошо оптимизировали код, а приложениям нужна была производительность
  4. Потому что ООП в 70х тоже было распространено меньше процедурного стиля, это не значит, что ООП хуже него
  5. ...
Смотря в каких универах. Во многих чтонибудь Lisp-образное — первый язык программирования. А ООП и тем более C уже потом. Потому что, как вы правильно заметили, императивная парадигма требует меньше усилий для освоения и освоить декларативную потом тяжело, гораздо тяжелее чем в обратной последовательности.

Нет, я этого не говорил. Наоборот, есть исследования западных универов, которые давали сначала ФП, после чего людям тяжело было понять императивное. Как я уже сказал, людям тяжело воспринимать ФП не потому, что оно сложнее понимается, а потому что у них первым обычно идет паскаль/си (особенно в снг). Если давать первым ФП, то людям будет выносить мозг запись i = i + 1. Я таких реально видел. Могли написать на комбинаторах полезную программку, а эту запись не понимали.

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

А как устроен компьютер? Ну, вычисляет чего-то. Вводит данные, выводит данные.
А если в итоге нужно объяснять, что такое память, байты, переменные, присваивание, то почему бы взамен не взять концепцию, где это можно не объяснять? ;)

Если давать первым ФП, то людям будет выносить мозг запись i = i + 1

Так это не от ФП зависит, а от математики. У нас в школе не было ФП, но эта запись для меня тоже поначалу выглядела непонятно. А все потому что математика не работает с состоянием, любое состояние раскладывается по оси времени на ряд независимых точек, существующих как бы одновременно. Самое близкое это границы суммы (∑), но обычно не уточняется как i переходит из одного значения в другое.

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

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

Это все ровно до тех пор, пока у нас простенькие алгоритмы вроде получили — преобразовали — вернули. А как только нужны сайдэффекты и прочее ИО — происходит беда, так как корректное "математичное" описание этих вещей — штука далеко не тривиальная, и в итоге императивная модель оказывается значительно проще.

А как только нужны DI и прочие солиды — происходит беда, т.к. простое "оопэшное" описание этих вещей — штука далеко нетривиальная.

Как его описать в терминах ООП так, чтобы компилятор гарантировал, что вы все ваши D дергаёте через I, а не напрямую?

Ну почему, каждая dependency выражается соответствующей монадой. А дальше включаем {-# LANGUAGE Safe #-} и копируем нашу дискуссию из соседней ветки.


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

Ну почему, каждая dependency выражается соответствующей монадой.

А как вы заставите выражать её соответствующей монадой, а не использовать напрямую? :-)

А как вы заставите выражать её соответствующей монадой, а не использовать напрямую? :-)

А как вы напрямую будете писать в лог, дёргать сервер или заниматься чем-то подобным?


Это ж эффекты всё.

Напишу весь код в монаде IO, и компилятор мне не помешает. Даже подскажет, где я ещё забыл IO добавить!


Но, вообще говоря, D в DI — это не обязательно эффекты. Вот совсем недавно я писал функцию преобразования типа данных DbRequestForRegistration -> RequestForRegistrationInfo, которая почти сводилась к копированию полей 1 к 1, за некоторыми исключениями. Эта функция, исходя из соображений архитектуры (к которой у меня есть претензии, но не я главный на проекте), внедряется в виде интерфейса (=тайпкласса) в конструктор.


И если бы я писал на Haskell — то у меня было бы написано как-то так:


data DbFoo = ...;
data FooInfo = ...;

class IFooMapper a where
    mapFoo :: a -> DbFoo -> FooInfo

data FooMapper = FooMapperInstance;

instance IFooMapper FooMapper where
    mapFoo FooMapperInstance entity = ...;

Так вот, в переводе на Haskell ваш вопрос будет звучать так:


как описать в терминах ФП, чтобы компилятор гарантировал, что ни одна функция bar за пределами Composition Root не использует FooMapperInstance напрямую, вместо этого имея сигнатуру вида IFooMapper m => m -> ...?

Да никак не описать! Просто от программиста нужна сила воли, чтобы либо следовать архитектуре, либо рефакторить её к чертям, но никак не обходить её.


PS на тот случай если вам кажется что код в примере слишком плохой для примера, можно было сделать лучше и вообще проблемы мы сами себе придумали:


Да, так и есть. Но это не отменяет того факта, что выделение и внедрение зависимостей довольно косвенно связано с контролем сайд-эффектов.

Я, наверное, не очень понял ваш пример, но кто мешает просто не экспортировать FooMapperInstance из вашего модуля?

Мешает тот факт, что вам его все равно нужно как-то прокинуть до Composition Root.

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

А кода выше вам не достаточно?


Ну вот ещё:


testData = ...

-- изображаем запрос к БД
good :: IFooMapper m => m -> Int -> IO FooInfo
good mapper id = return $ mapFoo mapper testData;

bad ::Int ->  IO FooInfo
bad id = return $ mapFoo FooMapperInstance testData

--  Composition Root
main = do 
    let query = good FooMapperInstance
    info <- query 12
    print info
Так вот, как бы вы доказывали доступными первокурснику методами, что структура векторного пространства над полем рациональных чисел определяется единственным образом?

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


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

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

Я, наверное, туплю, но как это поможет доказать, что если у вас есть две операции • и ◙ умножения вектора (из какого-нибудь V) на скаляр, и если скаляры лежат в ℚ, то ∀ α: ℚ, v: V. a • v = a ◙ v?

>Я, наверное, туплю, но как это поможет доказать, что если у вас есть две операции • и ◙ умножения вектора (из какого-нибудь V) на скаляр, и если скаляры лежат в ℚ, то ∀ α: ℚ, v: V. a • v = a ◙ v?

Не поможет, но чего там вообще доказывать? 1 @ v = 1 ^ v для любых умножений @/^, тогда (1+..+1)@v = (1+...+1)^v по дистрибутивности (т.о. для целых чисел все выполняется). Далее, если a@v = a^v => 1/a @ v = 1/a ^ v, т.к. (a * 1/a) @ v = (a * 1/a ) ^ v => a @ (1/a @ v) = a ^ (1/a ^ v) и для целых а мы уже доказали.

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

А мне через то, что ℤ начальный объект в Ring, морфизм ℤ → ℚ — эпиморфизм в Ring, а векторное пространство над ℚ — это просто морфизм в соответствующее кольцо эндоморфизмов, как-то очевиднее. То есть, если задешугарить все эти определения, то получится то же самое (особенно если посмотреть на доказательство того, что это эпиморфизм), но мы как-то сразу построили удачные абстракции, и потом надо их просто комбинировать.

А мне через то, что ℤ начальный объект в Ring

Ну вы ж спрашивали про "элементарными методами для первокурсника" — вот тут не сильно сложнее.
Здесь показательнее пример с теоремой про существование и единственность решения задачи Коши, которая доказывается дефолтно долго-долго, а через теорему о неподвижной точке — в три-четыре строки (+ 1 лекция чтобы доказать саму теорему о неподвижной точке...)


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


а векторное пространство над ℚ — это просто морфизм в соответствующее кольцо эндоморфизмов, как-то очевиднее

Дык, а почему морфизм-то этот — единственный? :)
Типа, на эпиморфизм сократили?

Дык, а почему морфизм-то этот — единственный? :)
Типа, на эпиморфизм сократили?

Да.

Передавать зависимости через конструктор, в чём проблема то?

А как только нужны DI и прочие солиды — происходит беда, т.к. простое "оопэшное" описание этих вещей — штука далеко нетривиальная.

Тривиальная, почему нет? Собственно, проще триггерорв/регистров и всего вот этого. И заведомо проще "математичного" ИО.

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

Я боюсь человеку не будет интуитивно понятно, что такое "фабрика блокнотов", откуда у блокнота метод "записать", и зачем тут какой-то "менеджер".

Есть мнение, что ФП без HKT невозможно. По крайней мере я не знаю, как писать в ФП стиле без монад.

в Ocaml HKT нет, но если кто скажет что это не фп язык, то даже не знаю какой он.

По той же причине, по которой PHP, Javascript и Go набрали популярность. Популярным становится не то что лучше, а то что проще объяснить.

В ООП, как и в реальном мире, все сущности – это объекты, у каждого объекта есть класс (кошечки и собачки). Классы могут наследоваться (и кошечки, и собачки – это животные). Можно инкапсулировать один объект в другой (как двигатель в машину). Можно часть объекта сделать приватной (как переписка в телеге с Машей), а часть – публичной (как профиль в инстаграмме). Еще можно делать фабрики объектов – это как настоящие фабрики, только любых объектов. Ну и так далее.

Поздравляю, если вы поняли то что я написал выше, значит вы усвоили принципы ООП. Теперь вы – энтерпрайз программист.
Вот допустим, надо вам написать что-то вроде алгоритма игры в шахматы. Т.е. на вход подается доска и нужно найти наилучший следующий ход, анализируя миллионы позиций неким изощренным эвристическим алгоритмом. Декомпозиция подобной задачи в функциональном стиле будет на порядки легче чем в объектном. Потому что в функциональном там и думать нечего, просто пишем функцию за функцией. В объектном там тоже думать не о чем, но уже потому, что от объектов с поведением там будет больше вреда чем пользы.
ИМХО, это и есть главное зачем нужно ФП. Конечно, еще не забываем многопоточность.
Вы сравниваете средний код ФП с плохим кодом ООП. Так будет выглядеть более-менее приличное решение с ООП. По вашему это плохо?
class FruitFinder {
  fruits = new Map([
    ['apple', { type: 'apple', price: 1.99 }],
    ['orange', { type: 'orange', price: 2.99 }],
    ['grape', { type: 'grape', price: 44.95 }], 
  ]);

  getFruit (name) {
    return this.fruits.get(name);
  }

  getPrice (name) {
    return this.getFruit(name).price;
  }
}

const finder = new FruitFinder();

console.log('apple price', finder.getPrice('apple'));

Ноль зависимостей. Мемоизация в Map в конструкторе. Разнесены медоды получения цены и фрукта. Возможность расширения. Возможность инкапсуляции. Решение на ФП против этого выглядит как вермишель.
По вашему это плохо?

Да. Я смотрю на сигнатуру функции getFruit (предполагая, что она бы была) и не понимаю, что она делает, если такого фрукта нет. Кидает экзепшон? Возвращает null? Возвращает созданный по умолчанию фрукт? Идёт на сервер и обновляет список фруктов?


От чего зависит getFruit? Если завтра в вашем классе появится список, не знаю, типов фруктов, какое эта функция будет иметь к нему отношение? А он появится, практика показывает.

Да. Я смотрю на сигнатуру функции getFruit (предполагая, что она бы была) и не понимаю, что она делает, если такого фрукта нет.

Простите, а что непонятного в сигнатуре (this: FruitFinder, name: string) -> { type: string, price: number } | null? :-)


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


От чего зависит getFruit?

От FruitFinder. Детальнее знать и не надо, A — Абстракция.

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

в ФП сигнатура отсекает такую возможность, в чем её суть и состоит. Если функция возвращает null, то эксепшон она не кинет (иначе возвращала бы Either).

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


Не делать дичи — вопрос соглашений и желания программиста им следовать.

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

Добавляете {-# LANGUAGE Safe #-} и исключаете всю эту ерунду.

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

пока есть модули

Из Safe вы можете импортировать только Safe.


FFI

В Safe всё FFI должно иметь тип IO.


позднее связывание бинарников в рантайме

Ну это либо тоже IO, либо таки на уровне драйверов ОС (но обсуждать это перебор, ИМХО).

Из Safe вы можете импортировать только Safe.

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


Ну ладно, отладку заменят типизация и внимательный взгляд, но что делать с счётчиками производительности?

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

Вы про Debug.Trace для отладки бага вот прям сейчас при разработке? Если да, то снимаете прагму, дебажите, навешиваете обратно.


Если вы про нормальное логгирование, то и не надо его было так делать, надо заворачиваться в MonadWriter/etc.


Ну ладно, отладку заменят типизация и внимательный взгляд, но что делать с счётчиками производительности?

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

Вы про Debug.Trace для отладки бага вот прям сейчас при разработке? Если да, то снимаете прагму...

… со все цепочки модулей. Так себе занятие...


Тоже заворачиваться в монаду.

Это если они постоянные.

… со все цепочки модулей. Так себе занятие...

Ну, тут есть несколько вариантов, по мере возрастания сложности и реюзабельности:


  1. Вы же пишете тесты? В тестах Safe всё равно не нужен, так что снимаете только с тестируемого модуля, и всё.
  2. Делаете отдельный Trustworthy-пакет с Debug.Trace и прочими нужными вам функциями, указываете при сборке своего кода -trust чётотам или тому подобное (точно не помню, у меня никогда такой нужды не возникало).
  3. Добавляете в ghc флаг, заставляющий его игнорировать safe-аннотации при сборке.

Это если они постоянные.

Не понял. В смысле?

указываете при сборке своего кода -trust чётотам или тому подобное

О, отлично, значит возможность сделать запрос к серверу из чистой функции снова есть :-)

Нет, вы же не добавили в свой trusted-пакет соответствующую функцию ;)

Но я и не один в команде работаю. И вообще это может быть legacy-код от индусов заказчика.

В любом случае, это opt-in, а не opt-out, и скорее уже организационные проблемы. Язык даёт вам средства очень точного контроля за лазейками, а как вы ими распорядитесь — вопрос десятый.