Комментарии 403
Слишком толсто. Попробуйте еще раз.
Толстовато, но тем не менее. Уверенность, что функции привязвать к объектам это хорошее решение, что сабтайпинг должен происходить обязательно через наследование, что передать IFooBarBaz
с единственным методом намного лучше чем просто лямбду, ...
Ну и да, ФП и правда легче. В ФП паттернов-то всего ничего: IO/Reader/Middleware/..., и большинство из них монады, поэтому их и компоновать как становится понятно, даже если сам паттерн не до конца понятен.
С SQL тут все-таки переборщили: это и правда неудобный язык. Расположение SELECT перед FROM приводит к постоянным использованием имён таблиц перед их объявлением, а это ломает контекстные подсказки в IDE. Не говорю уже о том, что мне вообще неизвестны редакторы SQL, которые бы не тупили и не тормозили.
Ну и фабрики тут упомянуты зря: после появления делегатов/лямбд/стрелочных функций в известных мне языках они выглядят куда компактнее. Не говоря уже о том, что в представленной задаче шаблон Abstract Factory вообще не нужен.
делегатов/лямбд/стрелочных функций
Так это все делалось для приближения ООП к ФП.
Но от "приближения" оно не перестаёт быть ООП. Однако, оно перестаёт содержать страшные классы, введенные ради всего одного метода.
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)
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 — то пишем getName в консоли и дальше браузер сам найдет эту функцию.О чём вы? Она ведь в замыкании. Никакой функции в глобальном скоупе нету. И что нам это даст? Ну найдём мы её в консоли, что дальше?
О чём вы? Она ведь в замыкании. Никакой функции в глобальном скоупе нету.
Но ведь откуда-то мы её получили, раз уж вопрос о поиске определения вообще возник? Вот там где получили, ставим точку останова, и теперь она в текущем скоупе есть.
И что нам это даст? Ну найдём мы её в консоли, что дальше?
А дальше можно перейти к её определению кликнув на неё левой кнопкой мыши. А также при необходимости посмотреть все замкнутые переменные.
Но ведь откуда-то мы её получили, раз уж вопрос о поиске определения вообще возник? Вот там где получили, ставим точку останова, и теперь она в текущем скоупе есть.Звучит как безумно удобно, класс! Не то, что ctrl+click в IDE, гадость какая
Напомню, что это функция может вызываться только раз в 3 месяца, когда полная Луна находится в фазе льва. Будете ждать фазу льва, чтобы отдебажить или просто поменяете время на компе?
Звучит как безумно удобно, класс! Не то, что ctrl+click в IDE, гадость какая
С другой стороны, ctrl+click в IDE хорошо работает только в пределах проекта, а в javascript можно точно так же увидеть исходники многих библиотек.
Напомню, что это функция может вызываться только раз в 3 месяца, когда полная Луна находится в фазе льва. Будете ждать фазу льва, чтобы отдебажить или просто поменяете время на компе?
Напомню, что нам не нужно ждать пока функция будет вызвана, нам нужно ждать пока она станет видима.
в javascript можно точно так же увидеть исходники многих библиотек.В которые тоже можно прыгнуть по ctrl+click. Если они нормально написаны, конечно.
Напомню, что нам не нужно ждать пока функция будет вызвана, нам нужно ждать пока она станет видима.
1. Что тоже может зависеть от фазы Луны
2. Если функция вызывается только через строковый параметр, то мы можем банально не знать, где она вызывается, пока она не будет вызвана.
Если функция вызывается только через строковый параметр, то мы можем банально не знать, где она вызывается, пока она не будет вызвана.
Если мы не знаем ни где функция вызывается, ни где она определена — то мы вообще не знаем про её существование. Так откуда в таком случае исходная задача взялась?
1. Мы не знаем все функции, которые можем вызвать из данного места.
2. Мы не знаем все места, из которых может быть вызвана данная функция.
Это два стула, на которые нужно садиться по очереди.
ФП работает с "чистыми" функциями. Но реальное приложение работает с файлами, БД, внешними сервисами, которые нарушают требования "чистоты", в итоге приходится придумывать костыли
ООП работает с "объектами". Но реальное приложение работает с файлами, БД, внешними сервисами, которые реализованны на чистом С, в итоге приходится придумывать костыли
в ФП переменные иммутабельны. Если у вас есть массив из 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#/…
Приведите бенчмарк что ли в подтверждение своих слов.
Слава богу, в ФП не надо помнить что там по ссылке, а что по значению, можно просто писать бизнес-логику.
А что вы делаете, когда выясняете, что приложение тормозит? Или скажете, что приложения написанные на ФП языке не тормозят?
Одно время в дотнете помню ненаблюдаемое исключение в асинхронной операции могло уронить рантайм.
Как видим, асинхронное программирование — та ещё ерунда.
Дальше-то вы что делаете?
То есть вам нужно знать как ваш код компилируется, чтобы подшаманить и он компилировался во что-то более эффективное. И не факт, что эти шаманства не дадут обратный эффект при обновлении компилятора. В JS это постоянная беда. JIT компилятор даже между запусками может всё по разному соптимизировать исходя из кучи эвристик. Идиомы типа "передача по ссылке" хотя бы детерминированы.
ООП работает с "объектами". Но реальное приложение работает с файлами, БД, внешними сервисами, которые реализованны на чистом С, в итоге приходится придумывать костыли
"Чистый C" часто очень ООПный. Обычно есть "create"/"open"/"new", "delete"/"remove"/"close" и зоопарк функций, принимающих первым аргументом хэндл. Различие с ООП исключительно синтаксическое. Перекладывается подобный API из сишного в ООПный и обратно очень тонкими синтаксическими прослойками.
Приложение на хаскелле который делает "копию на каждый чих" скорее всего будет производительнее вашего мутабельного варианта на Java/C#/… И да, copy elision, даже С++ умеет.
Есть разные алгоритмы, и где-то выгоднее иммутабельность, а где-то она убивает производительность. Если нужна максимальная скорость, алгоритм всё равно будут писать на C++ или подобном языке.
- Любая работа с ресурсами — это костыли. Вообще все функции кроме чистых — это костыли. Либо это dispodsble, либо это RAII — все одно. Вот только ФП не создает иллюзии, что ты работаешь безопастно.
- Точно. Это позволяет программисту почаще задумываться о структурах данных, которые он использует — например в вашем примере с пользователями это был бы связный список или хеш, которые были бы практически столь же эффективны в io нагруженных приложениях. Но вот для складывания матриц в GPU ФП скорее всего не подходит.
- Благодаря function as first class, сопоставлению с образцом и грамотному написанию кода в ФП в большинстве случаев не используется операция присваивания (=) — "переменные" не нужны.
- Исключения — это костыль. В отличие от монады. И это математически доказал Дейкстра в своих основах структурного программирования, которые исключения жестко нарушают. Кроме того, "костыли" в виде Maybe уже написаны за нас. Использовать их — не сложнее чем написать ключевое слово
raise
. - ФП не противоречит ООП. Кроме того, вызовы
u.foo()
иfoo(u)
ничем ни отличаются. Это хорошо видно к примеру в питоне, где метод первый аргументом принимает в обязательном порядкеself
. Модификация объекта — это головная боль с того момента, как программе появляется многопоточность. А она появляется во всех "серьезных" программах. - Это называется декларативное программирование — вершина грамотной архитектуры и композиции программных систем. Приучить мозг, который ходит по шагам, к понимаю декларативных описаний занимает несколько недель — если конечно захотеть.
Что, даже линейные типы — костыли? :-)
Любая работа с ресурсами — это костыли.
Ну тогда в ФП есть костыли, ведь оно умеет работать с ресурсами. Как и любой другой язык, весь все они позволяют читать из файлов. Тогда определение безопасное, я уж думал, под костылем понимается что-то плохое.
Точно. Это позволяет программисту почаще задумываться о структурах данных, которые он использует — например в вашем примере с пользователями это был бы связный список или хеш, которые были бы практически столь же эффективны в io нагруженных приложениях. Но вот для складывания матриц в GPU ФП скорее всего не подходит.
Сколько матриц на ООП вы сегодня сложили? Сириузли, когда у вас проблемы с производительностью, вы и ООП использовать не будете, потому что известная AoS vs SoA проблема.
Благодаря function as first class, сопоставлению с образцом и грамотному написанию кода в ФП в большинстве случаев не используется операция присваивания (=) — "переменные" не нужны.
Если пишут не студенты-третьекурсники, которые обожают играть в кодгольф и пытаются все в одну строчку уложить то проблем этим нет. Опять же, от языка не зависит.
4
По этому пункту нет возражений, спасибо за уточнение.
ФП не противоречит ООП.
ФП как правило является подмножеством ООП в некотором смысле. любая функция ФП будет валидной функцией в ООП, но не наоборот, из-за требований к ссылочной прозрачности.
6
Не понял, к чему это. Декларативное описание, конечно, всегда лучше, потому что позволяет думать о бизнес-логике, а не бойлерплейте. Спасибо за еще одно уточнение.
когда у вас проблемы с производительностью, вы и ООП использовать не будете, потому что известная AoS vs SoA проблема.
Если я поравильно понял о чём вы, то это проблема лишь языков типа Java. В нормальных языках объекты можно размещать прямо в массиве. Не без ограничений, конечно, но всё же.
Нет, это прежде всего проблема для языков типа Си. В Java с этим все настолько плохо, по умолчанию, что проблема AoS vs SoA уходит на второй план. Но годится в качестве примера важности порядка размещения элементов в памяти.
Нету в языках типа Си такой проблемы. Ну кроме совсем маргинальных случаев.
Ну массив из полиморфных объектов в C++ сделать не получится. А если не делать их полифорными, то ООП тут не при чём.
- Можно и полиморфный массив сделать.
- ООП и полиморфизм ортогональные понятия.
Можно и полиморфный массив сделать.
Как это сделать в 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;
не получается, подозреваю, что вызвать методы тоже не выйдет. Соответственно массива полиморфных объектов пока не получилось.
Это следует из определения объекта.
Объект это набор методов, которые чем-то манипулируют. Поля для объекта не обязательны.
В классический объект входит и то, и другое. Вырожденные случаи (методы тоже не обзяательны) — так себе аргумент.
В классический объект входят только методы, существование полей, да, подразумевается, но только подразумевается. Объект, в котором нет полей это нормальная штука.
Слышали про паттерн "Посетитель"?
Получается, что при добавлении ещё одного потомка нужно будет её поправить.
Ужас-то какой..
Смысл полиморфизма в том, чтобы можно было добавлять реализации не внося такие правки.
Это уже ваши фантазии.
написать shapes[0].x.writeln; не получается
https://run.dlang.io/is/asBYYV
вызвать методы тоже не выйдет.
А давайте вы поверите мне на слово, что всё это тоже не реализуемо?
Объект это набор методов, которые чем-то манипулируют. Поля для объекта не обязательны.
Главное, что клиентскому коду не надо знать что там внутри объекта.
Ужас-то какой… [что смысл полиморфизма в том, чтобы можно было добавлять реализации не внося такие правки] Это уже ваши фантазии
Ну может и фантазии, но определённо не только мои. Я думаю любой программист бы сильно удивился, если бы в инструкции к применению библиотеки предлагалось добавить в неё типы, которые он собирается использовать, и потом её пересобрать.
А давайте вы поверите мне на слово, что всё это тоже реализуемо?
Охотно верю, возможности языка впечатляют.
Главное, что клиентскому коду не надо знать что там внутри объекта.
То есть объединение методов и данных в одном объекте это не самое важное? А важно, чтобы клиент не знал как реализованы методы, да? Кстати, такие методы, реализация которых выбирается в рантайме, как раз называюся полиморфными )))
любой программист бы сильно удивился, если бы в инструкции к применению библиотеки предлагалось добавить в неё типы, которые он собирается использовать
Я вам по секрету скажу — эти типы можно передавать библиотеке как статические параметры, а не хардкодить внутри.
То есть объединение методов и данных в одном объекте это не самое важное? А важно, чтобы клиент не знал как реализованы методы, да? Кстати, такие методы, реализация которых выбирается в рантайме, как раз называюся полиморфными )))
Не знать как устроен объект и работать с разными объектами — ортогональные вещи. У вас какая-то беда с логическим мышлением. Мне с вами не интересно общаться.
Я вам по секрету скажу — эти типы можно передавать библиотеке как статические параметры, а не хардкодить внутри.
То есть получается, что в библиотеке будет объявлен тип Shape, а потом пользователь сделает реализации, потом он каким-то образом передаст библиотеке набор реализаций и библиотека включит их в список типов, реализующих Shape и сможет вызывать методы Shape на этих новых для неё типах? Не совсем понятно, как это реализовать с помощью подхода, который вы продемонстрировали кодом на D. Например, если в библиотеке есть метод, который принимает коллекцию реализаций Shape, то как сказать, что там ещё есть наши реализации? Ведь библиотеке надо будет декларировать тип, который принимает коллекция. Можно как-то расширить описание этого типа?
Не знать как устроен объект и работать с разными объектами — ортогональные вещи. У вас какая-то беда с логическим мышлением.
Вы сначала сказали, что важно, чтобы в объекте были методы и поля и это самое важное. А потом сказали, что важно, чтобы клиентский код не знал, как устроен объект. Что важнее?
Тогда можно пример создания массива полиморфных объектов, которые наследуются от абстрактного класса ShapeЕсли нужно именно это, а не массив из вообще любых типов, то это типовая задача в плюсах.
Пример кода на скорую руку. И ничего не нужно добавлять для новых потомков.
A variant, который выше предлагалось использовать, предназначен для других случаев — когда нужно хранить несовместимые между собой типы. Например, shape, string и double.
Мы обсуждали вопрос хранения объектов единым массивом, а не массива ссылок на объекты, находящиеся где попало.
Кстати, вопрос, один элемент массива будет требовать памяти столько, сколько занимает наибольший тип? Или они каким-то чудом ещё и пакуются?
один элемент массива будет требовать памяти столько, сколько занимает наибольший тип?В моем примере на плюсах будет занимать столько, сколько по факту занимают объекты, плюс оверхед на указатели.
В примерах с variant выше само собой будет занимать как наибольший тип, плюс оверхед на идентификатор типа.
То есть вы развели тут дискуссию, даже не сходив по ссылкам? https://habr.com/ru/company/ruvds/blog/462483/#comment_20480627
Ну вы же развели дискуссию о том, что можно сделать на С++ массив полиморфных объектов, хотя код можете показать только на D )))
Однако на всякий случай скажу, что я со ссылками ознакомился, но решил на всякий случай уточнить. Вдруг в случае с коллекциями там есть какая-то магия, кто знает.
Мы обсуждали вопрос хранения объектов единым массивом, а не массива ссылок на объекты, находящиеся где попало.Никакого уточнения про хранение по ссылкам или значению я не обнаружил, обсуждался именно сабтайп-полиморфизм для массивов. Который во всех приличных ООП языках делается через ссылки, и я не вижу никакого практического смысла реализовывать его иначе.
Но если прям очень-очень захочется, то ничего не мешает запилить собственный класс массива, который будет складывать в себя эти же объекты по значению, но итерировать их через указатель.
Всё началось с обсуждения AoS vs SoA проблемы.
https://habr.com/ru/company/ruvds/blog/462483/#comment_20477977
И продолжилось обсуждением in-place расположением объектов в массиве.
https://habr.com/ru/company/ruvds/blog/462483/#comment_20478075
Под вариантом AoS зачастую подразумевается именно массив указателей, особенно если речь идет об оптимизации уже готового кода путем перевода его в SoA (пример с хабра), и тем более если там требовался полиморфизм. А во многих ООП языках объект это вообще ссылочный тип сам по себе, и это никак не избавляет его от сабжа.
Что важнее, под AoS очень редко рассматривается массив variant, потому что это максимально неэффективный способ хранить объекты разного размера в массиве. Если уж отказываться от ссылочности, то в сторону множества однотипных массивов, как в ECS.
По второй ссылке нет ничего про «in-place». Зачем вы меня отправили по ссылкам в другом сообщении я вообще не понял, я и так по ним ходил.
Один и тот же код работает работает с разными типами. Это полиморфизм по определению.
Чаще всего при обработке variant'а у вас по отдельной ветке на каждый
Есть полиморфный код, которому не важно содержимое. Есть мономорфный, который пишется разный для разных вариантов.
У вас больше одного, у меня другого — бывает. Но даже если полиморфного кода у вас мало, он от этого не перестаёт быть полиморфным.
это параметрический полиморфизм
А параметрический полиморфизм — это внезапно уже не полиморфизм? Или как это работает?
у меня есть код на хаскеле
А я не понимаю хаскель.
Во-вторых, если у меня есть код на хаскеле типа
В хаскеле же объединения размеченные, по-этому варианты типами не являются. Вы же не можете создать значение типа Circle или ф-ю, которая принимает только Circle.
Это вопрос писанины на клавиатуре:
Нет, это вопрос семантики. В хаскеле у вас нет соответствующих типов. И, более того, вы их никак не сможете даже объявить или эмулировать. Размеченные объединения можно выразить через обычные объединения и пары — но не наоборот.
Хранит ли массив [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)
— и эти два значения будут различимыми.
А вас не смущает, что та же статья в Википедии приводит примеры из Си?
Но вот для складывания матриц в GPU ФП скорее всего не подходит.
Кстати, внезапно подходит, см. фреймворк accelerate.
Есть неплохая статья на тему, почему эксепшны не очень хороши.
Либо это dispodsble, либо это RAII — все одно. Вот только ФП не создает иллюзии, что ты работаешь безопастно.
Почему абстракции, позволяющие безопасно работать с ресурсами, вы называете иллюзией?
Исключения — это костыль. В отличие от монады. И это математически доказал Дейкстра в своих основах структурного программирования
Можно ссылку на это математическое доказательство?
Модификация объекта — это головная боль с того момента, как программе появляется многопоточность.
Нет там никакой головной боли при использовании правильных абстракций.
Это называется декларативное программирование — вершина грамотной архитектуры и композиции программных систем.
ФП не является декларативным по определению. В ФП описывается не результат, а функция генерации результата из входных параметров.
Ложь в каждом из пунктов.
Чистота функций — это про то, что эффекты функции объявлены в её сигнатуре. IO — ни в коей мере не костыли.
У вас какое-то очень своеобразное представление о чистоте.
https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D1%82%D0%BE%D1%82%D0%B0_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8
Или в компиляторах. Или в статических анализаторах. Или в скрейперах. Или в веб-серверах.
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)).
Это на уровне бреда. Вполне можно и последовательные вычисления организовывать, мешает-то кто?
а ввод-вывод оставить грязным функциям? IO-монада, там, вот это все.
Наличие стандартной затычки красноречиво свидетельствует о протечке абстракции.
Лишний повод почитать структуры данных. Immutable hash trie (через который в ФП реализуются immutable массивы) решает проблему.
Вот и почитайте сколько стоит работа с этими структурами по сравнению с обычным массивом. Как по памяти, так и про процессору.
Зато есть Either (это в scala), Option и до черта всякого остального. Пропуск функции делать не нужно, монада сделает это за вас.
А в языках с поддержкой исключений — компилятор делает эту монаду и Either за вас. Причём в нормальных реализациях ещё и cost-free, без 100500 условных переходов, которые так любит конвейер процессора.
до черта всего. Алгебраические типы данных. Coproduct-ы. Функторы. Аппликативные Фнукторы. Монады, в конце-то концов. Полугруппы, моноиды, группы. Этим всем вполне можно представлять объекты реального мира.
Можно и из буханки хлеба сделать троллейбус. Только зачем?
Ну то есть, откуда-то взяли, что функции только чистые, и даже тот факт, что программа на Хаскеле может читать и писать файлы, и делать многое другое с побочными эффектами, вас не смущает.
Ну да, иммутабельность чего-то стоит, несомненно. Но в тоже время она почти даром дает нам например такие вещи, как возможность версионирования значения, и соответственно, undo.
И так почти по всем пунктам.
И что еще характерно, почти все пункты уже аргументированно разобрали, а комментарий все же имеет +24. Это я не к тому, что он плохой — по большей части тут рассмотрены вполне осмысленные вопросы, которые стоит себе задавать. Но он односторонний — а значит в конечном счете будет как-то способствовать распространению все тех же искаженных представлений об ФП.
Ну да, иммутабельность чего-то стоит, несомненно. Но в тоже время она почти даром дает нам например такие вещи, как возможность версионирования значения
Да вполне терпимо стоит. По асимптотике функциональные структуры данных такие же, как и мутабельные. Зато параллелизм из коробки.
Что-то это уже для меня слишком тонко. Где тут шутка, а где реальное мнение? :)
А ФП вроде Хаскелл позволяет создать 10 вариантов функции с одинаковым именем, но разным типом аргументов.
И при чём тут ФП языки?
перегрузка функций
c++: ravesli.com/urok-102-peregruzka-funktsij
перегрузка методов
java: javarush.ru/quests/lectures/questcore.level05.lecture03
c#: metanit.com/sharp/tutorial/3.5.php
Что было раньше: декларация или операция?
Совсем непонятно про наследование. В каком месте функциональное программирование отменяет концепцию наследования???
Функциональное программирование позволяет получить строгий вывод в терминах объектов. Функциональная парадигма в части своей основной идеи интуитивна ничуть не менее кошечек и собачек.
В этой статье присутствует ложная дихотомия функционального программирования и ООП. Эти подходы совершенно не исключают друг-друга.
Если вы уберете наследование из ООП вы потеряете всё ООП.
Если вы уберете требования чистоты из ФП вы потеряете всё ФП.
В этом и суть, я не вижу возможностей для "промежуточного" подхода, который бы работал. Заигрывания в паттерн матчинг или там функции высшего порядка это мишура, которая используется в ФП, но никак его не определяет.
Да и зачем их смешивать? Какая концепция в ООП позволяет решить класс задач, которые в ФП не решаются легко? Наследование? Тайпклассы справляются с этим больше, композиция считается лучше даже самими ООП идолами вроде Фаулера. Инкапсуляция? Это просто термин для создания абстракций, оно существует в любой парадигме начиная с процедурных алголов, это не прерогатива ООП. Полиморфизм? Существует и в ФП, параметрический работает точно так же, как и подтиповой.
В итоге, что остается от ООП? Какая концепция в нем есть, которая дает какие-то ощутимые преимущества?
Если вы уберете наследование из ООП вы потеряете всё ООП.
Нет, это не так, ООП потеряется только если убрать полиморфизм. Наследование там не главное.
Ну полиморфизм есть и в остальных парадигмах. Вопрос, что есть в ООП такого, чего нет у остальных, и без чего тяжело обойтись.
Если ставить вопрос так, то можно смело сказать, что функциональная парадигма ничем не выделяется, потому что чистые функции можно писать на любом языке.
В ООП центровым является понятие объекта, который есть совокупность методов, предназначенных для манипуляции состоянием и получения информации об объекте. Если методы не полиморфные, это уже не ООП, а элементы ООП.
В ФП центровым понятием является функция, функция должна быть чистой. Если функции не чистые, то уже не ФП, а элементы ФП
Возвращаясь к вашему утверждению о том, что полиморфизм есть в остальных парадигмах, я хочу сказать, что оно некорректно. Полиморфизм можно сделать в языке, который прямо не поддерживает ООП, это правда. Но это будет использованием парадигмы ООП в языке, который на ООП не заточен, а не демонстрацией, того, что полиморфизм есть и в других парадигмах.
Смысл в том, что функция putStrLn не выводит на экран ничего, а создает соответствующий IO. А println именно что выводит, она не создает никакую структуру, которую можно потом проинтерпретировать.
Натянуть на глобус си и считать его чистым при большом желании можно, но зачем? Все понимают, о чем идет речь.
Тут фокус в том, что без контроля над эффектами мы не можем использовать тип IO, а значит и объявлять функцию чистой в силу какого-то там изоморфизма.
Это неважно, есть очевидный изоморфизм между живущими в IO программами на хаскеле и нечистыми программами на какой-нибудь джаве (может, даже на С есть, но мне лень анализировать низкоуровневые свойства С).
Изоморфизм есть только в слчае лени, а её нет.
Если вы сделаете let _ = putStrLn
то ничего не произойдет. Если вы сделаете () _ = println(...)
то эффект будет.
Точно так же вы можете дропнуть весь IO при желании, и его эффекты не сработают.
Отправиться в прошлое в си и отменить печать в консоль вы не сможете.
Но я в хаскеле не объявил функцию, а вызвал её.
Что эквивалентно невызову функции.
Вызов функции в хаскеле эквивалентен невызову в си? Окей, но мне сложно назвать это "похожими вещами", не говоря про "Одно и то же".
Как провести изоморфизм, если хаскель разделяет a
и IO a
, а си – нет?
Собственно main :: IO () в haskell — это просто выражение, которое описывает определенные действия из N возможных. Описание это формируется при помощи композиции (монадической композиции, скажем так), ряда функций вида a -> IO b.
Вот и все. Далее, на основании этого описания компилятор, собственно, генерирует машинный код (или код ассемблера, или код на промежуточном языке, таким, как C).
В C можно написать функцию
T foo (Arg1, ..., Arg N),
которую, если вызывать с одними и теми же значениями Arg1 и ArgN, то она будет всегда возвращать разные значения. Это и есть нарушение чистоты функции. Про «состояние машины» речь не идет совсем.Дело даже не в IO, а в том, что в Си можно вызвать функцию, передав туда указатель на структуру, потом изменить поле структуры, переданной ранее в функцию, потом вызвать ту же функцию, передав туда тот же указатель и получить другой результат.
Если ставить вопрос так, то можно смело сказать, что функциональная парадигма ничем не выделяется, потому что чистые функции можно писать на любом языке.
Можно, но ФП парадигма не в том, чтобы можно было писать чистые функции, а в том, что она требует писать только чистые функции. А что до возможности их писать в других парадигмах… Ложка дегтя портит всю бочку мёда. Чтобы был мед, дёгтя не должно быть вообще.
В ФП центровым понятием является функция, функция должна быть чистой. Если функции не чистые, то уже не ФП, а элементы ФП
Объекты это частные случаи функций, и наоборот. Объект А всегда можно представить как функцию () -> A.
Возвращаясь к вашему утверждению о том, что полиморфизм есть в остальных парадигмах, я хочу сказать, что оно некорректно. Полиморфизм можно сделать в языке, который прямо не поддерживает ООП, это правда. Но это будет использованием парадигмы ООП в языке, который на ООП не заточен, а не демонстрацией, того, что полиморфизм есть и в других парадигмах.
foo<T>
— это полиморфизм без использования парадигмы ООП.
foo — это полиморфизм без использования парадигмы ООП.Если вы имеете в виду дженерики или шаблоны, то это не полиморфизм, потому что там нет вызовов полиморфных методов.
Посмотрите определение. Параметрический и подтиповой полиморфизм — и то и то виды полиморфизма, причем второй частный случай первого.
Я думал, что мы говорим про тот полиморфизм, который используется в ООП. То есть получается про подтиповой. Когда начинаете им пользвоаться — начинаете использовать ООП. Параметрический полиморфизм такой неотъемлемой частью ООП не является.
Полиморфизм в ООП подтиповой, можно реализовать через параметрический, в той же статье это написано.
То есть это просто более богатый языковой концепт, как паттерн матчинг против if
. В итоге мы можем выразить всё то же самое используя его.
Инкапсуляция (не в смысле сокрытия) данных и алгоритмов их обрабатывающих с привязкой их друг к другу by design. Как по мне, то именно это стало основным шагом от структур типа сишных с указателями/ссылками на функции к ООП. Не token = auth.login(auth, username, password), а token = auth.login( username, password).
Ну возьмите Раст, ООП там нет, но там тоже будет auth.login(...)
, ФП никак этому не противоречит.
В Rust нет наследования (привычного), а не ООП.
А в чем еще ООП состоит? Я ниже спросил, никто так и не ответил.
В моем понимании единственным значимым отличие именно наследование и есть.
А в расте ни привычного, ни непривычного наследования структур нет.
Ответ вам дали в этой ветке 4 комментариями выше. Вы ходите кругами.
В моем понимании единственным значимым отличие именно наследование и есть.
А в моём — инкапсуляция и полиморфизм.
А в расте ни привычного, ни непривычного наследования структур нет.
Есть наследование интерфейсов-трейтов, и его более чем достаточно.
В Haskell, насколько я знаю, инкапсуляция только уровня модулей. Напомню с чего началась ветка:
Инкапсуляция (не в смысле сокрытия) данных и алгоритмов их обрабатывающих с привязкой их друг к другу by design.
Если вам не нравится "инкапсуляция" — читайте это слово как "использование значения пользовательского типа как пространства имен для возможных операций, определенных над этим типом".
ООП потеряется только если убрать полиморфизм. Наследование там не главноеЕсть два основных вида полиморфизма: подтиповый и параметрический. Haskell и Rust на полную катушку используют параметрический полиморфизм, при этом по крайней мере Haskell точно не является ООП-языком. Подтиповый полиморфизм невозможен без поддержки наследования на уровне языка. В Delphi/Object Pascal долгое время был только подтиповый полиморфизм, что врочем не мешало этим языкам считаться «ООП».
Получается, что с точки зрения полиморфизма ООП от не-ООП отличает как раз присутствием подтипового полиморфизма, который в свою очередь невозможен без наследования.
В большинстве паттернов ООП тип полиморфизма не важен, из исключений я могу сходу назвать только Composite. Важен тип полиморфизма становится только тогда, когда возникает необходимость сформировать гетерогенную коллекцию из разных реализаций одного и того же интерфейса.
Но даже в этом случае для ООП более чем достаточно тех возможностей, которые дают dyn Trait в Rust или экзистенциальные типы в Haskell, независимо от того как этот тип полиморфизма называется на самом деле.
Но даже в этом случае для ООП более чем достаточно тех возможностей, которые дают dyn Trait в Rust или экзистенциальные типы в HaskellЧто приводит нас к логическому умозаключение что видимо наследование в принципе не так уж необходимо, раз без него можно реализовать ключевые паттерны проектирования.
Только не называйте пожалуйста паттерны проектирования «патернами ООП», из-за этого у некоторых возникает предположение что там где нет ООП, там нет и возможности строить абстракции.
Что приводит нас к логическому умозаключение что видимо наследование в принципе не так уж необходимо, раз без него можно реализовать ключевые паттерны проектирования.
Ну да, так и есть. Это удобный механизм, а не необходимая часть ООП.
Только не называйте пожалуйста паттерны проектирования «патернами ООП», из-за этого у некоторых возникает предположение что там где нет ООП, там нет и возможности строить абстракции.
Так ведь в чистом ФП все эти "паттерны проектирования" оказываются слабо применимы, а те, что применимы — реализуются совсем другим образом.
Не вижу причин не называть их паттернами ООП.
Получается, что с точки зрения полиморфизма ООП от не-ООП отличает как раз присутствием подтипового полиморфизма
Получается да
который в свою очередь невозможен без наследования.
Да нет, наследование не нужно. Нужно позаботиться, чтобы объект реализовал интерфейс и всё.
Ребята, вы забываете, что ООП — это Объектно Ориентированное Программирование. Ничто не мешает реализовать объекты в ФП. Так же как и чистые функции в ИП. Разница лишь в том, на какой тип использования язык ориентирован.
Сахар сахаром, но это именно что ключевая особенность!
Парадигма ООП заключается в рекурсивной декомпозиции программы на ряд взаимодействующих друг с другом объектов, содержащих данные и операции над ними (методы). Таким образом, поддержка ООП со стороны языка как раз и заключается в том, чтобы предоставить синтаксические конструкции для формирования этих самых объектов. Всё, на этом минимально необходимая поддержка ООП и заканчивается: все остальные свойства языка проходят уже как упрощения жизни программиста.
Если же синтаксический сахар не учитывать, то надо записывать Си без плюсов как поддерживающий ООП язык, ведь ядро Linux и оконная подсистема WINAPI написаны именно в парадигме ООП!
Ну да, по вашему определению получается Си это ООП язык, и вы правы, это не так, но не потому, что там нет сахара вызова методов через точку.
Тем не менее, такой подход вряд ли удовлетворит обывателей, которые жалуются что даже «ООП не смогли нормально завезти»: в первом нет наследования, а во втором – параметрического полиморфизма.
По этой причине я предпочитаю подход когда заявляется что язык не является ООП в том смысле в котором вы ожидаете, но это не потому что авторы не справились, а потому что предложены альтернативные (и в контексте данного языка лучшие) подходы по построению абстракций.
Ну, я тоже считаю, что в 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, например, до сих пор кривейший провайдер, который часто генерит полную дичь. Так что приходится писать запрос руками, добиться вменяемого запроса нативными средствами ОРМ просто невозможно.
Это помешает заниматься оптимизацией отдельных запросов.
Он про то, что если у вас есть репозиторий FilterByDate() и FilterByName() вы не можете при реализации FilterByDateAndName() их использовать, придется писать отдельный метод.
Впрочем, как я выше написал, весь смысл в самом антипаттерне репозитория, без него таких проблем нет.
Если бы там только FilterByDate и FilterByName — я бы согласился.
Но ведь я совсем недавно (4 месяца назад) обнаружил ошибку, что запросы всех видимых отчётов, всех "своих" отчётов, и всех видимых, но чужих отчётов не были согласованы друг с другом.
И общих спецификаций использовать там не получается — при попытках поиграться в комбинирование стратегий как минимум один из трёх запросов оказывается неоптимальным.
Вот были бы они все в одном месте — ошибка была бы заметна сразу же, а не на проде...
Вот тут вы отгребете за наследование и пойдете рефакторить. Пример который вы описали – это прямо типичный случай, когда нужно использовать композицию.
Вы принципиально не читаете того, что я вам пишу?
Так выбирайте, все 3 подхода доступны вам в Rust. Обертка != наследование.
Но все три требуют писать вручную 9 делегирующих методов.
Важно, до тех пор пока предок A у вас торчит наружу
А где я писал, что он торчит наружу?
Но все три требуют писать вручную 9 делегирующих методов.Именно. И я вам пытаюсь объяснить почему это скорее всего правильно. Лучше архитектура не там где меньше кода.
Если A вообще нигде планируется использовать, то задача не имеет смысла – просто измените поведение A и не вводите новых сущностей.
Во-первых, а если все-таки планируется?
Во-вторых, а как же SRP?
Лучше архитектура не там где меньше кода.
А причём тут вообще архитектура, если я сравниваю два разных языка (Rust и гипотетических Rust с делегированием) при неизменной архитектуре?
Но все три требуют писать вручную 9 делегирующих методов.
Если вам лень писать 9 методов и поэтому вы ломаете архитектуру лучше повесить атрибут [DelegateCallsTo] чтобы он нагенерировал вам эти прокси.
Вопрос ведь в том, как сделать правильно и расширяемо, а не просто решить задачу наименьшим диффом в гите.
Не говоря о том, что можно забить на закон Деметры и дать юзеру вызвать методы этого объекта напрямую, а не проксировать.
вы ломаете архитектуру
В какой момент?
лучше повесить атрибут [DelegateCallsTo]
Не нашел такого аттрибута, ни в Rust, ни в C#
В какой момент?
В момент когда вы наследуете классы на основании того, что код в теле методов похожий, а не потому, что они по логике отвечают отношению "является".
Не нашел такого аттрибута, ни в Rust, ни в C#
Его несложно сделать самому, написав просто рослин анализатор, который будет на него триггериться и генерировать соответствующий код. Я меня самого три таких генератора написано, на каждый уходит пара вечеров.
Ну реалистичный пример — это GUI. Классический гуй где есть абстрактный класс контрола и вот это все отлично работает, а в расте как сделать нормально до сих пор не придумали.
Других примеров, правда, придумать не получается.
Вообще GUI это монументальная задача с точки зрения архитектуры. Я реализовывал TUI фреймворк на Rust. В принципе тоже самое что GUI но попроще. Пока реализовывал выучил Rust. Паттерн Visitor который распространяет события и выполняет прорисовку спускаясь по иерархии виджетов вполне справляется.
Ну расскажите о своем опыте, потому что то что я видел (gtk-rs/azul/yew...) пока только экспериментирую, и никаких устоявшихся паттерном пока не изобрели.
С другой стороны иерархичный GUI всегда хорошо работал, возьмите хоть WPF, хоть Qt.
А сотню бессмысленных методов можно придумать для любой задачи, означает только плохо продуманную иерархию.
Я просто привел пример задачи, на которую вам сами авторвы гуй фреймворков скажут "да, это проблема".
Подозреваю что проблемы с проектами типа 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, С# и так далее.
Присоединяюсь к вопросу выше. Во-первых ООП как его задумал Кей это совсем не то, что имеется ввиду при оформлении вакансии на современные языки.
Во-вторых, если не это является основным для ООП, то что? "Данные вместе с функциями" в ФП точно так же живут в виде тайпклассов. Тогда что же отличает ООП от всего остального? С моей гипотеза, что это наследование, вы не согласны. Интересует, что же тогда?
Гипотезы здесь не уместны, поскольку есть первоисточники.
В первоисточнике кибернетика — наука об управлении государством, но сегодня это определение никто не использует. Точно так же и ООП, которым мы пользуемся сегодня это не то, что имел в виду Алан Кей, поэтому ссылка на Алана Кея тут неуместна.
Раньше был в этом вопросе на стороне Кея, но после ваших доводов задумался.
Может быть, чтобы не было путаницы и холиворов, стоит различить «объектное» (в стиле Кея) и «объектно-ориентированное» (в мейнстримном понимании) программирование? В конце концов, мы же не говорим «функционально-ориентированное» или «процедурно-ориентированное».
Единственное, что отличает ООП от ФП, — это угол зрения:
Можно применять ООП в функциональных языках и ФП в объектно-ориентированных, ничто этому не препятствует. Разница лишь в акцентах, а не в чём-то фундаментальном.
Вы не можете применять ФП в языках, где функции не чистые. Сам рантайм ФП языков предполагает, что функции чистые, и когда у вас компилятор заинлайнит функцию log(x); return x
, и в эластик перестанут сыпаться логи, вы не обрадуетесь.
В ООП который мейнстрим, важна инкапсуляция, тоесть мы забываем про иммутабельность, в ФП иммутабельность вроде как уже является основопологающей вещью, так как у нас могут быть только вычисляемые выражения. И да согласен что нельзя выкинуть от туда наследование, но к нему надо относиться осторожно, тоесть нельзя переводить наследование на уровень переиспользованию кода, а нужно придерживаться абстракций. В ФП я так понимаю наследования можно добиться через функции высшего порядка, или просто применения разных функций к одним и тем же данным или подобия дженерикам, вполне возможно ФП накладывает более строгий подход.
Я не сильно силен в ФП, от сюда и вопрос, а разве нет подобия наследования в ФП, только оно вполне возможно выглядит иначе?
Наследования (реализации) как такового в ФП нет, но те задачи, которые решаются
наследованием можно решить другими путями.
важна инкапсуляция, тоесть мы забываем про иммутабельность
Какая связь то? Посмотрите модель акторов (на примере Erlang/Elixir)… Инкапсуляция такая, что мейнстрим языкам даже в самых влажных мечтах не снилось, но при этом абсолютно все данные иммутабельны.
Инкапсуляция к мутабельности отношения не имеет вроде как. У нас может быть иммутабельный объект с сильной инкапсуляцией и защитой.
Он просто усложняет вывод типов, без чего в ФП жить становится немного неуютно. Пример Scala в этом плане показателен. Качество IDE и подсказок компилятора, что ни говори, важно.
А ещё оно, в случае мутабельности, ломает вариантность и принцип Лисков.
const getFruitPrice = type => fruits =>
fruits
|> filterByType(type)
|> first
|> get('price');
Для того, чтобы получить цену первого фрукта заданного типа мы отфильтровали весь массив. А если там миллиард элементов?
Фильтрация ленивая ведь. Мы остановимся как только встретим первый подходящий фрукт
Ну или если лайфтаймы и линейные типы напрягают, на скале: https://scastie.scala-lang.org/w6JXitIoRimV6zyfTGRzzg
Кстати, забавно отметить, что я вот изначально не подумал что функция возвращает Option, ведь в массиве такого фрукта может и не оказаться.
Насколько мне известно, итераторы в ECMA2015 уже появились, поэтому ничего не мешает написать такой же ленивый filter
. Уверен, в куче сторонних библиотеку уже давно реализовано, а может даже уже в вендорских функциях.
В гугле первой ссылкой находится вот такая библиотека: https://fitzgen.github.io/wu.js/
На первый взгляд выглядит достойно.
Если функциональное программирование гораздо лучше чем ООП, то почему он не используется более широко чем ООП?
- Потому что в универах обычно его не преподают (по инерции, тянущейся еще с семидесятых, паскалей и си)
- Потому что когда человека отучили от математического определения функции и заменили императивной "процедурой", обратный переход неприятен
- Потому что до недавних пор компиляторы ФП не всегда хорошо оптимизировали код, а приложениям нужна была производительность
- Потому что ООП в 70х тоже было распространено меньше процедурного стиля, это не значит, что ООП хуже него
- ...
Нет, я этого не говорил. Наоборот, есть исследования западных универов, которые давали сначала ФП, после чего людям тяжело было понять императивное. Как я уже сказал, людям тяжело воспринимать ФП не потому, что оно сложнее понимается, а потому что у них первым обычно идет паскаль/си (особенно в снг). Если давать первым ФП, то людям будет выносить мозг запись i = i + 1
. Я таких реально видел. Могли написать на комбинаторах полезную программку, а эту запись не понимали.
Если давать первым ФП, то людям будет выносить мозг запись i = i + 1
Так это не от ФП зависит, а от математики. У нас в школе не было ФП, но эта запись для меня тоже поначалу выглядела непонятно. А все потому что математика не работает с состоянием, любое состояние раскладывается по оси времени на ряд независимых точек, существующих как бы одновременно. Самое близкое это границы суммы (∑), но обычно не уточняется как i переходит из одного значения в другое.
Ну я к этому и веду. ФП математично, поэтому после 10 лет математики его понять и правда проще, чем объяснять концепцию переменных, триггеров, регистров и вот этого всего. Не говоря про то, что человек плохо в голове держит состояние, одна из причин, по которым goto был признан неудачным, и намного лучше справляется с причинно-следственными цепочками "если-то".
Ну я к этому и веду. ФП математично, поэтому после 10 лет математики его понять и правда проще, чем объяснять концепцию переменных, триггеров, регистров и вот этого всего.
Это все ровно до тех пор, пока у нас простенькие алгоритмы вроде получили — преобразовали — вернули. А как только нужны сайдэффекты и прочее ИО — происходит беда, так как корректное "математичное" описание этих вещей — штука далеко не тривиальная, и в итоге императивная модель оказывается значительно проще.
А как только нужны DI и прочие солиды — происходит беда, т.к. простое "оопэшное" описание этих вещей — штука далеко нетривиальная.
В чём проблема с DI?
Так это вы и в ФП не сможете.
Ну почему, каждая 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 на тот случай если вам кажется что код в примере слишком плохой для примера, можно было сделать лучше и вообще проблемы мы сами себе придумали:
Да, так и есть. Но это не отменяет того факта, что выделение и внедрение зависимостей довольно косвенно связано с контролем сайд-эффектов.
Мешает тот факт, что вам его все равно нужно как-то прокинуть до 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м классе рассказывали, значит и первокурсник поймет. Ежели мотивация понимать будет. Вот без мотивации всё плохо...
Не поможет, но чего там вообще доказывать? 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
Ну вы ж спрашивали про "элементарными методами для первокурсника" — вот тут не сильно сложнее.
Здесь показательнее пример с теоремой про существование и единственность решения задачи Коши, которая доказывается дефолтно долго-долго, а через теорему о неподвижной точке — в три-четыре строки (+ 1 лекция чтобы доказать саму теорему о неподвижной точке...)
В курсе диффуров вообще было много такого рода примеров, я постоянно развлекался передоказыванием таких теорем с квадратно-гнездового метода на адекватный, т.к. учить все эти квадратно-гнездовые доказательства мне было банально лень.
а векторное пространство над ℚ — это просто морфизм в соответствующее кольцо эндоморфизмов, как-то очевиднее
Дык, а почему морфизм-то этот — единственный? :)
Типа, на эпиморфизм сократили?
Передавать зависимости через конструктор, в чём проблема то?
А как только нужны DI и прочие солиды — происходит беда, т.к. простое "оопэшное" описание этих вещей — штука далеко нетривиальная.
Тривиальная, почему нет? Собственно, проще триггерорв/регистров и всего вот этого. И заведомо проще "математичного" ИО.
Там извлекают самое лучшее из ООП и заворачивают это в ФП.
fsharpforfunandprofit.com/posts/fsharp-is-the-best-enterprise-language
Есть мнение, что ФП без HKT невозможно. По крайней мере я не знаю, как писать в ФП стиле без монад.
в Ocaml HKT нет, но если кто скажет что это не фп язык, то даже не знаю какой он.
В F# вычислительные выражения в качестве и.о. монад есть, хоть и определены ad-hoc без HKT.
В ООП, как и в реальном мире, все сущности – это объекты, у каждого объекта есть класс (кошечки и собачки). Классы могут наследоваться (и кошечки, и собачки – это животные). Можно инкапсулировать один объект в другой (как двигатель в машину). Можно часть объекта сделать приватной (как переписка в телеге с Машей), а часть – публичной (как профиль в инстаграмме). Еще можно делать фабрики объектов – это как настоящие фабрики, только любых объектов. Ну и так далее.
Поздравляю, если вы поняли то что я написал выше, значит вы усвоили принципы ООП. Теперь вы – энтерпрайз программист.
ИМХО, это и есть главное зачем нужно ФП. Конечно, еще не забываем многопоточность.
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 (предполагая, что она бы была) и не понимаю, что она делает, если такого фрукта нет.
Простите, а что непонятного в сигнатуре (this: FruitFinder, name: string) -> { type: string, price: number } | null
? :-)
Ну да, формально сигнатура не отсекает возможности пойти на сервер или кинуть исключение, возможно мешая тем самым каким-то автоматическим инструментам, но на практике лично мне глядя на сигнатуру очевидно, что она таки возвращает в этом случае null.
От чего зависит getFruit?
От FruitFinder. Детальнее знать и не надо, A — Абстракция.
Ну да, формально сигнатура не отсекает возможности пойти на сервер или кинуть исключение, возможно мешая тем самым каким-то автоматическим инструментам
в ФП сигнатура отсекает такую возможность, в чем её суть и состоит. Если функция возвращает null, то эксепшон она не кинет (иначе возвращала бы Either).
В ФП, в некоторых языках, бывает и ⊥. И даже unsafePerfornIO
, так что даже запрос к серверу формально сигнатурой не может быть исключен.
Не делать дичи — вопрос соглашений и желания программиста им следовать.
Полностью исключить ерунду невозможно, пока есть модули, FFI, позднее связывание бинарников в рантайме и левые драйвера в ядре ОС. Есть лишь уровни уверенности в отсутствии этой ерунды.
Из Safe вы можете импортировать только Safe.
Ну, значит конкретно эту проблему вы решили. Заодно избавились от удобного языкового средства трассировки, теперь если что не так — только отладчиком...
Ну ладно, отладку заменят типизация и внимательный взгляд, но что делать с счётчиками производительности?
Вы про Debug.Trace для отладки бага вот прям сейчас при разработке? Если да, то снимаете прагму...
… со все цепочки модулей. Так себе занятие...
Тоже заворачиваться в монаду.
Это если они постоянные.
Да. Я смотрю на сигнатуру функции getFruit (предполагая, что она бы была) и не понимаю, что она делает, если такого фрукта нет. Кидает экзепшон? Возвращает null? Возвращает созданный по умолчанию фрукт? Идёт на сервер и обновляет список фруктов?
Ну, типа, если бы вы не поленились и посмотрели сигнатуру, а не рассуждали, то:
interface Map<K, V> {
clear(): void;
delete(key: K): boolean;
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V): this;
readonly size: number;
}
и, с-но, сигнатура getFruit будет:
(method) FruitFinder.getFruit(name: string): {
type: string;
price: number;
} | undefined
Теперь, посмотрев на реальную сигнатуру, вы же сможете ответить на свой вопрос? :)
ЗЫ: и, да, этот код тайпчек не пройдет на this.getFruit(name).price
ЗЫЫ: и непонятно, при чем тут ФП-то
Я всё ещё не знаю, не лезет ли эта функция на сервер.
Ну, вы ни в каком языке не знаете, у вас может быть только определенная уверенность (чуть большая или чуть меньшая). Вот в тс, если ф-я не возвращает void или промис — значит на сервер, скорее всего, не лезет и сайд-эффекты не совершает.
возвращает мне эта функция на самом деле undefined, если она ничего не нашла, или же какой-то по умолчанию сконструированный фрукт.
Это вы зависимые типы хотите уже. Хотя, опять-таки, непонятно при чем тут ФП — типы-то ему ортогональны. ФП не про типы и не про чистоту, ФП — про нетривиальную композицию функциональных объектов. Когда вы redux-saga юзаете, вы вполне себе занимаетесь полноценным фп, хотя это может быть даже не ts, а js, т.е. без типов вообще (хотя генераторы и в тс нормально не типизируются). Т.к. это свободная монада прикрученная к эквивалентному do-нотации синтаксису. С другой стороны, если вы на идрисе пишите и при этом у вас все чисто, да и вообще каким-нибудь point-free обмазались, это не значит, что у вас ФП (хотя и одни функции кругом). Ну точно так же как если вы в ф-ю reduce вместо IAbstractReducer какого-нибудь передали просто лямбду — ФП не появилось от этого, вопрос ведь в ментальной модели.
Если у вас функция lookup :: Ord k => M.Map k v -> v, то вы знаете, что построить объект она не может
Эм, так в get(key: K): V | undefined; тоже нет ничего про V. И вы видите, что она возвращает undefined, если ключа нет. А в вашем случае если ключа нет, то что происходит? Как это определить из сигнатуры? И, кстати, из этой сигнатуры отсутствие тотальности как раз не следует никак. Для того, чтобы сделать заключение о тотальности, вам нужно знать семантику ф-и (то, что мапа, и что вы по ключу что-то достаете, и что ключа может не быть, и вот если его нет...).
ИМХО цены для рассуждений о коде в этой композиции не так уж много.
А при чем тут рассуждения о коде? Какое отношение рассуждения о коде имеют к ФП?
Толку вам с того, что у вас есть какая-то функция с именем map и другая функция с именем filter, и вы что-то там как-то куда-то им передаёте?
Так я об этом и говорю. Совершенно без толку. Важно, не какие у вас filter и map, или какие типы, а какая у вас ментальная модель программы.
Тот факт, что вы на идрисе что-то написали и доказали и вот это вот все — какое отношение имеет к ФП? Да никакого, ведь вы вполне могли написать все это в рамках императивной модели. Про которую тоже можно доказывать не хуже чем про ФП.
Вот и получается, что типы — не являются ни достаточным ни необходимым условием для ФП, раз уж у нас на практике существует ФП без типов и не-ФП с типами.
Я вам даже больше скажу — ФП вполне может быть без лямбд :)
Если пытаться формализовать хаскель, то там как раз любой возвращаемый тип на самом деле объединяется с ⊥.
Это понятно, но остаётся открытым вопрос: как понять из одной только сигнатуры, что произойдет когда нужного ключа нет?
Если что, возможности тут следующие помимо вашей:
функция может вернуть значение по умолчанию, только не через класса Default, а просто сохраненное в самом контейнере;
функция может сформировать значение по умолчанию исходя их ключа (используя сохраненную в самом контейнере фабрику);
функция может вернуть значение по ближайшему ключу снизу или сверху (Ord же!).
Безусловно, семантику (или денотацию) знать надо (или хотя бы надо знать. что такое Map). Но разве это не подразумевается предметом нашего обсуждения?
Так весь фокус в том, что если мы знаем что такое Map
FruitFinder
— то нам сигнатура уже и не нужна. А если не знаем — то сигнатура, конечно же, ответит нам что функция нам вернет — но без завтипов не ответит в каких случаях.
А вот и нет, вариант номер 3 никак при этом не отсекается.
Если подумать, то и варианты 1 и 2 тоже не отсекаются — просто в мапе может быть одним из полей Maybe v
и поведение lookup при создании мапы разными конструкторами может отличаться.
Как, если мапа пустая?
Если совсем-совсем пустая — можно и ⊥ вернуть. А если чем-то наполнена — то ⊥ возвращать не обязательно.
Который будет Nothing, потому что откуда взяться значению типа v при создании мапы, если мы про v ничего не знаем?
Повторюсь: поведение lookup при создании мапы разными конструкторами может отличаться. Вам недостаточно знать, что есть функция empty :: Map k v
, вам надо видеть полный список возможных конструкторов и мутаторов.
Если смотреть самое начало — то там вы утверждали, что по сигнатуре getFruit непонятно что функция делает. Так вот, по сигнатуре lookup точно так же непонятно что она делает. Разве что понятно что на сервер она не будет ломиться, если только её создатель вообще понимает зачем вообще в языке ограничения. Но то, что она для отсутствующего ключа гарантированно вернет ⊥ — уже из сигнатуры не видно.
А насчет тотальности — да, согласен. Из двух функций (empty
и lookup
) хотя бы одна обязана не быть тотальной, и скорее всего это не первая.
Я бы очень удивился, если бы обход пустой мапы возвращал ⊥
, это больше на баг похоже.
Учитывая, что каждая строчку каждой программы может возвращать ⊥
, чтобы хоть как-то можно было судить о коде предлагаю исключить это из рассмотрения. Толку говорить, что в любой момент программа зависнет особого нет.
⊥ происходит.
Ага, но жопа неявно в любом типе есть, то есть это как если бы с get в ts всегда в любом типе был null/undefined. Оно так и есть в non-strict режиме, но в non-strict никто не пишет, он нужен только при портировании.
При этом нас компилятор никак не форсит эту жопу обработать — а обработку undefined форсит. Ну т.е. сигнатура в тс заведомо информативнее, безопаснее, и вообще круче :)
Если пытаться формализовать хаскель, то там как раз любой возвращаемый тип на самом деле объединяется с ⊥.
Ну вы ж понимаете, что это как "у нас в любом типа может быть налл" :)
Но разве это не подразумевается предметом нашего обсуждения?
Тогда подразумевается и знание того, как себя ф-я ведет в исключительных ситуациях. Какие кидает исключения, да даже с какой сложностью работает.
Только метатеория (пока?) не очень хорошо проработана, но то такое.
Вообще, она была исчерпывающе проработана еще в те времена, когда ФП не было даже в проекте.
Но, раз уж мы об этом заговорили, то в чём смысл и ценность ФП?
Ну свою точку зрения я выше уже указывал — для меня ФП начинается тогда, когда мы сами определяем средства (де)композиции. В чем ценность? Ну, с-но, в том, что мы можем выбрать (или создать) тие средства, которые хотим и которые наиболее удобны в данном конкретном случае.
(или, строже, что есть функция empty :: Map k v)
Ну пусть empty возвращает жопу, с-но пустых мап нет :)
fun main() {
val fruitRepository = FruitRepository(
Fruit(type = "apple", price = 1.99.toBigDecimal()),
Fruit(type = "orange", price = 2.99.toBigDecimal()),
Fruit(type = "grape", price = 44.95.toBigDecimal())
)
val price = fruitRepository.findFruitPriceByType("apple")
println(price ?: "priceless")
}
data class Fruit(val type: String, val price: BigDecimal)
class FruitRepository(vararg fruits: Fruit) {
private val index = fruits.map { it.type to it }.toMap()
fun findFruitByType(type: String): Fruit? = index[type]
fun findFruitPriceByType(type: String): BigDecimal? = findFruitByType(type)?.price
}
Что мы имеем:
1. Мы можем передать этой функции FruitFinder, но ей может быть нужен, скажем getPrice, но не нужен какой-нибудь getByWeight, который может быть в FruitFinder. Кроме того, ей и только ей может быть нужен какой-нибудь getByColor, которого в FruitFinder не предусмотрели. Что делать? Наследовать много FruitFinder-ов для каждого такого алгоритма или делать один универсальный монструозный FruitFinder?
2. В FruitFinder пока нельзя добавлять фрукты. Создавать FruitFinder с расширенным списком каждый раз накладно. Допустим, наследуем FruitFinder с возможностью добавления. Тогда чтобы привести список в исходное состояние (для следующей итерации) придется использовать функцию удаления последних добавленных фруктов.
3. Учитывая мутабельность списка в предыдущем пункте, один и тот же список фруктов в FruitFinder нельзя использовать в разных потоках (при параллельной обработке разных запросов). Для обработки каждого запроса нужно копировать свой экземпляр списка.
4. Новый программист пытается использовать FruitFinder. Он видит иерархию FruitFinder-ов или один FruitFinder с множеством методов. Что происходит внутри FruitFinder — нужно разбираться. Если не разбираться, могут быть сложные последствия для скорости работы, памяти и прочего. Но программисту возможно нужна только одна очень простая функция…
Ни-одна из этих проблем при ФП подходе даже не возникает. Проблемы 1 нет, потому что мы не задаемся целью склеивать функции по классам. Мы просто используем функцию, которая нам нужна. Проблемы 2 и 3 решаются за счет добавления дополнительных фруктов к списку без изменения исходного списка (односвязанный список или что-то похожее).
Проблема 4 не возникает потому, что программист видит функции, которые получают нечто на входе и выдают нечто на выходе, вместо черных ящиков — объектов, которые делают больше чем ему нужно и устроены сложнее чем нужно (банан, обезьяна и все джунгли). И так далее.
На самом деле, в ФП всё так же возникает проблема номер 2 (вариация "Создавать FruitFinder с расширенным списком каждый раз накладно"). Если его и правда накладно создавать — его и в ФП будет накладно на каждой итерации создавать, и вы тут ничего не измените. А решения этой проблемы, используемые в ФП, и для ООП сгодятся.
Новый программист пытается использовать FruitFinder. Он видит иерархию FruitFinder-ов или один FruitFinder с множеством методов. Что происходит внутри FruitFinder — нужно разбираться. Если не разбираться, могут быть сложные последствия для скорости работы, памяти и прочего.
Любой метод этого объекта соответствует функции из ФП. Чистый метод — чистой функции, нечистый — функции использующей какую-то из монад.
Единственное место, где может "просесть" производительность — в конструкторе, ведь метод всегда принимает объект целиком и не может принять его часть. Но тут тоже всё просто: надо держать конструктор простым, а класс — в рамках SRP.
На самом деле, в ФП всё так же возникает проблема номер 2 (вариация «Создавать FruitFinder с расширенным списком каждый раз накладно»). Если его и правда накладно создавать — его и в ФП будет накладно на каждой итерации создавать, и вы тут ничего не измените. А решения этой проблемы, используемые в ФП, и для ООП сгодятся.Простейшее решение добавления элемента для ФП — создавать новый список, содержащий еще один элемент и указатель на исходный список. Таким образом можно получить сколько угодно вариантов расширенных списков одновременно и все они будут использовать один и тот же исходный список. Это возможно потому, что в ФП у нас все «кишки наружу» и список доступен непосредственно. Как это просто сделать в объектном стиле, если FruitFinder скрывает список и не позволяет никому пользоваться его элементами?
Любой метод этого объекта соответствует функции из ФП. Чистый метод — чистой функции, нечистый — функции использующей какую-то из монад.Но речь же не о математической эквивалентности. Основная проблема программирования — борьба со сложностью. Есть много задач которые отлично декомпозируются в стиле ФП и для которых чистых функций достаточно, а если их не достаточно, можно чуть отклониться в обычный процедурный стиль и все равно все останется простым и понятным.
Как это просто сделать в объектном стиле, если FruitFinder скрывает список и не позволяет никому пользоваться его элементами?
Добавить метод в FruitFinder, которому все внутренности будут доступны.
При нарушении SRP — вынести сам список в отдельный класс. Ну или взять готовый класс из подходящей библиотеки.
Но речь же не о математической эквивалентности. Основная проблема программирования — борьба со сложностью. Есть много задач которые отлично декомпозируются в стиле ФП и для которых чистых функций достаточно...
… а есть много задач, которые отлично декомпозируются в объекты, и для которых методов достаточно. Иногда это даже те же самые задачи.
Добавить метод в FruitFinder, которому все внутренности будут доступны.В итоге мы рискуем получить большую и сложную для понимания систему для решения довольно тривиальной по своей сути задачи.
При нарушении SRP — вынести сам список в отдельный класс. Ну или взять готовый класс из подходящей библиотеки.
Вероятно та же проблема возникла бы с ФП, если задача не сводится к одному сложному алгоритму с явным входом и выходом.
… а есть много задач, которые отлично декомпозируются в объекты, и для которых методов достаточно. Иногда это даже те же самые задачи.Это конечно же правда. Пример с FruitFinder был хорош тем, что этот класс сам по себе уже довольно искусственная абстракция и возможно здесь ФП стиль предпочтительнее.
Это возможно потому, что в ФП у нас все «кишки наружу»
Основная проблема программирования — борьба со сложностью.
Вы не находите связи между этими утверждениями? Как раз для борьбы со сложностью в ООП существует инкапсуляция, которая скрывает сложную реализацию (те самые кишки), предоставляя простой интерфейс. Невозможно передать удовольствия, когда приходит задача поменять поведение во всей программе, а вам нужно поменять пару строчек в одном файле, потому что сложность удалось обуздать.
Задача, требующая сложного поведения при довольно простых алгоритмах отдельных методов тоже может быть представлена в стиле ФП. Стоит ли это того, не знаю, у меня нет такого опыта. Наверняка это не простая задача, которая не по зубам новичкам в ФП.
Задача, содержащая сложный алгоритм (анализ, оптимизация, моделирование) и очень простое поведение (просто данные на входе и на выходе) может быть представлена как совместное поведение множества объектов, которые в итоге реализуют требуемый алгоритм но ИМХО, это плохая идея во всех отношениях. Придется сочинить массу неустойчивых и искусственных концепций и все излишне усложнить.
— очень неблагодарное дело перезакладываться на все возможные случаи изменений требований
— конкретно это требование (случайно пробно добавляться по несколько фруктов...), в реальных проектах будет возникать сравнительно редко
— в ООП такие вещи тоже не так сложно решаются, созданием метода клонирования объекта, сериализации или просто вынесением фруктов в отдельную сущность и добавлением её к экземпляру класса агрегацией.
Мой тезис в том, что, к сожалению, очень многие из тех, кто активно ратует за ФП, не освоили в должной мере ООП, пишут плохой код на ООП (взять даже эту статью) и жалуются, что парадигма плохая. Парадигма замечательная, просто для хорошего кода не достаточно знать функторы и монады, хорошо бы знать наблюдатель, фабрику, стратегию, зачем нужна слабая связанность, когда ей можно пожертвовать, когда и зачем использовать инкапсуляцию и т.п… Только это долго изучать, ФП кажется коротким путём, но это только кажется, почти весь промышленный софт всё таки по ООП написан.
— конкретно это требование (случайно пробно добавляться по несколько фруктов...), в реальных проектах будет возникать сравнительно редкоДля моих проектов это самые обычные требования. Мне кажется главная проблема книг и статей по ООП и ФП в том, что они никак не классифицируют задачи и делают вид будто описываемая технология одинаково эффективно и просто декомпозирует любого рода задачи («просто» — в смысле легко понять как это работает и легко переделать или расширить). Когда автор описывает некие патерны, он подразумевает решение неких привычных для него задач. Но объяснять он это может на жирафах и слонах, из любви к искусству. Если программист, который читает книгу не сталкивался с теми же задачами, как автор, возникает куча недоразумений и недопонимания (это мой случай).
В следующем фрагменте кода продемонстрированы проблемы,
вы продемонстрировали проблемы непонимания основ структурирования кода потому что вы делаете хеш таблицу
prices = { 'apple': [1.99 , 1.59 ..?] , 'orange': [2.99] , 'grape' : [44.95]}
там просто должно быть несколько цен иначе код вообще бессмысленнен и вы хотите выбрать первую цену которая у яблок, это
lambda type_name : prices.get(type_name , [0])[0]
а функциональное программирование уменьшает флудокод в 200500 раз, делает его более чистым и отлично сочетается в OOP в нормальных руках
for (const u in users)
это вообще не о функциональном программировании.
а вот за ненужные циклы вместо хеширования надо сразу увольнять с занесением в бд дураков. На практике видел следующий проэкт -> человек делает через JDBC запрос выбрать все то есть select что то там * и потом В ПРОГРАММЕ ЦИКЛОМ ФИЛЬТРУЕТ, причём каждый раз при выборе по имени.
Так что там или селект или хеш таблица {userSelectionStrategy: user_name}
Для того чтобы достичь достойного уровня в сфере ООП нужно 20-30 лет.надо прослушать нормальный курс софтваредизайна и поучаствовать в нескольких проэктов чистого кода, тогда будет видно что весь код описанный в этом посте г, и например если флудить фабрику, то пусть хотя бы одна для всех фруктов, а чтобы не искать, каждый тип заносится в отдельный лист. И это не вопрос функционального программирования.
class Fruit {
constructor(type, price) {
this.type = type;
this.price = price;
}
}
class FruitRepository {
constructor() {
this.fruitListPerTypes = {};
}
class FruitFactory {
constructor(FruitRepository f) {
this.fruitRepository = f;
}
Fruit newFruit(type, price)
a = Fruit (type, price)
this.fruitListPerTypes.get(type,[]).append(a)
return a
Fruit newApple()
return this.newFruit('apple', price)
...
getCheapestFruit = lambda type : this.fruitRepository.get(type, [null]).sort()[0]
Автору нужно обьяснять почему интерактивные девайсы никто не пишет и не будет писать на фп? Я про самолеты, корабли, автомобили и т.д.
Огромный круг инженерных задач, с которыми фп адекватно не справится.
Возможно в анализе данных и комбинаторных задачах оно хорошо, но это тонкий слой в промышленной разработке.
К тому же, решение подобых задач неплохо описывается с помощью конечных автоматов. А конечные автоматы как раз естественно выражаются через фп.
var fruits = new[]
{
new { Type = "apple", Price = 1.99 },
new { Type = "orange", Price = 2.99 },
new { Type = "grape", Price = 44.84 },
};
var applePrice = fruits.FirstOrDefault(f => f.Type == "apple")?.Price;
Console.WriteLine($"apple price: {applePrice}");
В 2 раза короче варианта на ФП (который, к слову, тоже можно было укоротить). То же самое с другими примерами.
А если кто-то обновит прайс, а там не будет яблок (или опечатка), прилетит apple price: null
? Или NRE?
А где тут ООП (да и ФП)? Вызвали метод для массива структур и достали одно поле. На Си будет то же самое, с разницей на то что там нет ?
оператора и экстеншн метод будет записан буквально Enumerable.FirstOrDefault(fruits)
А где тут ООП (да и ФП)?
А их в такой простой задаче нигде и не должно быть явно видно.
делать геттер цены через прогон массива это очень плохое инженерное решение. Почему
— у вас может не быть яблок (но цена на них нужна, для расчетов ситуаций, когда яблоки будут)
— у вас может быть разная цена на яблоки (но функция вернет только первую)
— этот геттер однажды может быть поставлен в функцию в другом геттере, и через N таких относительно дешевых и незаметных изменений вы получите функцию со сложностью n в степени M
Короче, как быстрый костыль функция работает, но код надо рефакторить — слишком опасный костыль, который однажды может вылететь и выбить глаз в продакшене
Суть ФП была в том, чтобы сделать функции безопасными, а не примкнуть к злу. Тут функция как бы чистая, но прогон массива в функции — это потенциальная засада, которую надо как-то обезвреживать — хотя бы неймингом. Из названия функции не понятно, что она гоняет массив, она выглядит как обычный геттер, а геттеры обычно дешевы и просты. Тут это не так — функция и не дешева на больших массивах, и не проста
Так как ФП в продакшне применяется исчезающе мало, соответственно, никто его и не учит — получается замкнутый круг. Всё нужное и ФП-подобное мы имеем в LINQ, а остальное не прошло проверку временем и индусиками.
Есть ли дебаггеры и как можно расставить точки останова в цепочке функций?
Возможно, вы по привычке пытаетесь оформлять алгоритмы очень императивно, с явными последовательностями и шагами. Считается и эмпирически демонстрируется, что чем ближе к чисто-функциональному дизайну (особенно если присыпать правильными типами), тем чаще заветное «скомпилировалось – работает». Хотя, конечно, в произвольно взятом случае приближаться так можно произвольно долго или дорого.
Я, кстати, использовал разок GHC-шный отладчик – в 2012 в Leksah, где он был довольно неплохо прикручен. И как раз на алгоритме, наспех переписанном с императивного.
О нём будут вспоминать как о каком-то казусе. Почему?
Потому что за ООП не стоит никакой… математики.
ООП есть порождение (во всяком случае восхваление и возвеличивание) банды четырёх.
ООП основан на «здравом смысле» в наблюдение и размышлении над природой.
ООП не основан на… математике.
За ООП не стоит никакая математическая теория.
«Здравый смысл» подвёл ООП.
Даже убогий и смешной SQL, созданный НЕ для программистов, а для конечных юзеров (бухгалтеров, библиотекарей, архивистов, кладовщиков, экономистов и торговцев), к которому лет 20 назад Oracle хотел «присобачить» объекты и развить до ООП языка (попытка эта тихо провалилась — почему тихо, а не с треском? — потому что лет 20 назад ООП гордо лез во все языки и дыры и громко его «выпинывать» никто не решился), даже SQL имеет за собой… математику (реляционную алгебру)!
ФП имеет за собой… математику. Много математики. Много.
Как протянуть много математики в… язык? — Это проблема. Верно.
Вспомним как понятие "множество" входило в среднюю школу. Как ввели в школьные учебники понятие «конгруэнтность», которое всё же выбросили из школьных учебников через
несколько лет, ибо даже учителя математики НЕ понимали в чём разница между «треугольники конгруэнтны между собой» и «треугольники равны между собой».
Язык ФП труден хотя бы тем что функция отдельно и данные отдельно.
И программист НЕ видит места в программе где данные «поступают» в функцию.
Он может поставить точку останова только в функции, а ему бы хотелось поставить точку останова «на входе» в функцию — а это трудновато сделать в ФП, где программисту приходится писать программу «жонглируя» функциями и только потом запускать её (программу), давая на вход (высоко высоко) начальные данные, ожидая где-то внизу (низенько низенько) конечных данных.
Логирование, конечно, можно вставить в каждую функцию, но программист привык ставить точку останова МЕЖДУ функциями, а в ФП это затруднительно. — Там «река данных» течёт от одной функции к другой часто довольно извилистым и непрозрачным для программиста путём, что… напрягает.
Но, наверняка, привыкнуть можно. Человек… пластичен и привыкает ко многому. ;-)
Математика победит здравый смысл. (С)
А ООП умрёт как недоразумение. (вангую)
а нет ли какой-то статистики, что 95% программистов пользуются ООП?Пойдут они туда же куда пошли фортранщики, кобольщики, плщики (PL/1), дельвятники-паскальщики. Коих было, в своё время, тоже 95%, поди.
Ландшафт меняется. (С)
(Но где-то в стороне, не реагируя ни на смену ландшафта, ни на попытки обратить их в веру микроядерную, сидят на глыбе монолита и пилят его ввысь плоские (не ФП и не ООП) сишники линукс-ядерщики.)
На фронтенде всё что можно написать на React.js будет написано на React.js — и разработчики React.js ясно сказали — уходим ребята от ООП (от классов). А куда они могут уйти то? — да к ФП.
Если глянуть синтаксис Java 8 — то те кто знает просто Java 7 — не смогут уже прочесть программу со стримами. Понятно что это «мимикрия под ФП» — но это показательно — ООП уже катится вниз.
С бейсика на паскаль, с паскаля на си перелезть можно просто заучиванием непонятных фраз, но познать абстракции все не смогут, а программистов нужно очень большое количество и сразу. Язык для них будет существовать и никуда не денется.
Я понимаю, что у вас восприятие, искаженное фронтендом, по-этому поясню некоторые детали, которые вы упустили.
Во всей индустрии, кроме фронта, ФП-хайп уже прошел. Его пик пришелся примерно на десять лет назад. С тех пор все полезные "ФП-фичи" были впитаны мейнстримными языками, и на этом, в общем, развитию ФП в менйстриме пришел конец. На фронте сейчас проходят тот же путь, что проходили в других отраслях десяток лет назад — и с тем же результатом, так что не надо лишних надежд.
И еще, вы можете привести пример равных треугольников, которые бы не были конгруэнтными? Или пример конгруэнтных, которые бы не были равными?
На фронтенде всё что можно написать на React.js будет написано на React.js
Реакт, с-но, закончился, еще год, максимум два хайпа — и он начнет умирать, и умрет окончательно в течение 5-10 лет, точно так же как аналогичные реакту подходы умерли и покрылись пылью времен на бекенде с развитием этого самого бекенда.
В ФП есть четкое формальное требование — чистота функций. Если удалось ее добиться, это в любом случае будет уже ФП. При этом следующая задача программиста — получить код в котором назначение каждой функции будет понятным, функции будут получать не слишком много параметров и не будут создавать копию всего мира на каждый чих.
В ООП нет понятия «чистый объект». Объект должен работать только со своими данными, но он может иметь ссылки на другие объекты и обращаясь к их методам получать данные из других объектов или модифицировать эти данные. В пределе формально обычный процедурный код можно назвать объектым, просто оснастив все структуры данных гетерами и сетерми и распределив функции по классам произвольно. Поэтому, нет четкого критерия «объектности кода». Программист должен стремиться к получению объектов с как можно более абстрактным назначением и как можно более скрытой реализацией этого назначения, но это требование интуитивное а не формальное.
Математика — это лишь язык для описания каких-то отношений между какими-то сущностями. Со своими плюсами и минусами. Но это не единственный язык.
Не ветвь математики, а способность другими средствами выразить то же, что выражает математика. Хороший пример — традиционная и формальная (математическая) логика. Они об одном и том же, но описывают одни и те же сущности разными способами. И традиционная не ветвь формальной, это два способа представления одного и того же. От того, что мы можем одну и ту же мысль выразить на русском и на английском, англйиский не становится ветвью русского.
Математика — это лишь язык для описания каких-то отношений между какими-то сущностями. Со своими плюсами и минусами. Но это не единственный язык.
То есть:
«Язык (парадигма) программирования за которым НЕТ математики» (например ООП) — НЕ ХУЖЕ чем «Язык программирования за которым стоит раздел математики.» (например SQL)?
Я вас правильно понял?
VolCh
Хороший пример — традиционная и формальная (математическая) логика. Они об одном и том же, но описывают одни и те же сущности разными способами.На традиционной логике НЕЛЬЗЯ построить язык программирования. От слова — совсем нельзя.
VolCh
И традиционная не ветвь формальной, это два способа представления одного и того же.Традиционная, то есть житейская, логика никак не связана с формальной логикой. Они НЕ представляют одно и тоже.
— Каждое воскресение ваш сосед ходит в баню. Сегодня воскресенье. Ваш сосед пойдёт в баню?
— Нет, он заболел.
(из реальных опросов задаваемых учёными жителям крайнего Севера, которые никогда не ходили в школу — вывод учёных был прост — Формальной логике нельзя научиться вне школы, например дома.).
Язык (парадигма) программирования за которым НЕТ математики» (например ООП) — НЕ ХУЖЕ чем «Язык программирования за которым стоит раздел математики
Хуже-лучше оценки по каким-то критериям, обычно с разным весом и, главное, субъективным. Мне вот мало важна лежит математика или нет за языком.
На традиционной логике НЕЛЬЗЯ построить язык программирования. От слова — совсем нельзя.
Отчего же?
Традиционная, то есть житейская, логика никак не связана с формальной логикой. Они НЕ представляют одно и тоже.
Вот тут вы не правы, на уровне определений. Традиционная логика — это аристотелевская логика:
Все люди смертны.
Сократ — человек.
Следовательно, Сократ смертен.
Традиционная, то есть житейская,
Что, простите?
традиционная и формальная (математическая) логика. Они об одном и том же, но описывают одни и те же сущности разными способами.Под «традиционной» VolCh понимает «житейскую» логику, противопоставляе ей математическую логику.
Но эти «логики» перпендикулярны! (С) А VolCh этого НЕ видит и НЕ понимает.
В давнишней, если не ошибаюсь, передаче Гордона
«2002-12-15
Диалектическое мышление
психологи Николай Веракса, Игорь Шиян
Является ли аристотелевская логика единственно верной? Есть ли иные, более продуктивные способы мышления? О том, почему не всякое оперирование противоположными понятиями является диалектической структурой»
Два психолога рассказывали о тестировании взрослых людей не обучавшийся в школе (они их нашли где-то на севере СССР), на вопросы такого типа:
— Каждое воскресение ваш сосед ходит в баню. Сегодня воскресенье. Ваш сосед пойдёт в баню?
Взрослые люди не понимали смысла этих вопросов. Они могли ответить так:
— Нет, он заболел.
Отсюда психологи сделали вывод, что житейская логика перпендикулярна математический логике. И понимать смысл таких вопросов можно только и только через обучение в школе. Как арифметики, так и языку (правописанию и чтению), ну и прочим наукам (геометрии, физике ...)
— Каждое воскресение ваш сосед ходит в баню. Сегодня воскресенье. Ваш сосед пойдёт в баню?
Взрослые люди не понимали смысла этих вопросов. Они могли ответить так:
— Нет, он заболел.
Отсюда психологи сделали вывод, что житейская логика перпендикулярна математический логике
На деле в варианте, названном «житейской логикой» просто опущен еще один предикат. Т.е. «сосед идет в баню» верно тогда, когда верны «сегодня воскресенье» и «нет других ограничений». Эти ограничения в «житейской логике» учитываются по умолчанию, и это никак не противоречит формальной логике. КМК.
Эти ограничения в «житейской логике» учитываются по умолчанию, и это никак не противоречит формальной логике. КМК.Как это никак не противоречит?
— Каждое воскресение ваш сосед, если здоров, ходит в баню.
— Сегодня воскресенье и ваш сосед здоров. Ваш сосед пойдёт в баню?
— Нет, баня сгорела.
— Каждое воскресение ваш сосед, если здоров и баня не закрыта и не сгорела, ходит в баню.
— Сегодня воскресенье и ваш сосед здоров и баня не закрыта и не сгорела. Ваш сосед пойдёт в баню?
— Нет, дорогу замело.
— Каждое воскресение ваш сосед, если Земля не налетит на земную ось и нет и не образуется никаких препятствий, ходит в баню.
— Сегодня воскресенье и Земля не налетела на земную ось и нет и не образовалось никаких препятствий. Ваш сосед пойдёт в баню?
— Нет, он вчера вернулся от свёкра и там уже ходил в баню.
Вы никак не придёте к формальной логике, через добавление еще одного предиката к каждому логическому предложению.
Почему? — Ибо:
— Если А и B («нет иных запретов»), то C.
— A истинно. В(«нет иных запретов») истинно.
— С истинно?
— Житейско-логически ответ — нет, ибо D истинно («он вчера вернулся от свёкра и там уже ходил в баню»).
Тогда вы добавите ещё предикат:
— Если А истинно и B истинно и D ложно, то С.
— A истинно, B истинно. D ложно.
— С истинно?
— Житейско-логически ответ — нет, ибо F истинно.
Житейская логика постоянно вводит в свои рассуждения при выводе новые (не определённые ранее) предикаты. Мы не знаем что появится на этот раз. — Так построить математическую логику нельзя.
Житейская логика перпендикулярна математической логике. (С)
P.S.
VolCh
Все люди смертны.
Сократ — человек.
Сократ смертен?
Житейско-логически ответ: нет.
Его никто никогда не видел.
Никто не видел его могилы.
Возможно от его имени выступали разные лица и рассказчики.
Тогда «Сократ» — не более чем «коллективный псевдоним», типа Козьмы Пруткова, который в общем то может существовать столько, сколько захочет, так как… люди, вещающие под именем «Сократ», могут меняться с течением времени!
Иначе говоря несмотря на то, что в конкретный момент времени Сократ есть человек, но он не смертен, так как это не более чем «коллективный псевдоним» смертных множества людей.
P.P.S.
mayorovp
Не могли бы вы привести цитату, где он её так понимает?Смотрите:
VolCh
Не ветвь математики, а способность другими средствами выразить то же, что выражает математика. Хороший пример — традиционная и формальная (математическая) логика. Они об одном и том же, но описывают одни и те же сущности разными способами. И традиционная не ветвь формальной, это два способа представления одного и того же.
VolCh противопоставляет формальной (математической) логике некую НЕ математическую логику. Он называет её традиционная.
Я не знаю иной НЕ математической логики, кроме житейской.
Если вы знаете, подскажите мне? Где про неё можно прочитать? Если ли учебники традиционной логики, что вовсе не ветвь формальной (математической) логике?
Если же VolCh под традиционной логикой понимал НЕ житейскую, или НЕ только житейскую, а какую-то более формализованную логику, позволяющую как-то НЕ пересекаться с житейской логикой, но всё же НЕ являющейся ветвью математики, то и в этом случае и эта некая логика остаётся перепендикулярной формальной (математической) логике.
P.P.P.S.
"ТРАДИЦИОННАЯ ЛОГИКА — этап исторического развития логики, противопоставляемый современной символической логике и характеризующийся рядом специфических черт.
Формирование этого этапа началось сразу же после основополагающих работ в области логики Аристотеля и было вызвано невостребованностью со стороны ученых тех новаторских идей и методов, которые были применены Стагиритом при создании первой логической теории — силлогистики. Это касается прежде всего введения переменных, что позволило четко выявить логические формы высказываний, и использования аксиоматического метода. Окончательно Тл. сложилась после издания Арно и Николь «Логики Пор-Рояля».
Характерной чертой Т.л. было тесное переплетение логической проблематики с психолого-гносеологическими положениями. Логика трактовалась не как теоретическая дисциплина, а как некое психологически окрашенное учение о понятиях, суждениях, умозаключениях и простейших методах научного познания. Типы и виды понятий, суждений и умозаключений в Т.л. просто перечислялись, а само обсуждение логических проблем осуществлялось на естественном языке, страдающем многозначностью и неопределенностью. Это был тупиковый путь, и потому не случайно И. Кант высказал мысль о полной завершенности логики как науки и ее неспособности к дальнейшему прогрессу.
В то же время следует отметить дидактическое значение Т.л. Ее изучение является хорошей пропедевтикой для дальнейшего освоения современной символической логики. К тому же последняя часто характеризуется как логика, которая является традиционной по предмету и математической по методу."
Словарь философских терминов. Научная редакция профессора В.Г. Кузнецова. М., ИНФРА-М, 2007, с. 594."
P.P.P.P.S.
ООП это тупик и не способно к дальнейшему прогрессу. (Вспоминая Канта).
Как это никак не противоречит?Обычный человек, который знает, что каждую субботу его сосед ходит в баню пойдёт его искать именно в бане, если он понадобится ему в субботу. А если у него спросят, то ответит: «да в бане он, он там каждую субботу». А вы написали принятую за уши глупость
Как это никак не противоречит?..
Житейская логика постоянно вводит в свои рассуждения при выводе новые (не определённые ранее) предикаты. Мы не знаем что появится на этот раз. — Так построить математическую логику нельзя.
Противоречия нет. Потому что любая новая система высказываний, с новыми предикатами, будет подчиняться формальной логике, будет поверяться и подчиняться тем же законам.
С какой стати вы, видя, что система меняется (при том, что рассуждения все так же построены в рамках формальной логики), считаете, что «здесь нет матлогики» — загадка века. Надо понимать, что новые вводимые предикаты — не бесконечны, и они исчислимы, причем заранее известны. То, что «обыватель» вспомнил про прочие ограничения и достроил в голове систему с новым предикатом, а наблюдатель — нет, не делает логику обывателя не-формальной. Там не предикаты D,E,F, а множество, огромное множество (но исчислимое и известное) этих предикатов. И все равно они все конъюнктивны.
Наблюдатель в данном случае должен был не строить ограниченную систему, а получить от знатока предметной области (испытуемого в данном случае) исчерпывающий список условий наступления события.
Потому что любая новая система высказываний, с новыми предикатами, будет подчиняться формальной логике, будет поверяться и подчиняться тем же законам.Но мы не может предвидеть появление новых предикатов и главное — мы не сможем на основе уже предложенных предикатов сделать логический вывод. — Ибо любой новый предикат может в логике вывода быть за истину или ложь.
В жизни обычно добавляют в договор ограниченное число предикатов — и потом ещё слово «иные причины» и (или) слово «форс-мажор».
Но это обычно приводит к обращению в суд, так как стороны не могут логическими рассуждения придти к однозначному выводу.
Другими словами — на житейской логике, как и используя ООП мы не можем оперировать детерминированными функциями и построить ФП.
Житейская логика перпендикулярна математической логике. (С)
Но мы не может предвидеть появление новых предикатов и главное — мы не сможем на основе уже предложенных предикатов сделать логический вывод
Это значит, что ваша система не полна. А не то, что она не подчиняется мат/формальной логике.
В жизни обычно добавляют в договор ограниченное число предикатов — и потом ещё слово «иные причины» и (или) слово «форс-мажор».
Но в примере про баню это самое «иные причины» не было добавлено, а потом делается удивленное лицо «как так, откуда в реальном мире взялись ограничения?»
Это значит, что ваша система не полна. А не то, что она не подчиняется мат/формальной логике.Учёные просто задавали логические задачи.
Задавали тем кто ходил в школу и тем кто никогда в школу не ходил.
Те, кто в школу ходил — те эти задачи решали. А кто не ходил — не могли решить.
Вывод учёных был прост — Логике учат только и только в школе.
А тем, кто был на домашнем обучении, задавали? Включая тех, у кого были специальные занятия логики, а не "выучите математику, заодно и про логику что-то узнаете"?
А тем, кто был на домашнем обучении, задавали?
Конечно тех учили логике.
Включая тех, у кого были специальные занятия логики, а не «выучите математику, заодно и про логику что-то узнаете»?Не знаю таких. Предмета «Логика» нет в школе.
Логику учат только и только на геометрии. Ну и потом на математике.
Немного логики есть в иных предметах.
Нет логики ни в истории, ни в географии, ни в биологии.
Есть некая логика в грамматике. ;-)
Нет логики на уроках труда. — Следуй указаниям учителя. Вжик, вжик.
Нет логики на уроках физры. — Делай как показывает учитель. Раз, два, раз, два.
Домашнее обучение? — тут будет логика раз будет геометрия, но как быть с социализацией? При домашнем — социализации проблема (общение с другими, непосредственное, не через письма и чаты). Но это не тема этого треда.
Ибо любой новый предикат может в логике вывода быть за истину или ложь.
В логике нету истины или лжи, там есть выводимые утверждения и невыводимые.
Истина или ложь появляется на интерпретации, но у логической системы может быть много разных интерпретаций, которые не будут элементарно эквивалентными (т.е. одно и то же утверждение может быть на одной интерпретации истинно — а на другой ложно). Более того — если ваша система достаточно мощна и непротиворечива — наличие таких интерпретаций вам гарантировано.
Под «традиционной» VolCh понимает «житейскую» логику, противопоставляе ей математическую логику.
Не могли бы вы привести цитату, где он её так понимает?
Под «традиционной» VolCh понимает «житейскую» логику, противопоставляе ей математическую логику.
С чего вы взяли, что я её понимаю так? Я её понимаю как аристотелевскую и её развитие, а не житейскую.
Про математику верно подмечено. Да, за FP стоит математический аппарат, доказавший непротиворечивость, полноту и кучу других фундаментальных вещей. Но FP, по крайней мере в том виде, в котором он существует сейчас в виде Haskell, Erlang и бога сегодняшних программистов JavaScript, еще не подходит для многих задач. Да, морды сайтов на нем можно писать, простые утилитки можно писать. Только, видимо, многие сегодняшние программисты просто не в курсе, что их обожаемый JavaScript не может работать сам по себе. Для этого ему нужен Node.js или хотя бы Chrome. А они сами написаны отнюдь не на JS.
Что? Разве есть что-то, написанное не на JS? Не верю!
Так что OOP, может, и умрет, но не сегодня, не завтра и вряд ли послезавтра. А FP, чтобы стать применимым для написания большого софта, придется «испортиться», включив в себя some flaws of not so pure programming paradigms. По крайней мере на сегодня, пока Intel нам еще не продает квантовые процессоры в коробочках.
Функциональное программирование: дурацкая игрушка, которая убивает производительность труда. Часть 1