Комментарии 48
Всегда умиляли эти теоретики от программирования, которые не закончили ни одного проекта.
Всегда хочется спросить - "когда ты написал свою миллионную LOC?"
Автор не упоминает LINQ и ему подобных, которые стали частью языка.
Дополню в ту же тему
Проблема Х
Все преимущества Раста показываются на примерах уровня хелловорлда - пара строк кода. И эта статья не исключение.
А если мы начинаем смотреть реальные проекты на Раст - "там водятся тигры" (С)
Я не говорю, что Раст совсем плохой - наоборот, в нем многое сделано хорошо (да, но далеко не все). Но показать это надо правильно - на реальных задачах.
F#, Haskell, Lisp, Scala — все они уступают Rust в скорости и потреблении ресурсов.
Citation needed. Да и в целом, «язык А быстрее языка Б, вот и пластмассовые бенчмарки подоспели» — это либо глупость, либо вредительство. Если вагон задач, в которых любой из вышеперечисленных языков уделает раст, как бык — овцу.
Раньше ФП ругали за медлительность. «Это не для серьёзных задач», «это только для прототипов».
Citation needed. Да и в целом, хаскель ругают за одно, а скалу — за совсем другое.
[…] попробовать функциональный Rust. Без
mut.
Совет прям от Григория Остера. А как насчет стейта в асинхронных тредах? В базу сохранять?
Не, я понимаю, что ваш подход прекрасно работает на примерах в три строки из туториала. Но вы тут заикались про продакшн, а там, знаете, бывает хайлоад, большая конкурентность, долгоживущие треды, и всякая другая непотребщина.
Не нужно тащить в джаву лямбды, в джаваскрипит классы, в раст — иммутабельность, пожалуйста. Возьмите подходящий вашим препочтениям язык (функциональных иммутабельных языков — полно́) — и пишите на нем. А в расте даже акторная модель через жопу сбоку прикручена.
Проблема II: Императивность
Вот тут не согласен с предложенным решением. Имхо плодить пятистрочные функции по типу bring_beer это вредный подход. Это выглядит красиво, не несёт никакой практической пользы, и в будущем усложняет отладку и понимание кода в целом. Я ещё могу понять, если это делается в нескольких местах сразу (но в таком случай любой, кто слышал про DRY выделит это в функцию, и про такое в статье можно и не писать). А делать пятистрочные функции, которые вызываются ровно в одном месте - это просто портить опыт чтения кода сверху-вниз, когда приходится прыгать куда-то там, где объявлена эта маленькая функция.
Проблема III: Некорректная или недостаточная обработка ошибок.
Здесь я бы упомянул, что неожиданно может оказаться, что вызываемая функция паникует, и это будет такой же тостер, сжигающий дом. Но nopanic это отдельная банка червей, как говорится.
Проблема исключений № 1: Вы не знаете, что именно может упасть.
В расте есть такая же проблема, хоть и в меньшей степени. Вызываемая функция может неожиданно запаниковать, как я уже выше писал.
Немного ошарашивает ваш религиозный фанатизм в отношении функционального программирования и поливание помоями императивного. Вам бы попросить чажпт (или с чьей помощью вы пишете) подуспокоится. Но может мне просто такой стиль не заходит :)
И немного раздражает вот этот подход с аналогиями, где каждый пункт начинается с “представьте…”. Ей богу, вы не для детсадовцев пишете, а для инженеров, тут не надо на грушах и яблоках объяснять.
В расте есть такая же проблема, хоть и в меньшей степени. Вызываемая функция может неожиданно запаниковать, как я уже выше писал.
Это не совсем так, #[no_panic] может в рамках крейта доказывать, что указанная функции не паникует. А при сборке с LTO, и с учётом других крейтов (транзитивно).
Ну так это сторонняя библиотека и вроде как у неё есть какие-то заковырки? В любом случае не часть стандартной поставки языка
Ну и ничего из перечисленного не является какой-то специфической особенностью именно вот функционального стиля.
Обработка ошибок путём явного возврата их по значению в Rust-е вообще из Go, а в Go из Alef, его прямого предшественника от тег же авторов. А Alef - это диалект Си. Вот уж никогда никто не назвал бы его функциональным языком.
А что до неприменимости ФП для системного программирования, и тут бы я тоже поспорил. Системное программирование - это отнюдь не только про перекладывание байтов из регистра в регистр. Более того, критический, в плане производительности, путь - это процентов 10 общего кода. А остальное - код логически сложный, но без особых притязаний в плане производительности. И писать его в стиле “выжимаем каждый такт из каждого байта” нет никакой необходимости. Скорее, его надо ментально упрощать, пусть даже и за счёт некоторых потерь в скорострельности, потому, что логические ошибки в этом коде слишком дорого обходятся.
Похая нейростатья, с дурацкими примерами.
Функциональный стиль не является декларативным ни в коем разе, и все эти фразочки "вы описываете, ЧТО вы хотите, а не КАК это сделать" не объясняют ничего. Функциональный стиль является функциональным, и демонстрировать его отличие от не-функционального надо так:
Сейчас будет не-функциональный (процедурный)
int a;
int b;
int result;
void calculate() {
sum = (2*a + 3*b)/7 - 5;
}
void main() {
a = 5;
b = 8;
calculate();
print("Result is:" + result);
}А сейчас - функциональный:
int calculate(int a, int b) {
return (2*a + 3*b)/7 - 5;
}
void main() {
int a = 5;
int b = 8;
int result = calculate(a, b);
print("Result is:" + result);
}Обе эти программы выдают правильный результат. С точки зрения CPU процесс вычисления в обоих случаях будет практически одинаковым (забудем про нюансы передачи параметров, считаем, что код заинлайнится). Принципиальна разница с точки зрения разработчика в том, как происходит shunting данных. В случае с функцией параметры в неё пропихиваются вызывающей стороной, у вызывающего нет никакой возможности не передать параметр, у него не получится забыть это сделать. А у функции нет необходимости куда-то идти за значениями, они в аргументах. Также у вызывающего функцию нет необходимости куда-то идти за результатом вызова, он не доступен никак иначе, кроме как значение, получаемое в месте вызова. Иммутабельность - это следствие.
Статья, к сожалению, имеет мало общего с реальным ФП и больше похожа на агитку.
// Императивный Rust
let mut squares = Vec::new(); // Мутабельно.
for num in 0..10 { // Водим программу за ручку.
squares.push(num * num); // Мутируем.
}
println!("{:?}", squares);
// 5 строк. Цикл. Мутации. Грязно.
В самих по себе мутациях нет ничего плохого. Проблемы начинаются тогда, когда они становятся нелокальными. В вашем примере squares это новый вектор, в его мутациях нет ничего плохого, так как к нему не имеют доступа другие части программы. Более того, я спокойно могу перенести этот код в хаскель, который достаточно строго следит за мутациями:
-- Иммутабельная переменная
squares :: [Integer]
squares = runST $ do
-- Начало мутабельного блока
squaresRef <- newSTRef [] -- Мутабельная переменная
forM [0..9] $ \i -> do
modifySTRef squaresRef (\old -> old ++ [i^2])
result <- readSTRef squaresRef
return result
ФП по умолчанию решает эту проблему: значение может быть пустым только явно — с помощью
Option<T>
Option<T>— сигнал: этого значения может не быть. Если возвращаемый тип функции не обёрнут вOption— вы не можете получить пустоту, даже если очень захотите.
foo :: Integer
foo = undefined
ghci> foo
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
undefined, called at /home/orenty7/projects/haskell/test/app/Main.hs:21:7 in main:Main
Такой полиморфизм не нагружает систему в рантайме: вместо этого он компилируется в мономорфные структуры, и за счёт этого полиморфизм не стоит вам производительности в функциональном программировании!
Открываем core (промежуточный язык, в который компилируется хаскель):
makeSound1
= \ (@a)
($dSpeak :: Speak a)
(animal :: a)
(eta :: State# RealWorld) ->
hPutStr2
stdout
(($dSpeak `cast` <Co:2> :: Speak a ~R# (a -> String)) animal)
True
eta
Аргумент $dSpeak это указатель на структуру Speak a, где лежит функция speak. Ничего не мономорфизировалось, производительность падает так же, как в ООП языках. И как в расте, при использовании dyn Trait
Ни один тип не преобразуется в другой неявно. Вы не можете сложить строку с числом. Вам нужны
format!,to_string,parse, либо другие функции для преобразования типа. И так не только в Rust — это суть всего функционального программирования. Если вам нужен другой тип — преобразуйте его явно:
-- Haskell
main :: IO ()
main = putStrLn $ show 67 -- Явно преобразуем Int в String c помощью show
Только если скормить этот код GHC, он выдаст:
• Defaulting the type variable ‘a0’ to type ‘Integer’ in the following constraints
(Show a0) arising from a use of ‘show’ at app/Main.hs:39:19-22
(Num a0) arising from the literal ‘67’ at app/Main.hs:39:24-25
Благодаря полиморфизму 67 может быть любым типом для которого определён инстанс Num. Потому что под капотом там что-то типа fromInteger (67 :: Integer), и результат может быть любым. В том числе строкой, если для неё есть инстанс Num:
instance Num String where
fromInteger x = show x
main :: IO ()
main = putStrLn 67
В Java, Python, JS, C# — сборщик мусора. Он соберёт, когда-нибудь. Может быть, через минуту. Может быть, через час. Может быть, когда файловых дескрипторов не останется.
В питоне есть context manager, в JS try-finally. Java и C# я не пользовался, но поисковик выдаёт, что в Java есть Closable, AutoClosable и try-with-resources, а в C# есть using и IDisposable
В функциональных языках, вроде Haskell, Scala и F# сборщик мусора, кстати, тоже есть, и поэтому я и веду этот блог; поэтому я выбрал именно Rust и именно ФП: функциональщина — не враг скорости.
В хаскеле для этого есть bracket, или, если вы достаточно смелый, чтобы тащить это в кодобазу – ContT
В ФП нет исключений. Вообще.
Их в хаскеле штуки четыре разных есть :
Нечёткие исключения – могут возникать в чистом коде
Чёткие исключения – бросаются в IO
Асинхронные исключения – прилетают из другого потока
MonadFail– специфичные для конкретной монады исключения:NothingдляMaybe,LeftдляEither, чёткие исключения для IO, и так далее
Композиция. Оператор
?из примера выше — это как «попробуй выполнить, если ошибка — выйди». Чисто. Явно. Без скрытых переходов и побочных эффектов.
Одна только проблема: Result<A, E1> и Result<B, E2> нормально не композируются. Дальше идут увлекательнейшие приседания в попытках объединить эти два типа ошибкок.
В TS они просто слабые
Вы просто не умеете их готовить:
type Option<T> = {
marker: 'none'
} | {
marker: 'some',
value: T
}
// В конечном итоге, это просто надстройки над JS. В рантайме они исчезнут.
Вы умудрились попасть в едва ли не единственную фичу тайпскрипта, которая доживает до рантайма: Enums are real objects that exist at runtime (source)
Исключительно из любви к формальности:
foo :: Integer
foo = undefinedХаскелисты традиционно притворяются, что undefined (и прочих нетотальностей) не существует, и вообще что Hask — настоящая категория.
Олсо, если подходить формально, то
ФП по умолчанию решает эту проблему: значение может быть пустым только явно — с помощью
Option<T>
имеет некоторый смысл даже в нетотальных языках при наличии undefined, error и прочих подобных: эти «пустые» значения невозможно наблюдать в чистой части программы в рамках стандартной метатеории [разных вершин] лямбда-куба, без компиляторной магии (ST/IO — компиляторная магия, seq — компиляторная магия, да и она тоже не меняет большую операционную семантику, потому что сходящиеся термы что с seq, что без seq имеют одну и ту же нормальную форму).
Иными словами, ты в чистом коде не можешь написать
isBottom :: Int → Bool
isBottom n = n == ?⊥?поэтому про наличие пустоты ты не узнаешь.
Аргумент $dSpeak это указатель на структуру Speak a, где лежит функция speak. Ничего не мономорфизировалось, производительность падает так же, как в ООП языках.
После инлайнинга и/или при наличии доступного анфолдинга в использующем модуле оно вполне может специализироваться, если компилятор решит, что это выгодно (я про это целый пост писал пару лет назад!)
И как в расте, при использовании dyn Trait
Для этого в хаскеле тоже надо явно обмазываться экзистенциальными типами, ну либо убить оптимизатор (что можно сделать в любом языке).
имеет некоторый смысл даже в нетотальных языках при наличии
undefined,errorи прочих подобных: эти «пустые» значения невозможно наблюдать в чистой части программы…
Прикольно, не задумывался об этом. Хотя следовало бы, учитывая, что в статье про imprecise exceptions вопрос наблюдаемых и ненаблюдаемых штук в чистом коде довольно подробно разбирался
без компиляторной магии (
ST/IO— компиляторная магия,seq— компиляторная магия…)
Как с помощью IO пронаблюдать понятно, а как можно с помощью ST или seq?
После инлайнинга и/или при наличии доступного анфолдинга в использующем модуле оно вполне может специализироваться, если компилятор решит, что это выгодно
Так-то и в объекто-ориентированных языках вызовы девиртуализироваться могут, но полагаться на оптимизатор ни в ООП, ни в ФП, имхо, не стоит. Особенно как автор статьи, заявляющий, что вообще всё мономорфизируется.
я про это целый пост писал пару лет назад!
А не осталась, случайно, ссылка?
Как с помощью
IOпронаблюдать понятно, а как можно с помощьюSTилиseq?
ST — это обобщённый IO (в прямом смысле, IO ≅ ST RealWorld).
seq — так, для полноты. Его расходящийся результат тоже ненаблюдаем в этом же смысле.
Так-то и в объекто-ориентированных языках вызовы девиртуализироваться могут, но полагаться на оптимизатор ни в ООП, ни в ФП, имхо, не стоит.
Только для девиртуализации компилятору надо доказать, что в данном месте других инстансов быть не может, а это задача сильно более нелокальная и сильно более сложная, чем просто подставить параметр, когда он доступен (что выполняется субъективно ИМХО куда чаще, чем условия для девиртуализации, потому что мало кто играется с экзистенциальными типами или полиморфной рекурсией и при этом ожидает должный перф).
Особенно как автор статьи, заявляющий, что вообще всё мономорфизируется.
Это он зря, конечно.
А не осталась, случайно, ссылка?
Последний пост в моём стандалон-блоге.
А ты знаешь, где обсуждается проблема нелокальности ленивости? То есть, как с этой нелокальностью бороться, да и вообще делается «постновка задачи».
Где обсуждается — не знаю. Но решение как в идрисе (когда ленивость явно помечается как Lazy) выглядит вполне себе дружественным к тайпчекингу и локальным рассуждениям. Ты, по крайней мере, знаешь, что именно у тебя ленивое.
И пару слов про microHS черкануть в формате поста можешь?
Ох ты ж блин - статья является отличной иллюстрацией тезиса: "Человек, впервые взявший в руки молоток - рассматривает все окружающие предметы как разновидности гвоздей" (С).
Да, мы все в курсе, что исчисление функций эквивалентно машине Тьюринга - а значит любая вычислимая задача может быть описана как функционально, так и императивно. То есть - даже мой поход в магазин в субботу можно описать как функциональное преобразование из цифр на банковском счете в набор продуктов в холодильнике...
Вопрос - нахрена, а главное - зачем ?! Если объективно процесс ествественным образом описывается как набор шагов, меняющих состояние системы - то почему бы не последовать совету берестяной грамоты N35, и не описать его на языке программирования тем же (т.е. императивным) способом ? В чем смысл натягивания совы на глобус и вытягивания цепочки лямбд - которые потом еще хрен отладишь нормально (потому что не видны промежуточные преобразования коллекций) ?
Я лично вижу два случая, когда ФП надо применять:
У вас есть большой и полезный фреймворк, который вы можете кастомизировать - и для этого вам надо в глубины этого фреймворка передать определенное поведение. Так вы вот это поведение запихиваете в функцию, и ее как first-class-object передаете в глубины чужого кода. В моем примере с магазином - это как если бы кто-то уже написал фреймворк, автоматизирующий все промежуточные этапы, и надо было только подать внутрь функцию выбора товаров с полок...
Либо у вас функциональное описание является естественным описанием процесса. То есть реально процесс зависит только от входов, не порождает side-effects (логгинг и инструментация весело машут ручкой в этот момент - ибо являются сайд-эффектами), и т.д.
Ну и до кучи скажу что архитектура современных ЭВМ нихрена не заточена под ФП - она заточена под императивное программирование с мутабельным состоянием в RAM. Соответственно, ФП всегда будет подвергаться performance penalty по сравнению с императивным кодом. Другое дело - что производительность софта за последние десятилетия опустили на такое дно, что добавление туда еще и ФП - уже существенно ничего не меняет...
То есть - даже мой поход в магазин в субботу можно описать как функциональное преобразование из цифр на банковском счете в набор продуктов в холодильнике… Вопрос - нахрена, а главное - зачем ?! Если объективно процесс ествественным образом описывается как набор шагов, меняющих состояние системы
Даже процесс похода в магазин вы внутри себя описываете декларативно. Вы думаете в терминах того, что надо сделать (выйти из квартиры, вызвать лифт, перейти дорогу, дойти до магазина), а не как это сделать (это будет настолько объёмно, что я даже проверку открытия-закрытия двери писать не буду), и вы притворяетесь, что у ваших действий нет несмоделированных сайд-эффектов: вы не рассматриваете износ петель двери в процессе выхода из дома, выхлопные газы автобуса, смену настроения продавщицы в магазине из-за рисунка на вашей кредитной карточке, и так далее.
Вопрос - нахрена, а главное - зачем ?!
Потому что о чистой функциональщине без неявных сайд-эффектов проще рассуждать и доказывать её свойства.
В чем смысл натягивания совы на глобус и вытягивания цепочки лямбд - которые потом еще хрен отладишь нормально (потому что не видны промежуточные преобразования коллекций) ?
Прелесть в том, что их не нужно отлаживать так часто, как вы думаете :]
Ну и до кучи скажу что архитектура современных ЭВМ нихрена не заточена под ФП - она заточена под императивное программирование с мутабельным состоянием в RAM. Соответственно, ФП всегда будет подвергаться performance penalty по сравнению с императивным кодом.
А почему у меня тогда на хаскеле регулярно получается писать код по производительности на уровне плюсов (иногда — быстрее)?
Потому что нет, конечно, не «всегда будет», это non sequitur. Компилятору ФП никто не мешает компилировать ФП-код во вполне производительный императивный мутабельный код. Более того, начиная с некоторого уровня сложности компиляторов это проще: типизированное ФП (а другое не нужно) выражает в типах больше инвариантов и даёт компилятору больше информации о том, какие оптимизации не изменят семантику программу, и их можно безопасно применить. Просто до этого уровня в императивном (и близком к железу — привет условной джаве) программировании программист делает ту работу, которую может (и должен) делать компилятор.
А вообще забавно то, что я как раз на неделе начал читать сборник публикаций первой конференции History of Programming Languages, и там целый Джон Бакус (который автор Фортрана, куда уж императивнее, и который Backus Normal Form для описания синтаксиса) пишет где-то в окрестности 1980-го года:
My own opinion as to the effect of FORTRAN on later languages and the collective impact of such languages on programming generally is not a popular opinion. […] I now regard all conventional languages (e.g. , the FORTRANs, the ALGOLs, their successors and derivatives) as increasingly complex elaborations of the style of programming dictated by the von Neumann computer. These " von Neumann languages" create enormous, unnecessary intellectual roadblocks in thinking about programs and in creating the higher level combining forms required in a really powerful programming methodology. Von Neumann languages constantly keep our noses pressed in the dirt of address computation and the separate computation of single words, whereas we should be focusing on the form and content of the overall result we are trying to produce. We have come to regard the DO, FOR, WHILE statements and the like as powerful tools, whereas they are in fact weak palliatives that are necessary to make the primitive von Neuman style of programming viable at all.
By splitting programming into a world of expressions on the one hand and a world of statements on the other, von Neumann languages prevent the effective use of higher level combining forms; the lack of the latter makes the definitional capabilities of von Neumann languages so weak that most of their important features cannot be defined-starting with a small, elegant framework-but must be built into the framework of the language at the outset. The gargantuan size of recent von Neumann languages is eloquent proof of their inability to define new constructs: for no one would build in so many complex features if they could be defined and would fit into the existing framework later on.
The world of expressions has some elegant and useful mathematical properties whereas the world of statements is a disorderly one, without useful mathematical properties. Structured programming can be viewed as a modest effort to introduce a small amount of order into the chaotic world of statements. The work of Hoare ( 1 969), Dijkstra ( 1 976), and others to axiomatize the properties of the statement world can be viewed as a valiant and effective effort to be precise about those properties, ungainly as they may be.
This is not the place for me to elaborate any further my views about von Neumann languages. My point is this: while it was perhaps natural and inevitable that languages like FORTRAN and its successors should have developed out of the concept of the von Neumann computer as they did, the fact that such languages have dominated our thinking for twenty years is unfortunate. It is unfortunate because their long-standing familiarity will make it hard for us to understand and adopt new programming styles which one day will offer far greater intellectual and computational power.
Вот вы удивитесь - но если я кому-то буду рассказывать как я хожу в магазин - то это будет именно пошаговое перечисление действий: одеваюсь и оказываюсь одет, выхожу и оказываюсь на улице, двигаюсь в разных направлениях по улицам и перекресткам - оказываюсь у дверей магазина, набираю продукты, расплачиваюсь на кассе, и т.д. А вот сайд-эффектами этой деятельности оказываются a) уменьшившееся количество денег на счете и b) увеличившееся количество продуктов в холодильнике. И да, можно дискутировать о том до какой степени детализовать этот процесс (в программировании мы обычно детализируем до stdlib call или syscall). Без шуток - покажите мне человека который будет описывать поход в магазин как функциональное преобразование цифр в продукты... "А папа-мама не сумасшедши ли ?" (C) Мультик
Дальше - аргументы о том, что проще рассуждать и доказывать. Охотно верю - но знаете, идите на матфак работать с такими аргументами, что-ли ? Там любят таких... А я тут у мамы инженер. Мне не надо чтобы красиво доказано - мне надо чтобы работало, и легко читалось. Понятно что когда у меня есть streams, то ими надо пользоваться - например не писать свой алгоритм сортировки, а передать лямбду в библиотечный sort(). А когда один черт в коде клеится весь алгоритм, но пересыпается .apply() .with() .orElse() и прочей матерщиной (привет Kotlin!) - очень хочется дать по башке, и отправить копию новгородской грамоты чтобы лучше доходило...
Аргумент про то, чтобы компилировать ФП в мутабельный код - это из серии "можно, но зачем?!". То есть сначала мы сношаем мозг чтобы императивный процесс реального мира описать эквивалентом в мире чистых функций, а потом ждем чтобы компилятор обратно превратил это в императивный код с мутабельными объектами... Я даже теряюсь что предложить в таком случае... Например, когда в следующий раз захотите в туалет - не идите сразу у себя дома, а не поленитесь и дойдите пешком до центра. Потом такси вас обратно привезет - и только потом на толчок... После эксперимента задайте себе вопрос - стоило ли оно того, чтобы оказаться в том же месте по такой кружной траектории ?
Сравнение желтого с вонючим. Кмк любому очевидно, что писать императивный код человеком и трансформировать математически доказанный код в императивный самой машиной - это две большие разницы.
Вот вы удивитесь - но если я кому-то буду рассказывать как я хожу в магазин - то это будет именно пошаговое перечисление действий: одеваюсь и оказываюсь одет
Это пошаговое описание именно в декларативном стиле. Ровно в том же смысле, как и, возвращаясь к программированию, «получить множество авторов статей в хабе “программирование” можно через nubOrd . map author . filter (λpost → "программирование" `elem` hubs post)». Вы можете переубедить меня, описав процесс надевания куртки в императивном стиле, с мутабельными переменными, циклами, явными условиями, и так далее. Чем меньше абстракций уровня «просовываю руку в рукав», тем ближе к императивщине.
Практически все попытки выше ассемблера (включая макроассемблер), все эти функции, ООП, структурное программирование, прочее — это попытки разной степени успешности выйти из императивно-детализированной концепции в декларативную. Эти все вот ООП-шаблоны, билдеры там, фабрики, стратегии — это попытка эмулировать декларативщину в императивной среде.
Без шуток - покажите мне человека который будет описывать поход в магазин как функциональное преобразование цифр в продукты… “А папа-мама не сумасшедши ли ?”
Сами придумали тезис («ФП/декларативщина — это когда поход в магазин представляется преобразованием цифр в продукты»), сами его оспариваете, сами победили. Как это называется? Это называется strawman argument.
Олсо, это ведь работает во все стороны: сколько людей описывает поход в магазин как ООП? Сколько людей… инстанциирует класс бумажника? обращается к синглтону бумажников? использует абстрактный класс ИбуМажник? Чей метод вызывается при обмене денег на товар — бумажника, денег, товара, продавца, или абстрактного менеджера сделок? Вы когда ребёнку будете объяснять процесс похода в магазин, будете рассказывать про абстрактный менеджер сделок?
Можно ли сделать вывод в вашем мире, что ООП — это академический зашквар?
Охотно верю - но знаете, идите на матфак работать с такими аргументами, что-ли ? Там любят таких… А я тут у мамы инженер.
Я нигде не говорил о красоте доказательства, я говорил о простоте и возможности. Императивный код особо не доказывают, там херак-херак и в продакшен обычно. Это да, это в императивщине и нетипизированной функциональщине делать проще.
Но я как-то всё-таки хотел бы использовать слово «инженер» как похвалу, а не как уничижительный термин.
Мне не надо чтобы красиво доказано - мне надо чтобы работало, и легко читалось.
И это быстрее происходит в типизированной функциональщине.
А когда один черт в коде клеится весь алгоритм, но пересыпается .apply() .with() .orElse() и прочей матерщиной (привет Kotlin!) - очень хочется дать по башке, и отправить копию новгородской грамоты чтобы лучше доходило…
Товарищ взял не предназначенный для ФП язык и удивляется, что ФП выглядит в нём паршиво. Ну, я не знаю, попробуйте в ООП-стиле на ассемблере писать и потом ругайтесь, что ерунда выходит, клятый ООП.
Аргумент про то, чтобы компилировать ФП в мутабельный код - это из серии “можно, но зачем?!”.
Оптимизирующие компиляторы вы не приемлите вообще, я так понимаю, или это только применительно к ФП такое неприятие?
Ну как бы если любое движение от ассемблера считать движением в сторону ФП - то дальше спор смысла не имеет. Просто живите там у себя в мире математических абстракций, и даст бог в реальной жизни я вас не встречу.
Я видел развитие языков программирования через руки - начиная от 8-бит ассемблера, и дальше через 80x86 к C, C++, Java и так далее. Никакого движения в сторону FP там не было. Есть объекты реального мира. У них объективно есть состояние, которое меняется со временем по присущим им законам - или под внешним воздействием. В программе мы строим модель этого объекта или явления, отображая законы реального мира в их подобия в модельном пространстве. То-есть заменяем реальный вес на последовательность битов, его кодирующую (с фиксированной или плавающей точкой), положение объекта в пространстве на значения эйлеровых углов (или чего-то посложнее), и так далее. На низком уровне (ассемблере) - не существует способов выразить в языке структуру модели - какая ячейка или регистр за что отвечает - приходится держать в голове. Все дальнейшие нормальные языки: от прости господи, бейсика - до джавы - вводили разные изобратительные средства чтобы отразить объективно существующую структуру реального мира в программе с минимальными искажениями. То есть - группировать признаки относящиеся к одному модельному объекту в структуры, и сделать сложнее некорректные действия - типа вызвать метод "покрутить хвостом" к кухонной двери вместо кошки. Никто, щука, никогда не пытался представить модель мира как комбинацию бесчисленного количества чистых функций.
При том, что интеллектуально я готов принять множественность описания объектов реального мира (например, описание сигналов в частотной области спектрами, или во временной - графиками) - идея описать всё сущее чистыми функциями - проходит в моей канцелярии как забавный курьез. Ну и что, что можно ?! Вон метафора конечных автоматов тоже полна по Тьюирнгу - и любой процесс или явление можно описать таким образом. И есть чудаки которые до сих пор носятся с концепцией автоматного программирования. И аргументы у них точно такие же как у вас: это простой формализм, компилятору легче оптимизировать, и т.д. Проблема в том, что существует все-таки естественное описание объекта, которое является предпочтительным. Есть природные явления которые прямо-таки созданы для описания КА (или ФП). И там их надо применять. А там где это не естественно - там начинаются натягивания совы на глобус: давайте создадим скрытые фиктивные состояния, давайте добавим фиктивные входные и выходные сигналы, и т.д.
В итоге - повторю свой изначальный тезис: то что у вас появился в руках молоток - не делает остальные предметы гвоздями! Пожалуйста не надо хреначить направо и налево - да еще и похваляться этим...
Олсо, это ведь работает во все стороны: сколько людей описывает поход в магазин как ООП? Сколько людей… инстанциирует класс бумажника? обращается к синглтону бумажников? использует абстрактный класс ИбуМажник? Чей метод вызывается при обмене денег на товар — бумажника, денег, товара, продавца, или абстрактного менеджера сделок? Вы когда ребёнку будете объяснять процесс похода в магазин, будете рассказывать про абстрактный менеджер сделок?
Ха-ха-ха Execution in the Kingdom of Nouns - 20 лет прошло, а ничего не поменялось!
Символическое в ООП — более воображаемое и нарративное. Оно создаёт красивую историю: «объекты взаимодействуют, как люди/вещи в мире».
ФП требует абстрактного, математического мышления: данные отдельно, поведение (функции) отдельно, всё immutable. Это мощно, но когнитивно дороже для большинства.
Моя голова устает сильнее когда я программирую на ФП.
Расплодилось великое множество объектно-ориентированных языков: C++, Java, C#, JavaScript
На последнем очень хочется остановиться) можно подумать, что это ошибка, но нет, дальше в списке будет и TypeScript, так что нет, автор явно намеренно записал JavaScript в расплодившиеся объектно-ориентированные языки с начала 90-х.
А теперь следим за руками:
Официальная дата запуска JavaScript - 4 декабря 1995 года.
Классы в JavaScript появились в ECMAScript 2015 (ES6), выпущенном в июне 2015 года.
И даже в таком случае это тоже функции, объявляемые иначе, о чём прямо сказано на MDN -
Classes are in fact “special functions”, and just as you can define function expressions and function declarations, a class can be defined in two ways: a class expression or a class declaration
Ничего себе, расплодившийся с начала 90-х ООПшный язык, в котором до 2015 года даже не существовало ключевого слова class) Ну и смысл читать дальше после подобных заявлений? Человек же явно не понимает, о чём говорит
Ничего себе, расплодившийся с начала 90-х ООПшный язык, в котором до 2015 года даже не существовало ключевого слова class) Ну и смысл читать дальше после подобных заявлений? Человек же явно не понимает, о чём говорит
Самоиронично выходит, если знать, что JS это действительно объектно-ориентированный язык (и всегда им был), просто использующий прототипное наследование вместо классов
Терминология — мать порядка.
Начнем с того, что у термина «объектно-ориентированный язык» есть два практически прямо противоположных друг другу значения: аланокаевское и джеймсоногослинговое.
Вика предлагает определение, списанное под копирку у Лема из «Дневников Ййона Тихого»:
Объектно-ориентированный язык программирования (ОО-язык) — язык, построенный на принципах объектно-ориентированного программирования.
Инстансы тайп-классов в хаскеле очень сложно не назвать объектами по Гослингу.
И вообще, какой язык называть «объектно-ориентированным», а какой «функциональным» в 2026 году, когда в джаве уже не продохнуть от лямбд и ленивых стримов — это теософский спор, как про табы и пробелы.
Объектно-ориентированный язык программирования (ОО-язык) — язык, построенный на принципах объектно-ориентированного программирования.
Там если по ссылке пройти, то есть набор основных понятий ООП: абстракция данных, инкапсуляция, наследование, полиморфизм подтипов, класс, объект. Абстракции данных в хаскеле нет, сокрытие происходит на уровне модулей; инкапсуляции нет, методы и данные живут отдельно; наследования нет; полиморфизма подтипов нет, ну потому что подтипов нет.
Инстансы тайп-классов в хаскеле очень сложно не назвать объектами по Гослингу.
Раскройте мысль, пожалуйста
И вообще, какой язык называть «объектно-ориентированным», а какой «функциональным» в 2026 году, когда в джаве уже не продохнуть от лямбд и ленивых стримов — это теософский спор, как про табы и пробелы.
Язык может быть мультипарадигменным, противоречий тут нет. Если говорить конкретно про джаву, разве лямбды там это не просто синтаксический сахар для анонимного класса?
Уже давно нет (к вопросу про джаву)
набор основных понятий ООП: абстракция данных, инкапсуляция, наследование, полиморфизм подтипов, класс, объект
Давайте я еще раз попробую. «Основных понятий ООП» не существует в аксиоматике нашего мира. Алан Кай под ООП подразумевал одно, Гослинг — совершенно другое. Тут уместно вспомнить еще Матца, доведшего идею «everything is an object» до абсурда в руби.
Потом еще на горизонте появился Боб Мартин, со своими мухоморовыми галлюцинациями. Графомания этого инфоцыгана, в жизни не написавшего ни строки серьёзного кода, внезапно была воздвигнута на хоругви (потому что люди en masse туповаты, и без шор и узды в приниципе ни на что не способны). В результате у нас появилось определение ООП размером не менее трех печатных листов.
Инкапсуляция, полиморфизм и еще добрый десяток «основных понятий ООП» — прекрасно доступны во всех языках, от КОБОЛа до Хаскеля. Остальные — наследование, классы и прочие паттерны — никому нафиг не нужны (единственное разумное «наследование» вместо правильного решения — полиморфизма — реализовано в джаваскрипте).
То ООП, которое придумал Кай ко всему этому никакого отношения не имеет. То, которое предлагал Матц — тем более. Гослинг ну кхм.
В этом смысле, если выбросить в мусорную корзину бобмартиновщину и фаулерщину, — то инстансы тайп-классов в хаскеле — это хорошие, правильные объекты.
Сеньор Матюшкин, вы снова с нами?
Давайте я еще раз попробую. «Основных понятий ООП» не существует в аксиоматике нашего мира. Алан Кай под ООП подразумевал одно, Гослинг — совершенно другое. Тут уместно вспомнить еще Матца, доведшего идею «everything is an object» до абсурда в руби.
Я привожу википедийное определение, которое более-менее совпадает с общепринятым. Утверждений о его единственности или универсальности я не делаю.
Инкапсуляция, полиморфизм и еще добрый десяток «основных понятий ООП» — прекрасно доступны во всех языках, от КОБОЛа до Хаскеля.
ООП-шной инкапсуляции в хаскеле нет: методы и данные живут отдельно, сокрытие происходит на уровне модулей. Сами модули считать объектами нельзя, поскольку в хаскеле они, в отличие от окамла, не first-class. Полиморфизмы тоже другие: в ООП полиморфизм подтипов, у хаскеля полиморфизмы параметрический и ad-hoc.
На всякий случай проговорю, что я не спорю с тем, что в хаскеле можно писать в ОО-стиле. Это можно делать едва ли не в любом языке. Я спорю с тем, что хаскель это объектно-ориентированный ЯП.
В этом смысле, если выбросить в мусорную корзину бобмартиновщину и фаулерщину, — то инстансы тайп-классов в хаскеле — это хорошие, правильные объекты.
Не могли бы вы привести пару примеров с использованием инстансов как объектов? Простенький счётчик с методами increment/decrement, объекты круга и квадрата с общим методом area, и тому подобное
википедийное определение, которое более-менее совпадает с общепринятым
«Обще-» это кем конкретно?
ООП-шной инкапсуляции в хаскеле нет […]
Я прочитал «Введение в Хаскель за 7 часов» и знаю на базовом уровне, как там устроена инкапсуляция. А вот что такое «ООП-шная инкапсуляция», простите уж, я не в курсе. Это какая-то особенная инкапсуляция? С блекджеком и методистками?
модули считать объектами нельзя, поскольку в хаскеле они, в отличие от окамла, не first-class
Ого! Окамл-то тут при чем? В Окамле что же, настоящее православное общепринятое ООП?
Я спорю с тем, что хаскель это объектно-ориентированный ЯП.
Очень за вас рад. А кто именно такой тезис высказал? Может быть, нам этого человека тоже позвать?
объекты круга и квадрата с общим методом area
Полиморфизм, реализованный через наследование, — это вообще никаким боком не часть парадигмы, это детали имплементации. Вы просите, чтобы вам повторили слово в слово джавовый код на хаскеле, а лучше бы попытались понять, о чем вообще талдычит собеседник.
«Обще-» это кем конкретно?
https://ru.wiktionary.org/wiki/общепринятый
Удивительные провалы в памяти, когда “графомания была воздвигнута на хоругви” или “наследование, классы и прочие паттерны — никому нафиг не нужны” вы понимаете о ком речь, а когда “общепринятое определение”, то нет. Удобно, что сказать.
Я прочитал «Введение в Хаскель за 7 часов» и знаю на базовом уровне, как там устроена инкапсуляция. А вот что такое «ООП-шная инкапсуляция», простите уж, я не в курсе. Это какая-то особенная инкапсуляция? С блекджеком и методистками?
Вы под инкапсуляцией явно понимаете что-то отличное от того, что обычно имеют ввиду, потому что обычной, общепринятой инкапсуляции в хаскеле нету. Уточнение “ООП-шная” просто это подчёркивает.
Ого! Окамл-то тут при чем?
Обычное сравнение со схожим языком. Не понимаю почему вы так взъелись.
В Окамле что же, настоящее православное общепринятое ООП?
Учитывая, что OCaml раньше назывался “Objective Caml”, подозреваю, что да
Очень за вас рад. А кто именно такой тезис высказал? Может быть, нам этого человека тоже позвать?
Цитирую:
Инкапсуляция, полиморфизм и еще добрый десяток «основных понятий ООП» — прекрасно доступны во всех языках, от КОБОЛа до Хаскеля. Остальные — наследование, классы и прочие паттерны — никому нафиг не нужны
Как вы думаете, является ли язык, в котором доступны десяток основных понятий ООП объектно-ориентированным?
Полиморфизм, реализованный через наследование, — это вообще никаким боком не часть парадигмы, это детали имплементации.
Давайте я просто вашими фразами и отвечу:
Очень за вас рад. А кто именно реализовывать через наследование просил? Может быть, нам этого человека тоже позвать?
Вы просите, чтобы вам повторили слово в слово джавовый код на хаскеле, а лучше бы попытались понять, о чем вообще талдычит собеседник.
Я вот уже третий комментарий пытаюсь от собеседника добиться куска кода подтверждающего тезис “инстансы тайп-классов в хаскеле очень сложно не назвать объектами по Гослингу”, но ему, видимо, приятнее пофилософствовать про ООП, дядю Боба и т.д., вместо того, чтобы реально привести код.
А кто именно реализовывать через наследование просил?
Мне неизвестны иные способы «привести пару примеров […] объекты круга и квадрата с общим методом area».
Если имелось в виду не с «общим методом», а «каждый с методом area» — уверен, вы сами осилите написать такой пример на хаскеле.
третий комментарий пытаюсь
Второй, но я уже заметил, что вам до фени факты,
ему, видимо, приятнее пофилософствовать про ООП, дядю Боба и т.д., вместо того, чтобы реально привести код
Естественно. Это же хабр, а не профессиональный форум. Убеждать лично вас мне без надобности, писать тривиальный код, который бесплатный дипсик за десять секунд может нагенерировать — тем более.
Я развлекаюсь, глядя, как вы пыжитесь. Для этого и существует хабр, разве нет?
Почему-то автор рассматривает только написание кода, но не весь его жизненный цикл. ООП при всех его недостатках упрощает и удешевляет развитие программного продукта. А в Rust даже простое добавление нового поля в структуру может потребовать либо дублирования кода, либо массового переписывания трейтов.
Не понял, если просто добавить поле в структуру, то какое дублирование нужно? В метод конструктор в Self{} дописать поле?)
И какие вы трейты собрались переписывать, когда в трейтах нельзя получить доступ к полю структуры?) Если вы имели ввиде переписывать имплементации, то это ничем не больше чем переписывать теже имплантации в любом ООП языке.
И какие вы трейты собрались переписывать, когда в трейтах нельзя получить доступ к полю структуры
Типаж это не только его описание, но так же и его имплементация. Они неотделимы.
это ничем не больше чем переписывать теже имплантации в любом ООП языке
За тем исключением, что в ООП можно унаследовать классы, а не переписывать как их, так и код, их использующий. Если в каких то методах класса иногда может потребоваться новый параметр, связанный с новым свойством (полем структуры в Rust), то достаточно перегрузить только эти методы.
Особенно ярко это проблема на Rust проявляется при динамическом связывании.
Я полагаю, что в серьезных mission-critical системах нужно уходить от fp и stack-return-based моделей языков, в пользу полностью императивных процедурных(no-return) event-driven систем. Tl;dr Можно было бы сгенерить статейку почему wal-executor(fifo) круче стека(lifo+забыли залогать) для какого нибудь финтеха или миссии на марс :)
Состояние автора лучше всего описывается словом "корёжит".
А я вот понимаю восторги автора, потому что и сам в детстве был укушен джавой и подхватил ООП головного мозга. Чтобы вылечиться, несколько лет ушло, а когда я дошел до ФП, моей радости не было предела (и до сих пор я от щастья плачу)
Ну что вы накинулись на человека? Дайте ему порадоваться. Со временем, он сам поймет, что это не панацея, и где-то он превратно что-то понял
Но сейчас-то - вы посмотрите сколько радости! А примеры какие живые - старался...

Функциональный Rust. Глава 0: Зачем нужно ФП?