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

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

Время на прочтение15 мин
Количество просмотров35K
Всего голосов 47: ↑36 и ↓11+25
Комментарии195

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

Есть подозрение, что стоит дождаться, когда запустят таки новую версию Nemerle с поддержкой решарпера и упрощением написания макросов. Ждать осталось не долго, ибо Nitra уже почти доделана.
есть подозрение, что ждать еще минимум три года, судя по динамике разработки
Они меньше чем за год закрыли Milestone 2, а там основное «мясо» нитры. Учитывая, что грамматика немерла у них используется как тестовая и для бутстрапа, думаю, к лету мы увидим что-то работающее. На фоне этого, а так же инопланетной системе именования сущностей в F#, не вижу смысла инвестировать в переезд на оный. Особенно учитывая, что немерл может переваривать сишарпные исходники.
А что значит инопланетная система именования сущностей? Я не в смысле защищаю F#, а правда интересно.
Несовместимая с основными .NET-языками (C#/VB.NET) и, следовательно, с остальным фреймворком. Потому по отношению к .NET «инопланетная». По факту банальный нижнийКемельКейс где не надо.
Извините, а пример можно? Просто мне показалось, что там ничего такого инородного нет.
С переводом корнё reason неудобно вышло. Т. к. в части статьи он переводится обосновывать, в части как продумывать… Лишняя путаница, оригинал читается куда легче.
Я хотел перевести так, чтобы донести смысл идей, а там от контекста он сильно меняется.
Надеюсь, не сильно испортил оригинал :)
Надеюсь, что не сильно. Я в этот момент убежал читать оригинал)
прошу оценить на соответствие требованиям описание учебного языка программирования из http://habrahabr.ru/post/219419/
К слову, самый первый пример возможен и без всяких javascript.
Банальная функция на C++, принимающая параметр по ссылке и присваивающая туда 0. И никаких приведений не надо!
(именно поэтому уважаю гугловский линт, который призывает передавать изменяющиеся параметры по указателю, а не по ссылке)
А для этого тут вообще пункт номер 5.
Для пункта 5, в с++ есть Const Correctness.
Так я об этом и говорю :)
В С++ в соответствии с 5 пунктом тоже не стоит делать модификацию объектов в методе.
Передавать параметр по ссылке для изменения — верный способ взорвать мозг того, кто будет читать этот код потом.
Или свой мозг через несколько месяцев.
Странно, но часто кодинг гайдлайны пишутся чтобы скрыть пробелы в образовании некоторых из участников проекта — это лишь закапывание проблемы, а не её решение. Потом по этим гайдам пишется такой когд, от которого волосы встают дыбом.
К слову, самый первый пример возможен и без всяких javascript. Банальная функция на C++

Вот до такого
#define var auto

я бы не додумался
А что, хороший метод. Ещё, правда, надо this. заменять ,)
Описанные проблемы вызываются банальной невнимательностью. Все это решается линтерами и прочими анализаторами кода, а также юнит-тестами.
Угу, вот только статья о том, как читать код глазами.
Если все соглашения выполняются — то и читать глазками. А если в проекте бардак — то и на «защищённом от дурака» языке можно написать ужас-ужас.
Так вот пойнт в том, что приемы, описанные в статье, уменьшают количество способов написать ужас-ужас.
уменьшают количество способов написать ужас-ужас.

Вот и выросло поколение инженеров, которые не знают закона Мерфи.

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


одно из следствий которого кстати

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

Это по вашему какая вероятность?
По-моему — разная. А по-вашему?
Ну, я поясню. Есть два списка, из 5 и из 1000 элементов, для простоты — чисел от 1 и далее. Список шаффлится рандомно, потом по очереди читается до конца, копируя элементы в вывод. Потом снова. Понятно, что через хотя бы 1000 выведенных элементов мы обязательно встретим там 1 (для любого из списков). Вопрос, с какой вероятностью следующий прочитанный из вывода элемент — 1 для первого и для второго списков? Из какого списка надо читать, чтобы встречать 1 (неприятность) как можно реже?
Тем не менее, я предпочту иметь дело с (одной) пятой причиной неприятностей, нежели с четырьмя устраненными.
Ок, по мне так лучше четыре известных, чем одна неизвестная. Вдруг осознать что всё было написано не правильно, за пару дней до дедлайна — бесценное чувство. Особенно, когда ЯП просто ставит тебя перед фактом, что твоя идеально написанная программа не укладывается по паямяти или по быстродействию.
Во-первых, предполагается, что «четыре известных» увеличивают время разработки. Соответственно, когда вы их устраняете, у вас больше времени до дедлайна на то, чтобы справиться с пятой.

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

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

Парадокс ФЯ конечно в том, что выучив этот подход, можно потом свободно применять его и в с++ и в с# и на java. Но стоит ли переходить на функциональный язык, с его идеологическими ограничениями — большой вопрос.
Четыре известных могут увеличивать время разработки, только если разработчик упорно игнорирует эти проблемы.

Что регулярно и происходит.

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

Угу, в твиттере скала лет пять уже, эрланг в эриксоне двадцать лет — никто серьезно не занимался, ага.

Парадокс ФЯ конечно в том, что выучив этот подход, можно потом свободно применять его и в с++ и в с# и на java.

Можно, только неудобно. Мне после F# некоторые вещи в C# кажется совершенно избыточными.
А вы на самом деле используете в работе F#?
Я пока только изучаю его, очень нравится, даже не столько сам язык, сколько приёмы, которые можно сделать на нём.
Наверняка это общее свойство ФП языков — красивые приёмы, просто этот самый Скотт обладает хорошим качеством доходчиво донести идеи. И даже монады он лучше всех разъясняет имхо. Если соберусь, переведу цикл его статей про монады.
Но вот никак не могу понять, пригодится мне в работе F# или останется просто академическим опытом, который несомненно изменяет стиль программирования.
Что скажете?
А вы на самом деле используете в работе F#?

В работе — нет, в pet projects — да.
Что такое pet projects?bv
Маленькие проектики для себя.
А я в роли таких проектиков использую spoj. Или они более практичны?
Вы не поняли. Pet project — это всего лишь термин, означающий проект, который человек делает по любви. Pet — питомец.
Нет-нет, я понял :)
Я имел в виду, что я для развлечения пишу на spoj (тоже проекты ведь), а у вас они чем практичны, то есть, какая польза от них?
Что-то решает мои личные временные нужды, чем-то решаю задачи в образовательных курсах.
Поинт статьи, исходный, стартовый «строгий язык упрощает понимание». В дальнейшем в статье этот поинт перефразировали в «строгий язык даёт больше шансов на простое понимание».
Мой поинт — выполнение соглашение и возможность их реализации в языке без костылей и инородностей — упрощает понимание.
Самый строгий язык не защитит от бардака, если писать наплевав на все стандарты. Все эти общие примеры кода на той же Scala подаваемые под соусом «смотрите насколько всё понятно» я вообще плохо воспринимаю — нет для меня там ничего очевидного. Про каррирование — вообще молчу.
Но даже на c++ вполне можно писать читаемые программы.
А с некоторыми моментами статьи я в принципе не согласен: например возвращение «статус-объекта» в котором ещё надо разбираться — приехал ответ, или пусто… например в Java при корректном описании контракта метода с использованием JSR-305 удобней возвращать null. Там сам компилятор, насколько я помню, предупредит о подозрительных местах — не проверенных nullable, лишних проверках notnull контактов (правда всё-же скомпилирует).

В целом эта статья очень напоминает проблему checked/unchecked исключений в Java — вроде хорошая штука, эти checked исключения, но в некоторых местах (работа с БД, например) — лучше бы их не было… всё равно корректно отработать их невозможно… и в такой ситуации вполне корректно уронить приложение, пока дров не наломали.

PS жуть как интересно — кто такой добрый на минусы (в том числе и в карму)… впрочем те, кто минусовал, скорее всего, промолчат.
Если вы не осилили синтаксис языка или его подходы, это еще не значит, что он не читабелен, непонятен и непредсказуем. Это значит, что вы его не осилили, не больше.
Про perl программисты на нём пишушие тоже всегда заявляли, что он «легко читается»… но с ними, почему-то, мало кто соглашается.
Я не говорю, что скала плохой язык — возможно потратив существенное время на изучение+привыкание всё это будет действительно читаться легко, но это не базовое свойство языка. И фразы «смотри, как легко читается» в постах рекламирующих этот язык действительности не соответвуют. Всё-таки эти посты, обычно, адресованы массовой публике, язык не знающей.
А где в статье хоть одно упоминание того, что язык должен просто пониматься массовой публикой, лол.
Программист на этом языке должен понимать, что происходит в коде при беглом просмотре, вот что важно.
Суть в том, что на некоторых языках бардак писать проще, чем на других, а это непосредственно и приводит к разнице в процентном соотношении этого бардака.
Где же там пишется про чтение кода глазами? Перечислены конкретные недостатки, присущие популярным языкам программирования, и жалобы на то, что компилятор позволяет писать плохой код. Т.е. речь все-таки идет об автоматическом отлавливании ошибок.
«
«reasoning about the code» means that you can draw conclusions using only the information that you have right in front of you, rather than having to delve into other parts of the codebase. In other words, you can predict the behavior of some code just by looking at it. You may need to understand the interfaces to other components, but you shouldn't need to look inside them to see what they do.

[...]

But this post is not about writing safe code, it's about reasoning about the code. There is a difference.
»
In this post, I'll show you some of the issues that these design decisions cause, and suggest some ways to improve the language to avoid them.

и еще
Of course, there is a huge amount of advice out there on how to do just this: naming guidelines, formatting rules, design patterns, etc., etc.

But can your programming language by itself help your code to be more reasonable, more predictable? I think the answer is yes, but I'll let you judge for yourself.

И далее недостатки «плохих» языков и комментарии о том, как «хороший» язык должен реагировать на плохой код. Т.е. речь все-таки об отлавливании ошибок компилятором. Может быть автор и хотел сделать упор на читабельности кода — reasoning about the code в его терминах, но в итоге статья совершенно о другом получилась. К тому же, если какой-то код называется плохим, то неплохо было бы привести пример хорошего reasonable кода на «правильном» языке.
Ну какая же ошибка, например, в передаче параметра по ссылке или в мутабельности, или в использовании глобальных переменных? А ведь речь и о них в немалой степени тоже, потому что они делают код менее очевидным.
Ок, это не ошибка. Но автор хочет, чтобы язык этого не разрешал, т.е. чтобы это было ошибкой с точки зрения компилятора, разве нет?
Может такая конструкция плохая с фунциональной точки зрения, но в других парадигмах жизнеспособна.
TransactionManager.TransferMoney(account1, account2, sum);

Тут подразумевается изменение объектов account1, account2, и это хорошо понятно из названия метода. Просто не надо называть методы DoSomething.
А с какого на какой счёт переводится sum? ;)
ФП языки это как-то решают? Если отказаться от изменяемости, из записи
(account1, account2) = TransactionManager.TransferMoney(account1, account2, sum);

тоже непонятен этот момент.
Да я насчёт как надо называть методы.
Тогда может лучше TransferMoneyFrom (account, toAccount, sum)?
Лучше так: [ TransactionManager Transfer money:sum from:account1 to:account2 ]
Это ObjC, если что.
Да, и так как в статье сравниваются разные языки с разным пониманием того, что считать ошибкой, то, по-моему, утверждение о том, что речь в ней «об отлавливании ошибок» — некорректно. Речь там не об отлавливании, а о том, что считать ошибкой, а что — нет, в контексте того, упростит ли это reasoning about the code.
Ок, но статический анализ кода делает тоже самое. Можно считать это ограничением возможностей языка. А юнит-тесты так вообще упрощают этот самый reasoning. Это если вернуться к моему первому комментарию, который был успешно заминусован.
А юнит-тесты так вообще упрощают этот самый reasoning.

Каким же образом?

(если помнить определение, данное автором статьи)
Ну да, он говорит о «using only the information that you have right in front of you». Но зачем себя так ограничивать?
Потому что это наименее ресурсоемко.
О каких ресурсах идет речь?
Читающего.
Не совсем. При чтении хаскель-функции я уверен, что она ничего лишнего не меняет и никуда там не пишет. Причем она может, но в случае, если это отражено в ее типе. Это не ограничение, а расширение возможности языка — ибо есть возможность ограничивать или явно разрешать. Вы же не будете аналогичную возможность (через какие-либо аннотации) считать ограничением возможностей языка. Статический же анализатор надо еще запустить, а тут достаточно читать код, это проще.
Стат.анализатор нужен не для чтения кода инлайн, он нужен для гарантий качества кода. А уже гарантии качества кода дают возможность читать инлайн с большим пониманием процесса.
Статический же анализатор надо еще запустить

Компилятор/интерпретатор тоже нужно запускать. Все это легко автоматизируется и проблемы в этом нет.
Ну, всё-таки предположение «если код на языке компилируется» более общее, нежели «если он ещё и проходит проверку стат анализатором». Если я ставлю какой-то пакет, я могу быть уже уверен, что оно хотя бы должно компилироваться. Может, для других языков принято ещё и указывать, проверку какими статическими анализаторами они проходят, но я о таком не слышал.
Вот описанные им issues и влияют на чтение кода в первую очередь. И да, они следуют из дизайна языка.
В статье тоже описаны способы решения, просто другие.
В Haskell, например, благодаря чистоте, описанные примеры почти нереальны и без использования линтеров и тестов. Но главное не это, а то, что, читая чужой код, можно полагаться на эту самую чистоту, что делает анализ чужого кода более простым.
Эх, еще бы в язык, в котором бы можно было делать именованные синонимы ко встроенным типам либо как-то еще передавать контракты — цены бы ему не было.

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

1)
В объекте «книга» делаем поле isbn типа String, а то, что этот тип особенный — держим в уме (что, естственно, плохо). Там где ожидается передача ISBN — передаем строку (и не имеем гарантий, что туда не подсунут что-то левое).
class Book { String isbn; }


2) Делаем тип IsbnType, который внутри содержит поле stringValue типа String. В объекте «книга» делаем поле isbn типа IsbnType. Проблемы: мы не можем непосредственно к IsbnType применять строковые операции, а также, что хуже, такое решение обычно намного более тяжеловесное — ведь у нас IsbnType самостоятельный объект.
class IsbnType { String value; }
class Book { IsbnType isbn; }


3) Делаем тип IsbnType, который расширяет тип String. В объекте «книга» делаем поле isbn типа IsbnType. Основная проблема — во многих языках возможности расширять стандартные типы просто нет; а там где есть — это выключает для «расширеных» типов многие оптимизации. А ведь нам-то всего-лишь хотелось разделить произвольные строки и ISBN…
class IsbnType extends String;
class Book { IsbnType isbn; }

Посмотрите вот этот цикл.
Мне кажется, он как раз про то, о чём говорите.
Смотри, что покажу :-) http://dpaste.dzfl.pl/e07109504a1c
Проблему 3 Scala к сожалению не решает. За пол! года сам дважды сравнил String с Option[String].
А лучше всего под указанные критерии, IMHO, подходит Rust.
НЛО прилетело и опубликовало эту надпись здесь
Увлёкшись одной парадигмой человек просто не видит других решений.

> Объекты с идентичным содержанием должны быть равными по умолчанию.

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

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

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

> Объекты всегда должны быть инициализированными до корректного состояния. Невыполнение этого требования должно приводить к ошибке времени компиляции.

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

> После создания объекты и коллекции должны оставаться неизменными.

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

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

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

> Если вы возьмёте две коробки и положите в них одинаковый набор игрушек, то они не станут одной и той же коробкой с игрушками — это будет две разные коробки с одинаковым содержимым. Различать или нет такие объекты зависит от задачи.
К слову, в языках, где нет мутабельности, различать их не имеет смысла, так что там такой вопрос не стоит. Дальнейшие же рассуждения про то, что эффективнее сравнивать ссылки — в немутабельном случае тоже применимы.

> при возвращении мне нужно лишь проверить, что мне вернули объект эквивалентный тому, что я выдал
Так это разве противоречит тому, что написано?

> После заполнения каждой графы, анкета ещё не становится полностью валидной
Если говорить об этом конкретном примере, то незаполненная, даже пустая, анкета — вполне валидна, она — незаполнена, это ж разное. Невалидное — это null в полях «имя», «фамилия» при наличии ещё и методов вида «ФИО()», который к ним обращается.

1. Когда вы отгружаете 10 одинаковых коробок, вы не можете отгрузить одну и сказать, что отгрузил все. Чтобы решить эту проблему приходится каждому объекту приписывать глобально уникальный идентификатор. Собственно это и есть протечка в нашей чудесной абстракции.
2. Но если мы сравниваем ссылки, то можем и мутабельные структуры использовать. Речь о том, что иммутабельность не является в данном случае преимуществом, а не о том, что она хуже.

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

Валидность — понятие относительное. null как раз и показывает, что поле не заполнено. Другое дело, что ошибка должна возникать не при обращении к null, а при попытке записать его в non-nullable контейнер. И плох тот язык, где все типы по умолчанию nullable.
> Когда вы отгружаете 10 одинаковых коробок, вы не можете отгрузить одну и сказать, что отгрузил все.
Этот пример просто хорошо ложится на ООП, но он как-то надуман. Если у вас реально разные одинаковые коробки, то припишите им какой-то ИД разный, и вот они уже разные в чистом ФП, и никакой протечки тут нет, просто в мейнстримовых языках у каждого объекта и так есть ИД — ссылка на объект, но хорошое ли это умолчание?

> Противоречит, автор предлагает кидать ошибку всегда при сравнении разных типов.
Я, если честно, не очень понял, почему скутер и велосипед в том примере — обязательно разные типы. Ну пусть это будет АДТ (Algebraic data type), например.

> Валидность — понятие относительное. null как раз и показывает, что поле не заполнено.
Для этого есть всякие Optional (он же Maybe), если это действительно нужно. Вопрос в том же, хорошо ли умолчание, когда null есть почти у всего?
Протечка в том, что мы добавляем костыли (уникальный айдишник), чтобы различать объекты. Хорошее ли это умолчание — хороший вопрос. На мой взгляд — да. И примеры автора не убеждают в обратном.

Супертип вполне можно рассматривать как АДТ его субтипов, разве нет?

nullable — это и есть optional.
Какой же это костыль, когда именно так и делается везде, просто везде такой костыль намертво приварен, который ещё и не всегда работает (ибо есть всякие value-типы).

> Супертип вполне можно рассматривать как АДТ его субтипов, разве нет?
Ну, есть весомые различия. Субтипы можно добавлять где-то отдельно, а АДТ надо по месту. В данном же контексте — два объекта одного АДТ — один тип, их сравнение — вполне нормальная операция. Т.е. и автор доволен, что нельзя сравнивать объекты разных типов, и мы, что можно сравнить велосипед и скутер.

> nullable — это и есть optional
Ну, да, речь об умолчании же. Что лучше — optional по умолчанию или специальным образом оговоренный?
Он намертво приварен просто потому что физическая реальность такая. Две коробки — это две коробки, даже если на них написать одинаковые идентификаторы.

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

Я уже превентивно ответил на этот вопрос.
А причем тут физическая реальность? Давайте лучше возьмем математику, вроде как неплохой инструмент при описании реальности. Вот две единичные матрицы — они одна или две разные?
При том, что программирование — это не математика (формирование одних знаний из других знаний), а инженерия (применение знаний, для решения прикладных задач).

> они одна или две разные?

Вы сами ответили на свой вопрос:

> Вот две единичные матрицы

> программирование — это не математика
Ну а вот homotopy type theory — математика, и язык программирования заодно. Почему для инженерии язык на основе математики хуже?

> Вы сами ответили на свой вопрос
Нет, я спросил. Какой смысл в, например, различении одного числа 5 и «другого»? То, что их кладут в переменные, а потом про вопросе «есть два числа 5, они разные?» говорят, что таки разные, по-моему, пример телеги впереди лошади.
Теория не может быть языком. Давайте завязывать с этим словоблудием?

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

Объясню по проще, пусть у нас есть запись:
a = 5
b = 5.0 + 0 * i

a и b эквивалентны, так как ссылаются на одну и ту же математическую сущность, но не идентичны, так как имеют разные имена и даже разные типы.
Можно я тоже поучаствую в вашей беседе?
Идея эквивалентности по внутренней структуре состоит в том, что объекты с одинаковой структурой (читай, типом) и наполнением этой структуры считаются эквивалентными.
Можно найти аналогию в реляционных БД.
Если у вас две записи в одной таблице с идентичным наполнением полей, то вы не сможете их отличить.
Введение суррогатного первичного ключа (не отвечающего физическому смыслу сущности) не только не решает проблемы, но делает её ещё хуже — это верный путь к дублированию сущностей и нарушению целостности.
Аналогом суррогатного ПК в ОО является адрес (ссылка) объекта.
Если объект отличается от другого объекта только адресом, то как вы поймёте какой из программных объектов соответствует какому реальному объекту?
В вышеприведённой ситуации с коробками вводится идентификационный номер коробки, который должен быть набит на реальной коробке.
Иначе кладовщик не согласится работать как материально ответственное лицо ни под каким соусом.
Либо он находит в этом хороший способ к усушке-утруске-уворовке :)
1. Я их смогу отличать
select from persons where name = 'Вася Пупкин' limit 1 offset 0
select from persons where name = 'Вася Пупкин' limit 1 offset 1

2. У суррогатного ключа есть неоспоримый плюс — он никогда не меняется в отличие от естественного.

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

4. Вы когда картошку покупаете, вы на каждой картофелине штрих код видите?
1. В теории реляционных БД порядок записей не определён до выполнения оператора order by.
2. Это я даже не понял.
3. О чём и речь — и именно поэтому порядковый номер, если он не является свойством коробки, как объекта реального мира, не может быть использован для идентификации объекта. Потому что это чистый произвол.
4. Меня не интересует идентификация картофелины.
Хм, а я тогда тоже присоединюсь.
1. Это мешает различать записи?
2. Номер пенсионного… не помню как эта зелёная карточка называется — неизменен, в отличие
от ФИО, серии/номера паспорта,… По крайней мере он задуман как неизменнный.
3. Ну а equals в Java/C# выполняет семантически-зависимую роль. Т.е. если у нас есть нобходимость считать, что объект с одинаковыми свойствами идентичен другому (пример с числом где-то дальше в тредике) — то equals принудительно определяется по этим свойствам, в противном случае используется дефолтовое значение — равенство по конкретному инстансу (коробке).
1. В теории реляционных БД нет операторов limit и offset, кортеж может идентифицироваться только по значениям атрибутов.
2. Так в чём плюс неизменности номера пенсионного? Особенно если учесть, что он никак не привязан к объекту, кроме произвольной договорённости.
3. Вам ничто не мешает в таком случае завести два и более объектов, соответствующих одному объекту реального мира. Опять же в реляционных БД для отношения (моделирующего объекты реального мира) с искуственным первичным ключом обязательно должен быть определён альтернативный уникальный ключ. Иначе неминуемо нарушение целостности и коллизии.
1. Не гарантирован какой-то конкретный порядок, но он вполне себе определён реализацией и стабилен.

2. Никогда не задумывались, почему использование естественных первичных ключей является антипаттерном? http://dic.academic.ru/dic.nsf/ruwiki/915680

3. Как это не может, если может? Это первая коробка, а это вторая точно такая же.

4. О том и речь. Отношений эквивалентности много разных. А отношение идентичности одно и оно гарантирует эквивалентность по любому отношению.
1. Это откуда вы взяли, что стабилен? Даже по вашей ссылке из п.2 цитата
В теории реляционных баз данных таблица представляет собой изначально неупорядоченный набор записей. Единственный способ идентифицировать определённую запись в этой таблице — это указать набор значений одного или нескольких полей, который был бы уникальным для этой записи.

2. А вы задумывались, что я не говорил, что суррогатные ключи — это зло?
3. Что мешает назвать первую коробку второй, а вторую первой?
4. Какое это имеет отношение к картофелинам?
1. Я не вижу причин ему быть не стабильным. Разве что там применяется метод Монте-Карло для поиска :-)

2. А я и не этот тезис опровергал.

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

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

И тем не менее, в MS SQL он может меняться в зависимости от индексов, созданных на таблице, и конкретных выбираемых полей.
Это разумеется, но один и тот же запрос по одним и тем же данным даёт один и тот же результат, а не каждый раз в новом порядке.
… до тех пор, пока кто-то не поменял индекс на таблице. Ну и да, запросы «дай мне id» и «дай мне id и name», казалось бы, не должны возвращать данные в разном порядке… но возвращают. Могут возвращать.
Это может зависеть от оптимизационных алгоритмов СХД.
> Теория не может быть языком.
Почему? Вы с HoTT знакомы?

> a и b эквивалентны, так как ссылаются на одну и ту же математическую сущность, но не идентичны
Именно эквивалентны, а не равны? Что вы подразумеваете под эквивалентностью? Насколько я знаю, с позиции какой-либо теории типов, для эквивалентности типы должны совпадать, разве нет? А разные они потому, что это — переменные. Т.е. надо держать в уме эту разницу между значениями и переменными, это просто разные вещи.
Давайте лучше другую запись, и на Хаскеле:
a = 5
b = a

Здесь a и b — одно и то же. Т.е. внутри они вообще говоря могут ссылаться на разные участки памяти с одинаковым содержимым, но для рассуждений о поведении программы это не должно иметь и не имеет значения. У них один тип и одинаковое значение. Т.е. не надо держать в голове, что есть значения, а есть переменные, мы рассуждаем только о значениях. Т.е. нет никаких двух разных пятёрок, число 5 — это число 5.

do
  a <- newMVar 5
  b <- readMVar a >>= newMVar

А вот тут a и b имеют тип MVar Int, и вот они не идентичны (a == b ≡ False), но их значения — равны. Вот тут у нас и впрямь переменные, что и выражено в их типах.
Мне кажется, есть более веские причины выбирать язык, чем его предсказуемость, особенно предсказуемость в наборе искусственных примеров. Возьмём хотя бы ваш первый пример:

var x = 2;
DoSomething(x);

// Каково значение y? 
var y = x - 1;

Основная проблема здесь даже не в том, что `x` меняет тип, а в том, что `DoSomething()` меняет `x`. Это неочевидно и вносит непредсказуемость? Да, отчасти. И тем не менее, это абсолютно стандартный паттерн в C и всех библиотеках, направленных на высокую производительность. В качестве примера, попробуйте написать библиотеку для обработки изображений, в которой функции каждый раз возвращают новую картинку вместо модификации той, которую ей передали в качестве параметра.

Для смены типа аргумента функции я так и не смог придумать примера — слишком уж он надуманный — но в целом динамическая типизация позволяет безболезненно имплементировать в языке `eval()`, делать mock объекты без влезания в байткод, да и вообще гораздо приятней для динамического и метапрограммирования.

То же самое касается неявного преобразования типов: если даже вам не нравится, что `1` может использоваться вместо `true`, то наверняка вы не имеете ничего против того, что `int` можно передавать в функцию, у которой в сигнатуре стоит `float`. А некоторые языки (например, OCaml) и это запрещают. Но, опять же, в OCaml приоритет на надёжность, так, чтобы можно было программировать самолёты и атомные станции, а большинство языков программирования вполне себе позволяют такое вот послабление системы типов.

И так, в общем-то, можно пройтись по каждому из ваших примеров. Вы говорите, что `Equals()` должен возвращать `true` для одинаковых объектов. А что вообще значит одинаковые объекты? В Common Lisp, например, есть штук 5 функций сравнения двух значений, ранжирующихся от «одинаковы по сути» до «тот же объект». Это Common Lisp просто укуренный? Ну не знаю, а как по вашему, что должно возвращать выражение `1 == 1.0`? Суперстроготипизированный Haskell, кстати, возвращает `True`.

Про неизменяемые коллекции я уже говорил — если сделаете эффективную библиотеку обработки изображений в функциональном стиле, можете смело писать кандидатскую. Про неизменяемость объектов тоже спорно: попробуйте, например, сделать неизменяемым объект типа `Connection`, у которого по определению есть изменяемый флаг «состояние».

Пытаться избавиться от `null` — дело благородное, а вот от `exception` — не только дурное, но ещё и бесполезное: попробуйте, например, избавиться от исключения деления на ноль, возникающего из-за ограниченной точности представления чисел. Вот здесь (кстати, страничка про Haskell, откуда все алгебраические типы и защищённые вычисления пошли) описана ещё целая пачка исключений, которые невозможно предугадать или предупредить.

Если очень хочется поиграть в проверяемыми исключениями, которые должны обязательно описываться в сигнатуры, поиграйте с Java — там всё именно так и реализовано. Только вот никто этим не пользуется — либо всё оборачивается в `RuntimeException`, который не проверяется, либо бросается просто обобщённый `Exception`, просто чтобы не писать по 5-8 бесполезных слов после каждой функции.

Кстати, вот этот пример:
// обработать оба случая
if (customerOrError.IsCustomer)
    Console.WriteLine(customerOrError.Customer.Id);

if (customerOrError.IsError)
    Console.WriteLine(customerOrError.ErrorMessage);

совсем не кошерный по меркам функционального программирования. Вместо того, чтобы на каждом уровне проверять все возможные варианты, в ФП линии ML (т.е. Haskell, OCaml, Scala, F#) принято оборачивать значение в монаду и передавать её по всей цепочке вызовов — если монада хранит нормальное значение (кастомера), то к нему будут применяться преобразования, если ошибку — то ошибка будет просто продвигаться вперёд.

Кстати, в C# появился оператор `?`, который, по-сути, выполняет ту же роль:

int? length = customer?.Length;


вместо

int? first = (customers != null) ? customer.Length : null;


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

Например на GPU обработка изображений именно так и работает.
К тому же результат может иметь иной формат/размер.
Не уверен, что вы хотели сказать, но что на CUDA, что на OpenCL ядра работают именно через модификацию выходного буфера:

__kernel void sum(__global const float *a,  
                     __global const float *b,
                     __global float *c)                     // выходной буфер
{                                                
      int gid = get_global_id(0);   
      c[gid] = a[gid] + b[gid];        
}

Обработка изображений отличается тем, что объёмы данных достаточно большие, а вычислений производится очень и очень много. Если при каждом изменении выделять новый кусок памяти для выходного изображения, то одно только выделение/освобождение памяти будет занимать времени больше, чем вся остальная программа.
Своим примером вы подтвердили мои слова. Входные буферы константны и не пишутся, а выходной буфер пишется, но не читается. Т.е. это чисто функциональный подход.
Альязить буферы (c (RW) == b (RO)) в данном примере можно, но работоспособность может не гарантироваться.

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

Буфер `c` передаётся извне и модифицируется, ровно так же, как в примере с `DoSomething`. Если вы хотите пример с RW переменными внутри одной функции — весь API стандарта BLAS (и его реализаций cuBLAS и clBLAS) к вашим услугам. Например, умножение двух матриц в BLAS выглядит так (в версиях для GPU больше параметров, но суть та же):

gemm('N', 'T', alpha, A, B, beta, C)


что читается как «alpha * нетранспонированную(A) * транспонированную(B) + beta * С; записать результат обратно в C».

Однако это детали реализации абстракции «каждый раз новый буфер».

«Каждый раз новый буфер» означает, что вы можете сохранять полученное значение сколь угодно долго и оно никогда никем не будет модифицированно. Пул буферов предполагает ровно противоположное — буфер будет переиспользован и его значение будет изменено. Так что я опять не понял, где вы тут увидели немутабельность, хоть на абстрактном, хоть на конкретном уровне.
Скажем так, вы вполне можете программировать в терминах «создаём новый буфер как функцию от старого», а компилятор может заметить что старый не используется и оптимизировать до «внести изменения в старый буфер и выдать его за новый».
Я боюсь, в этом случае программист будет больше думать не о решении задачи, а о борьбе с компилятором, как бы случайно не написать так, чтобы компилятор не смог соптимизировать. А написать так очень легко:
buffer = putpixel(buffer, x, y, red);
if (IsBufferFilled(buffer)) { buffer = putpixel(buffer, x+1, y, red); }

Компилятор может упустить, что ф-ция IsBufferFilled переданное значение на положило куда-нибудь в поле класса (или программист по ошибке не вызвал оттуда ф-цию, для которой не доказано что значение больше не нужно).
Ну и ещё момент — компилятору придётся реализовывать два варианта putpixel — на случай, когда вызывающий код больше не использует buffer, и на случай, когда использует (во втором случае перед изменением надо сделать копию). Если же подобные ф-ции вложены, кол-во оптимальных вариантов, учитывающих, когда надо копировать и когда не надо, будет расти экспоненциально (от глубины вложенности).
В функциональных языках вы не можете просто так «положить куда-то». Так что проблемы выяснить какие значения уже не используются нет.

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

В идеальном мире — да, а в реальном много чего может быть. Например, запуск нового потока для контрольной записи буфера в файл, с передачей buffer ему без каких-либо предосторожностей. Какой-то программист подумает: «Все типы неизменяемые, можно не заморачиваться с копированием». А другой программист подумает: «Все функции чистые, если объект не используется дальше, можно его не копировать, а изменить»

>> универсально скомпилировать

Задать невидимый пользователю параметр — копировать или изменять буфер, потому что это знает только вызывающий код. Да, это выход
В чистых языках (а, я так понимаю, речь о них, раз обсуждается «каждый раз создаём новый») IsBufferFilled не может никуда положить, ибо нет мутабельных переменных. Вообще говоря, они есть, конечно, но в таком случае тип у Buffer будет другой (отражающий тот факт, что он мутабелен), ну и тогда не надо будет прикрываться ширмой «каждый раз новый буфер».

В Clean для такого есть en.wikipedia.org/wiki/Uniqueness_type
В Хаскеле — ST и State, например.
Т.е. функции вида «взять буфер, вернуть новый» соединяем в одну большую такую функцию, а потом ей кормим буфер. Изнутри ему некуда «убежать», потому результат — один «новый» буфер. ST — если хотим таки работать с мутабельными переменными и массивами, но с гарантией, что итоговый результат — чистый. Можно комбинировать, разумеется, под капотом ST, чуть повыше — State.

Насколько это на самом деле оптимизируется, я врать не буду, не проверял.
Трюк с unique мне понравился. Однако, проверка, что объект повторно не использован, происходит в compile-time или run-time?

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

buffer = putpixel(buffer, x, y, red);
(flag, buffer) = IsBufferFilled(buffer);
if (flag) { buffer = putpixel(buffer, x+1, y, red); }


чтобы компилятор получил новый unique после вызова IsBufferFilled, а не передавал использованный unique-объект в следующий putpixel.
В compile-time, это особенность системы типов.
Интересно, если unique-объект включен в какой-то другой объект как поле (как элемент списка, если терминах lisp), весь родительский объект/список тоже становится unique?
Иначе я не представляю, как это можно проконтроллировать без полной верификации всех путей выполнения (что в принципе невычислимая задача)
Насчёт этого, к сожалению, не в курсе
Давайте ближе к телу. Возьмём функцию `filter2D` из OpenCV, которая, грубо говоря, имеет сигнатуру:

void filter2D(Mat src, Mat dst);

Где `src` — исходная картинка, а `dst` — выходная матрица, в которую записывается результат (destination). Возможно ли переписать эту функцию в функциональном стиле, чтобы она не модифицировала `dst` и при этом не требовала выделения новой памяти? При этом не забываем, что функция библиотечная, так что вызываемые и вызывающий код компилируются в разное время и разными компиляторами.
Содержание dst на входе имеет значение при выполнении filter2D?
В данном случае если и имеет, то не используется. Ниже в примере с `gemm` аналогичный параметр используется и как входная, так и выходная переменная.
Но тогда что мешает, как уже было неоднократно сказано, использовать заранее выделенные области внутри функции и возвращать их?
О каких заранее выделенных областях внутри функции идёт речь? О преаллоцированной статической переменной (в смысле C++) внутри фукнции? Автор функции не может знать, матрицу какого размера ему передадут. О «бесконечном пуле буферов»? Это то же самое, что просто доступ в RAM, и в RAM-е использованную память (свободные буферы) нужно когда-то освобождать.

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

Но здесь «выделение памяти» в этих двух пунктах может иметь разное значение.
Даже в самом кошерном ОО С++ если нужна сверхскорость при работе с объектами, начинают переопределять new() с использованием нестандартных аллокаторов.
Кто мешает написать функцию ФП с «выделением» памяти, которое на самом деле будет просто возвращать ссылку на область памяти из предвыделенного пула?
Вы опять всё сводите к какому-то абстрактному пулу предвыделенных буферов. Ну пусть будет у нас пул из 1000 буферов, пусть даже каждый буфер уже будет нужного размера — что угодно. Мы вызываем нашу функцию, результат записываем в буфер #1, затем в буфер #2, затем в буфер #3 и т.д. В конце концов буферы у нас заканчиваются, что будем делать в этом случае?
Буферы так же будут освобождаться, когда отработают.
Можно вручную, а можно предусмотреть и автоматическую сборку мусора (которая, можно сказать, из FP родом).
Просто вы говорите, что выделение памяти на каждый вызов функции затормаживает обработку, поэтому надо выделять заранее буфер и всё время его передавать в функцию, чтобы сэкономить на операции выделения памяти.
А я говорю, что скорость выделения может быть очень высока и этим можно пренебречь.
Можно вручную, а можно предусмотреть и автоматическую сборку мусора (которая, можно сказать, из FP родом).

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

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

Пример на Julia:

# импортируем BLAS
julia> using Base.LinAlg.BLAS

# создаём два исходных массива A и B плюс один буфер C
julia> A = rand(10000, 100); B = rand(100, 10000); C = zeros(10000, 10000);

# перемножение матриц с выделением памяти
julia> @time for i=1:10 C = gemm('N', 'N', 1.0, A, B) end
  8.822950 seconds (13.02 k allocations: 7.451 GB, 6.15% gc time)

# перемножение матриц с использованием преаллоцированного буфера
julia> @time for i=1:10 gemm!('N', 'N', 1.0, A, B, 0.0, C) end
  4.607092 seconds

Не знаю как вы, но я бы не стал пренебрегать оптимизацией алгоритма на 92%.
«Выделение» памяти может быть без выделения.
Ну вот, например, как здесь C++: Custom memory allocation
(И это я ещё не вспоминаю о том, что автор статьи явно указал, что пишет не о проблемах производительности :))
Я могу подробно описать, почему ни один из описанных аллокаторов не решает задачу и почему «выделение» — это всегда выделение. Но как бы подробно я это ни описал, найдётся деталь или ветвь размышления, которую я не покрыл, и мне опять и опять придётся писать большие тексты. Поэтому давайте сделаем проще: вместо того, чтобы я доказывал, что это невозможно, вы докажите, что это сделать можно. Т.е. с помощью кастомных аллокаторов (или чего угодно ещё) трансформируйте функцию

void filter2D(Mat src, Mat dst)


в функцию

Mat filter2D(Mat src)


без необходимости динамически выделять память.
Mat filter2D (Mat src)
{
Mat dst = MemoryManager.GetMat(sizeof(src));
dst = map (src, filter2Dfunc);
return dst;
}
Я начинаю подозревать, что вы меня троллите. Мы 16-й комментарий обсуждаем стратегии выделения памяти, а вы всю суть вопроса снова сводите к какому-то абстрактному классу. А что должно находиться внутри `MemoryManager.GetMat`?
Да зачем мне вас троллить?
Просто не знаю как уже донести эту мысль (которая здесь и другими участниками высказывалась), что для производительности можно выделять память из пула.
Уж не напоминаю, что в самой статье тема производительности тоже затрагивалась, причём с однозначным ответом на этот вопрос.
Внутри MemoryManager.GetMat может быть, например, кольцевой буфер.
Или дерево с ссылками на свободные chunkи памяти.
Или что угодно, что может оперировать с _предвыделенными_ областями памяти.
Единственная задача — предоставить абстракцию, чтобы в этот логический блок памяти записывались данные только для матрицы dst, и не записывались никакие другие.
Принципиальная реализация не важна.
Вы лучше скажите почему «выделение» — это всегда выделение.
Может я что-то неправильно понимаю в ваших словах.
И ещё было бы интересно понять почему ни один из описанных аллокаторов не решает задачу.
Там в конце даже тесты производительности, вполне достойные.
Уж не напоминаю, что в самой статье тема производительности тоже затрагивалась, причём с однозначным ответом на этот вопрос.

Это ветка ответа на мой первый комментарий началась с ответа beeruser относительно производительности, про тругие юз кейсы для модификации переменных и смены типа я ответил дальше (параграф про `eval()`).

Внутри MemoryManager.GetMat может быть, например, кольцевой буфер.

Который при достижении лимита либо начнёт перезаписывать старые значения, либо вообще откажется выделять новые объекты. Шикарный вариант.

Или дерево с ссылками на свободные chunkи памяти.

И повторить урезанную функциональность malloc-а. Действительно, давайте изобретём велосипед.

Или что угодно, что может оперировать с _предвыделенными_ областями памяти.

malloc и garbage collector могут работать с предвыделенными областями памяти. Иногда могут запрашивать у операционной системы дополнительную память, но большинство аллокаций происходит в предвыделенной области. Внезапно.

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

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

Принципиальная реализация не важна.

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

Вы лучше скажите почему «выделение» — это всегда выделение.

  • Быстрое выделение памяти
  • Быстрое освобождение памяти
  • Общий случай

Выберете 2 из 3. Если вы хотите быстро выделять память (например, просто увеличить указатель в куче в системах со сборщиком мусора), то придётся потретить время на её освобождение (сборка мусора). Если вы хотите быстро освобождать (пометить кусок памяти как свободный), то придётся потратить время на поиск подходящего куска в следущий раз (как это делает malloc). В некоторых специальных случаях можно получить и (1) и (2) (например, стек и выделяется, и уничтожается простым изменением указателя), но это налагает серьёзные ограничения (стек может либо расти, либо раскручиваться, сохранение и разрушение произвольных фреймов невозможно). Как я уже говорил, найдёте способ — пишите кандидатскую.

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

Начнём с того, что каждый из этих аллокаторов требует предварительного выделения некоторого объёма памяти, какого именно ни создатели библиотеки, ни чаще всего даже пользователи библиотеки не знают. Вы можете выделить 1Gb и использовать 256К, а можете выделить 100Мб и через 3 минуты свалиться с нехваткой памяти.

Кроме того:

  • Linear Allocator вообще не умеет освобождать буферы по одному, только очищать всю выделенную память сразу. Мне вообще сложно придумать, где он применим
  • Stack Allocator, как и обычный стек, не позволяет произвольного выделения памяти. Т.е. буфер изображения #3 мы сможем уничтожить не раньше, чем уничтожим буфер изображения #4
  • FreeList Allocator — по сути, тот же malloc, но без системных вызовов и в маленьком куске памяти. Результаты абсолютно бессмысленны: во-первых, используется метод first-fit вместе оптимального best-fit, во-вторых, сначала делается N аллокаций (константная операция, учитывая first-fit), а затем N деаллокаций (опять же, константная операция). Если попробовать выделять и освобождать память вперемешку и на большем объёме памяти, результаты будут как раз как у malloc
  • Pool Allocator работает только с буферами фиксированного размера. Так в документации и напишем: накладываем фильтры на изображение, но только если оно размера 96x96.


Так что бесплатного сыра не бывает, пора бы это уже осознать.
По поводу выделения гигабайта или 100 Мб — как поступают в реализации std::vector? Или там всегда известно какой длины массив туда собираются сохранить?
То же и про кольцевой буфер.
Конкретные реализации дают выигрыш в конкретных случаях.
Про бесплатный сыр никто не говорил и не ожидал.
Менторский тон заключительной фразы не способствует приятному обсуждению.
По поводу выделения гигабайта или 100 Мб — как поступают в реализации std::vector?

Я не пишу на C++ и не знаю, как там это реализовано, но в Java аналогичная структура — ArrayList — хранит массив с запасом, а при достижении границы переаллоцирует весь массив. К менеджерам памяти (или, если хотите, мендежеру буферов) такая схема неприменима, хотя бы потому что любое удаление элемента (т.е. освобождение памяти) требовало бы полного переписывания массива/преаллоцированного куска памяти.

То же и про кольцевой буфер.

Действительно, то же и про кольцевой буфер.

Про бесплатный сыр никто не говорил и не ожидал.

Нет, вы говорили про сыр, стоимостью которого можно пренебречь. Когда я показал, что стоимость велика, вы начали доказывать, что где-то есть сыр дешевле.
Ок-ок-ок, вы победили.
Ну «можно пренебречь» не значит, что он бесплатный, а какой стоимостью сыра можно пренебречь — зависит от задачи. Можно ж и обратно развернуть — мутабельность и прочие плюшки тоже не бесплатны — увеличивают «стоимость» разработки.
мутабельность и прочие плюшки тоже не бесплатны — увеличивают «стоимость» разработки.

Ну это тоже спорное утверждение: сравните, например, Haskell и Python — те, кто пробовал писать продакшн системы на обоих, говорят, что на Питоне таки быстрее ;)
Продакшн я не писал, но активно писал (и пишу) SublimeHaskell — плагин для Sublime, ну и вот на Хаскеле — быстрее намного и проще :)
Добавил один конструктор — компилятор укажет все места, где надо что-то поменять. Поменял сигнатуру — так же.
В Питоне с этим дело обстоит несколько иначе. Обкладывать даже такие вещи тестами? Ну так это увеличивает время обнаружения ошибки на порядок и повышает время на их исправление.

Так что такое сравнение в лоб (вида «писал что-то похожее на одном и другом») — это всего лишь один элемент, необходимый для оценки, вывода по нему делать нельзя, ибо в наличии разные инструменты; влияют разные стили написания и разный background у пишущих.
Эта ветка началась, вроде как, вот с этого:

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

А это далеко не общий случай, это очень частный случай. Возьмем, например, обработку списков. В цепочке обработок списка вида take n . filter f . map g и типа [a] -> [a] никаких списков не создаётся, к слову, и более того, даже обход будет — один, а не три (в ленивом языке), и обходить будет не весь список (в конце ж у нас take n); причём map g, например, может быть оформлено отдельной функцией, которая якобы принимает список и возвращает «новый» список.
Для того же самого в императивном языке придётся придумывать итераторы или что-то типа того.

При этом в чистом языке можно писать и грязно (в том числе с мутабельными буферами), если надо, и даже внутри чистой можно выделять куски, которые где-то под капотом пишут грязно, но снаружи — чистые. А вот как чистоту добавить в изначально грязный язык?
Спасибо большое за этот пост!
Вы отлично написали, просто не в бровь, а в глаз.
Только сложно убедить того, кто не пользовался этим.
Об этом писал Грэм в Beating the Averages про гипотетического программиста на Blub.
Только сложно убедить того, кто не пользовался этим.

Если вы это про меня, то на функциональных языках я пишу года этак с 2008, причём года 3 писал продакшен системы на Лиспе, про который говорит в своей статье Грэм. Только нужно понимать, что любая фича любого языка появилась не просто так, и функциональные языки дают прирост скорости разработки только в своей небольшой области. Так же, как и императивные языки дают серьёзный выигрыш в своей области. А в большинстве задач вообще всё равно, какую парадигму использовать.
Нет, не про вас.
Я уже согласился с вами, и давайте ещё раз соглашусь — каждый инструмент хорош для своего дела.
А, тогда простите, просто тот комментарий был ответом на мой.
Разумеется, и каждая команда хороша для своего проекта, поэтому лучше набирать команду ежемесячно. Да вот беда, уж очень трудно сразу узнать хороший ли специалист. Вот есть команда со своими умонастроениями, другие бы за другие деньги на другом проекте сделали бы во сто крат лучше.
Только вот наглядный код на Хаскеле тормозит, а оптимальный — ребус.
http://geektimes.ru/post/261144/#comment_8759612
Это fibs-то — ребус?
Да, и это уже на элементарном приветмире.
Это не ребус для тех, кто сколь-нибудь программировал на Хаскеле.
Кому хочется орудовать состоянием в более сложных примерах, ну можно взять и State.

Я там выше упоминал take n . filter f . map g, как это переписать на императивный?
Вы не поверите: [1,2,3,4,5,6,7,8,9].map!q{ a * 2 }.filter!q{ a % 4 }.take( 5 )
Во-первых, теперь мой черёд говорить «ребус» :)
А во-вторых, там же наверняка не просто списки, я угадал?
Можно из этого всего выдрать маленький кусок в виде отдельной функции:
foo = map g
Как это будет выглядеть?
auto g( Item )( Item a ) { return a * 2; } auto foo( Item )( Item a ) { return a.map!g; }
Я с D знаком весьма поверхностно, потому у меня есть ещё вопросы.
Если я верно помню, то! — это применение шаблона.
Значит ли, что g обязана быть известна в момент компиляции?
В коде, который я написал не вам, чуть ниже, но на основе моего:
foo = take n
bar = filter f
baz = map g
quux = foo . bar . baz

Проход по списку будет один. Верно ли будет это и в вашем примере, если аналогично дописать остальные функции, а потом их скомпоновать? Верно ли для обычного списка, или для только для итераторов?
Да, забыл сказать, что map, filter и take возвращают те же ленивые итераторы, что и в хаскеле. http://dlang.org/phobos/std_algorithm_iteration.html

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

> Да, забыл сказать, что map, filter и take возвращают те же ленивые итераторы, что и в хаскеле.
Вот только в хаскеле эти ленивые итераторы определяются так:
data List a = Null | Cons a (List a)

И так для любого своего типа данных.

> Да, g должна быть выводима во время компиляции
А так — нечестно :)

Я веду к тому, что необходимость «ленивых итераторов» очевидна, и везде так или иначе реализуется. Но в Хаскеле не нужно делать это как-то особенно, там оно делается по умолчанию. А чистота даёт больший просто для высокоуровневых оптимизаций.
Хотя, конечно, стоит отметить, что иногда излишняя ленивость становится источником проблем, когда неясно, где и в какой момент что-то вычисляется, а это может порой неслабо повлиять на поведение программы. Вот в этих случаях анализировать сложнее, но не так, чтобы очень.
> Вот только в хаскеле эти ленивые итераторы определяются так:

Я не силён в Хаскеле, что тут происходит?

> А так — нечестно :)

Почему?

> Но в Хаскеле не нужно делать это как-то особенно, там оно делается по умолчанию.

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

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

Почему?

Потому что в моём коде это не так.

Понятней не стало :-) Это бесконечный список из Null-ов или что?

Значит его и фиг оптимизируешь толком
Понятней не стало :-) Это бесконечный список из Null-ов или что?

Это просто список, т.е. либо пустой (Null), либо голова + хвост.

Значит его и фиг оптимизируешь толком

Ну как-то умудряются же, и чистота этому только способствует, потому что, например, map f . map g можно преобразовать в map (f . g) вне зависимости от того, что там за f и g.
Список чего? Или это абстрактный тип такой?

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

Полиморфный

Преобразовать-то можно и в императивном языке

Разумеется, нельзя, потому что в императивном языке эти две конструкции просто-напросто неэквивалентны.
f потенциально может выводить на экран, g — тоже, а порядок менять негоже.
Может хватит говорить загадками? Что конкретно содержится в этом полиморфном списке?

Почему это негоже? map возвращает ленивый итератор, а не результат, так что не гарантирует когда функции будут вызваны и будут ли вызваны вообще.
Может хватит говорить загадками?

Куда уж конкретнее? Параметрический полиморфизм

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

Я специально написал «порядок». Если же map может применять функцию и в обратном порядке, то это уже не оптимизация.

d = {'x':0}
def foo(x):
	d['x'] = d['x'] + 1
	x = x + d['x']
	print(x)
	return x

def foofoo(x):
	foo(x)
	return foo(x)

def test1():
	d['x'] = 0
	return list(map(foo, [0,0,0]))
def test2():
	d['x'] = 0
	return list(map(foo, map(foo, [0,0,0])))
def test3():
	d['x'] = 0
	return list(map(foofoo, [0, 0, 0]))


Что выведет test1()? Что выведет test2()? А что выведет test3()?
Я знаю, что такое полиморфизм, мне не понятен конкретный ребус, который вы привели.

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

Это не совсем так. Ленивый, он же нормальный порядок вычислений определён абсолютно строго, просто иначе, чем аппликативный. Например, при аппликативном порядке выражение `f(g(x))` будет вычисляться строго слева направо (сначала `x`, потом `g()`, потом `f()`), а при нормальном — сначала слева направо (вызовы функций), а потом слева направо («закрытие» функций). Но этот порядок всегда строго определён и вполне поддаётся анализу.
Разумеется он строго определён и зависит от реализации f, g, x
У вас ужасный, ужасный, ужасный стиль программирования на Python. Как я уже говорил, если кто-то применяет функции с побочными эффектами в функциях типа `map`, то ему нужно отрывать руки с корнем, вне зависимости от языка.
У вас ужасный, ужасный, ужасный стиль программирования на Python

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

Как я уже говорил, если кто-то применяет функции с побочными эффектами в функциях типа `map`, то ему нужно отрывать руки с корнем, вне зависимости от языка.

Ну вот print — вроде грязная функция? А В Хаскеле можно передавать :)
sequence_ $ map print [1..3] -- 1 2 3
sequence_ $ reverse $ map print [1..3] -- 3 2 1
Ну вот print — вроде грязная функция? А В Хаскеле можно передавать :)

Кто ж виноват, что в Haskell нет `foreach` :D

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

Хм, вы имеете ввиду оптимизацию на уровне компилятора? Тогда я не очень понимаю её суть — насколько я понимаю, композитная функция `(f. g)` всё равно транслируется в машинный код как последовательное применение двух функций к каждому элементу. Или здесь можно ещё что-то соптимизировать?

Единственный раз, когда я видел полезность композиции функций для производительности, был в PySpark, где выражение (`rdd`, грубо говоря, — это большой-большой ленивый список, загружаемый с диска):

rdd.map(g).map(f)


транслировалось в

rdd.map(compose(f, g))


Но там это сделано из-за того, что каждый вызов к `.map` означает передачу данных между процессами Java и Python через диск, так что композиция функций уменьшает количество IO операций ровно вдвое. Это оптимизация на уровне PySpark, которая может произойти, а может по определённым причинам и не произойти. Но, опять же, от программиста ожидается, что он не будет стрелять себе в ногу и передавать в `.map` грязные функции, от порядка вызова которых может измениться результат.

Обратите внимание, что в этом случае ни Java (Scala), ни Python чистыми языками не являются.
Кто ж виноват, что в Haskell нет `foreach` :D

Отчего ж, есть и mapM и forM, но можно и так тоже.
Есть даже такое :)
ghci> mapMOf_ (each . _Right . each . _head) putChar [Right ["foo", "baz", "baz"], Left 10, Right ["x", "y", "zoo"]]
fbbxyz

Взять каждый (each) элемент, затем пойти внутрь конструктора Right (_Right) (при этом откинув те, что Left), затем взять каждый элемент (each), и затем первую букву (_head)
И для них выполнить putChar
Ну да ладно, это так, к слову.

Хм, вы имеете ввиду оптимизацию на уровне компилятора? Тогда я не очень понимаю её суть — насколько я понимаю, композитная функция `(f. g)` всё равно транслируется в машинный код как последовательное применение двух функций к каждому элементу. Или здесь можно ещё что-то соптимизировать?

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

Вот, например, из документации:
So, for example, the following should generate no intermediate lists:
array (1,10) [(i,i*i) | i <- map (+ 1) [0..9]]



К сожалению, глубоко тему не рыл, но вот, например: www.randomhacks.net/2007/02/10/map-fusion-and-haskell-performance
Стоит также отметить, что если в случае итераторов всё понятно, они специально так заделаны, то с двумя подряд foreach так уже сделать нельзя:
for x in xs:
  f(x)
for x in xs:
  g(x)

# Совсем не то же самое, что:
# for x in xs:
#   f(x)
#   g(x)

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

Написал несколько сумбурно, но, надеюсь, донес мысль :)
Это типичный функциональный список. Он же односвязный список. Представляет собой всегда голову (то есть первый элемент и хвост — такой же список только без первого элемента). По идее, он генерик, какого реально типа определяется type infering по использованию.
class FunctionalList<T>
{
      T Head{get;set;}
      FunctionalList<T> Tail{get;set;}
}


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

class List
class Null: List
class Node: List

Ибо ваше на хаскель переводится так:

data List a = List (Maybe a) (Maybe (List a))
Что совсем не то же самое
Там либо пусто, либо голова + хвост, так что фраза «представляет собой всегда голову» неточна.
Это с точки зрения функционального языка и паттернматчинга принципиально.
Я там выше упоминал take n. filter f. map g, как это переписать на императивный?

А вас какая часть этого выражения интересует? Функции работы со списками, каррирование или композиция функций? В доброй половине императивных языков, с которыми я имею дело, все эти вещи либо есть из коробки, либо добавляются в несколько строк кода. Например, если нужно просто прогнать список `a` через эти 3 функции, то идеоматичный код на Python будет выглядеть так:

[g(x) for x in a if f(x)][:n]


Если вам больше по душе вид с функциями, то можно ещё и так:

from itertools import *
islice(ifilter(f, imap(g, a)), n)


Хотя, конечно, более кашерный подход с точки зрения Python будет вынлядеть так:

from itertools import *
r = imap(g, a)
r = ifilter(f, r)
r = islice(r, n)

Префикс `i`, кстати, говорит о том, что мы работаем с итераторами, т.е. теми же самыми ленивыми списками.

> А вас какая часть этого выражения интересует? Функции работы со списками, каррирование или композиция функций?
Композиции и возможность записывать куски композиции отдельно, т.е. типа такого:
foo = take n
bar = filter f
baz = map g
quux = foo . bar . baz


> т.е. теми же самыми ленивыми списками
Да. При этом в чистом языке можно подряд идущие filter f . map g сворачивать в использование одной функции (Rewrite rules), потому что ни f, ни g не производят побочных эффектов.
А что если у нас не списки, а деревья? Как сделать всё то же, но для своего типа данных?
import functools
from functools import *
from itertools import * 

def itake(n, coll): return islice(coll, n)
 
foo = partial(itake, 3)
bar = partial(ifilter, lambda x: x > 2)
baz = partial(imap, lambda x: x + 1)

def compose(*functions):
    return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, lambda x: x)

quux = compose(foo, bar, baz)
list(quux([1, 2, 3, 4, 5, 6]))  # ==> [3, 4, 5]


Да. При этом в чистом языке можно подряд идущие filter f. map g сворачивать в использование одной функции (Rewrite rules), потому что ни f, ни g не производят побочных эффектов.

Я ничего не знаю про Rewrite rules, но ни одна из перечисленных функций не обладает побочными эффектами.

А что если у нас не списки, а деревья? Как сделать всё то же, но для своего типа данных?

По деревьям вы тоже итерируете последовательно? Тогда добавьте интерфейс итератора к вашему объекту (переопределите некоторые специальные функции) и сможете итерировать по ним.
Уж лучше идиоматично через list comprehensions, а то так и нечитаемее, и тормознее. А если идиоматично, то уже bar оттуда не вычленить так просто, композиционность страдает.

> Я ничего не знаю про Rewrite rules, но ни одна из перечисленных функций не обладает побочными эффектами.
Как же это не обладает, когда _может_ обладать (у меня там недаром f и g — произвольные функции), а это для оптимизатора весьма важно.

> По деревьям вы тоже итерируете последовательно?
Можно даже бесконечные деревья (как и списки) строить. Необязательно последовательно. Те же самые filter, map, что-либо ещё.

> Тогда добавьте интерфейс итератора к вашему объекту (переопределите некоторые специальные функции) и сможете итерировать по ним.
Итератор ходит вперёд. По дереву я могу своей функцией идти в том направлении, в котором мне надо. Это уже другой тип итератора совсем. Ходить вперёд — частый, хотя и распространённый, случай.

Понятно, что я не буду утверждать, что вот Haskell-way — самый лучший, я просто к тому, что дьявол в деталях. В придуманных примерах важнее одно, в реальных задачах — другое, причём в каждой что-то своё.
Уж лучше идиоматично через list comprehensions, а то так и нечитаемее, и тормознее. А если идиоматично, то уже bar оттуда не вычленить так просто, композиционность страдает.

Ну так и язык не функциональный, понятно, что сложные функциональные штуки типа каррирования и композиции функции в нём будут хотя бы чуть-чуть, но сложнее синтаксически :D

Как же это не обладает, когда _может_ обладать (у меня там недаром f и g — произвольные функции), а это для оптимизатора весьма важно.

А, вы про `f` и `g`. Ну, тут есть 2 момента. Во-первых, Python по-умолчанию считает своих разработчиков достаточно вменяемыми, чтобы не передавать в `filter` и `map` функции с побочными эффектами. Если кто-то всё-таки передаёт, то тут уж извините, отстрелить себе ногу, как известно, можно в любой ситуации. Во-вторых, Python — это как раз тот язык, в котором про оптимизацию особо не думают. Если хотите, могу переписать всё это на высокопроизводительной Julia ;)

Можно даже бесконечные деревья (как и списки) строить. Необязательно последовательно. Те же самые filter, map, что-либо ещё.

Ну так ради б-га, кто ж мешает. Python здесь ничем не отличается от Haskell (или Java, или C++): выбираете абстрактную функцию с поведением (например, `map`), задаёте требуемый интерфейс (например, `get_next`), реализуете интерфейс для своего класса и вуа-ля!

Итератор ходит вперёд. По дереву я могу своей функцией идти в том направлении, в котором мне надо. Это уже другой тип итератора совсем. Ходить вперёд — частый, хотя и распространённый, случай.

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

Ну так и язык не функциональный, понятно, что сложные функциональные штуки типа каррирования и композиции функции в нём будут хотя бы чуть-чуть, но сложнее синтаксически :D

Чуть-чуть — это мягко сказано :)

Python по-умолчанию считает своих разработчиков достаточно вменяемыми

Да Python вообще динамический :) Так что Bondage & Discipline явно не в его стиле, конечно

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

Речь не о том, можно или нет. В любом Тьюринг-полном можно много чего. Вопрос в том, чего это стоит.

-- | Дерево
data TreeT a t = Empty | Node a [t]
type Tree a = Fix (TreeT a)

-- | Бесконечное дерево, в узле n, поддеревья: genTree (n + 1) и genTree (n + 2)
genTree n = Fix (Node n $ map genTree [n + 1, n + 2])

-- | Свернём в список, первый элемент - узел, а затем идут свернутые поддеревья, которые обходятся в обратном порядке (т.е. сначала сворачивается последнее поддерево, затем предпоследнее, и т.п.)
foo = cata f (genTree 0) where
  f Empty = []
  f (Node x xs) = x : concat (reverse xs) -- reverse - это вот обратный порядок уже обойденных поддеревьев, мы лишь склеиваем "готовые" списки

-- | Берём первые 10 элементов
test = take 10 foo -- [0,2,4,6,8,10,12,14,16,18]


Вот здесь дерево ленивое — две строки.
foo — обходит поддеревья в обратном порядке
test — берёт только первые 10 от «бесконечного» дерева
В foo можно было не брать библиотечную cata (катаморфизм), а обходить ручками, если хочется более полный контроль, хотя помимо cata там есть и другие вещи, к которым и так сводится множество обходов и обработок.
Чуть-чуть — это мягко сказано :)

Эээ, это вы с учётом того, что приведённый кусок — это готовый к исполнению код, а конкретно ваш пример:

foo = take n
bar = filter f
baz = map g
quux = foo . bar . baz

транслируется в

foo = partial(itake, n)
bar = partial(ifilter, f)
baz = partial(imap, g)
quux = compose(foo, bar, baz)

Так что да, я бы сказал, что именно чуть-чуть.

Вот здесь дерево ленивое — две строки.

Нуу, тут хоть и две строчки, но специальных фич Haskell-а целая куча — и ленивые вычисления, и рекурсия через фиксированную точку, и развитая система типов. Я даже не буду пытаться повторить это на императивном динамически-типизированном Python. Но просто, чтобы показать, что всё возможно и на нём, вот вам простая реализация рекурсивного дерева:

class Tree(object):
    def __init__(self, n):
        self.n = n

    def __getattr__(self, name):
        if name == 'left':
            return Tree(self.n + 1)
        elif name == 'right':
            return Tree(self.n + 2)
        else:
            raise AttributeError("No such attribute: %s" % (name))

    def __repr__(self):
        return "Tree(%s)" % (self.n,)

И пример использования:

In [30]: t = Tree(1)

In [31]: t
Out[31]: Tree(1)

In [32]: t.right
Out[32]: Tree(3)

In [33]: t.right.right
Out[33]: Tree(5)

In [34]: t.right.left
Out[34]: Tree(4)

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

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

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

Согласен, приятно было пообщаться.
Ну пусть будет у нас пул из 1000 буферов, пусть даже каждый буфер уже будет нужного размера — что угодно. Мы вызываем нашу функцию, результат записываем в буфер #1, затем в буфер #2, затем в буфер #3

Да кстати это плохое решение, если у моего процессора 24 МБ кеша L3, а я реализую быстрое возведение в степень 4-мегабайтных матриц. На Си++ всё бы влезло в кеш, в случае с тысячей буферов — уже нет.
Кто мешает написать функцию ФП с «выделением» памяти, которое на самом деле будет просто возвращать ссылку на область памяти из предвыделенного пула?

Так мы смешиваем системное и прикладное программирование.
Пользователь хочет писать чистую функцию, а это значит, в неё нельзя передать указатель на буфер.
То есть чистая фунция пользователя — закрытый ящик, внутри которого пользователь размещает грязные хаки, вызов ф-ций с побочными эффектами, такие как захват буфера из списка. Если большая часть программы — работа с такими буферами, то большинство написанных пользователем ф-ций будут такими ящиками (внутри содержащими работу с памятью), это неидеоматично писать на ФП-языке в грязном стиле
Почему неидеоматично?
Классикой написания на хаскелле считается выведение всех операций ввода-вывода в отдельные функции и тем самым получение в остальной программе чисто функционального стиля.
Чем же хуже вариант с выведением в отдельные функции операций, оптимизирующих работу с памятью и другими ресурсами?
Фунция, которая вызывает не чистую функцию «получи следующий буфер», сама становится не чистой. Значит, многие оптимизации к ней уже не применимы.
«Буфер `c` передаётся извне и модифицируется, ровно так же, как в примере с `DoSomething`»

Он не модифицируется (для этого необходимо чтение), а _полностью перезаписывается_.
Ваш пример чисто функциональный 'с = a + b'.
Причём тут DoSomething() вообще?

Нет никакой разницы между C sum(A B) и sum(*C, const *A, const *B)
Когда вы в том же C возвращаете структуру, её адрес передаётся первым параметром.
"«Каждый раз новый буфер» означает, что вы можете сохранять полученное значение сколь угодно долго и оно никогда никем не будет модифицированной."
Да кто ж мешает. Сделайте «бесконечный» пул и храните там.
«Сколь угодно долго» хранить объект на который никто не ссылается — бессмысленно.
Он не модифицируется (для этого необходимо чтение), а _полностью перезаписывается_.
Ваш пример чисто функциональный 'с = a + b'.

Прочитайте описание функции внимательно — предыдущее значение `C` считывается наравне с `A` и `B`.

Причём тут DoSomething() вообще?

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

Нет никакой разницы между C sum(A B) и sum(*C, const *A, const *B)

Разница в том, что в первом случае `sum()` сначала конструирует новый объект в памяти, а во втором результат сразу последовательно пишется в преаллоцировнный (и переиспользуемый) `C`. Чтобы совсем очевидно было (на псевдо-Си):

array sum(array a, array b)
{
    array c = zeros(length(a));    // мы только что выделили кусок памяти
    for (i = 0, i < ) {
        c[i] = a[i] + b[i];
    }
    return c; 
}


array sum(array a, array b, array c)
{
    for (i = 0, i < ) {
        c[i] = a[i] + b[i];   // заполняем преаллоцировнный буфер
    }
    return c; 
}

Как результат: в первом случае результат более «предсказуемый», но мы получили существенное снижение производительности; во втором переменная `c` до и после вызова функции `sum` будет иметь разное значение, но мы работаем на константном объёме памяти, даже если вызовем функцию 20000 раз (примерно столько раз в секунду вызывается функция, которую я сейчас оптимизирую).

Да кто ж мешает. Сделайте «бесконечный» пул и храните там.

А память чистить не будем?
Мне кажется, что правильное именование методов более способствует возможности продумывать код:
>DoSomething(x)
а почему не TurnToFalse(x) или хотя бы Process(x)?
>cust1.Equals(cust2);
опять же cust1.IsSameInstance(cust2), cust1.HasSameValue(cust2),

более того cust1.HasSameTypeWith(cust2), cust1.HasSubTypeOf(cust2), cust1.HasCommonBaseTypeWith(cust2)

>будет ли свойство Address равно null или нет
ну это просто: cust.Address.Country == null
но с пунктом о корректной инициализации согласен безоговорочно

>GetHashCode
ComputeHashCode

>repo.GetById
а почему не repo.TryGetById?, ведь может и не найти
repo.GetByIdOrDefault найдет обязательно

еще в C# есть nullable types
msdn.microsoft.com/en-us/library/1t3y8s4s.aspx(полезный пример по ссылке)
Кажется тут есть фундаментальная проблема. F# и Scala построены на .NET и Java стеках, (не только на CIL или JVM).
Зачем? Чтобы использовать базовую библиотеку (весьма богатую в случае .net) или сторонние библиотеки (коих дофига и там и там).
И тут вся предсказуемость летит к чертям.
Не то, чтобы это делало такие фишки языка бесполезными, в конце концов мы чаще вызываем другие написанные нами же функции. Но надеяться на полную предсказуемость в условиях таких сложных стеков кажется бесполезно.
Я вот не понял на кой вначале упомянут F#? Типа это язык, лишенный обозначенных проблем? Тогда почему ни к одному примеру не идет вариант «как это прекрасно выглядит на F#»?

Или я не понял, а Ф — тоже «плохой язык»?
Где решение то обозначенных проблем?
А вы внимательно читали статью? :)
Интерестно, что Rust проходит одновременно по всем пунктам.
Забавно, что вопросы о том, зачем переходить с Java на Scala и Clojure (вот тут я вообще не знал, что на него переходят) не поменялись с приходом лямбд.

Переходят из-за синтаксиса и скорости разработки, теряя в скорости работы программы.
Тысяче-первая статья о второстепенных проблемах, которые мало кого волнуют. Теоретики потому теоретизируют, что практически ничего не пишут. Основная проблема — как сделать вот этот кусок программы не зависимым вот от этого куска программы. Допустим два куска имеют разные интерфейсы — за день можно настрочить огромную формальную обертку одного в другое. Ну и что, что избыточный и глупый код — зато он делается на раз. А давайте программировать сверху вниз, по плану, по порядку. А на деле всегда снизу вверх — уж что получится в полном беспорядке. Каждое наследование — это порождение новой сучности. Вот возьмем например клиента и товар — как хорошо, как любят такие примеры. А вот другой пример: абракадабра12 наследуется от нонсенса16, который наследуется от абстракция_палата_6. Всего то одна тысяча сучностей, нетрудно запомнить, все логично и последовательно. Порой лучше написать повторяющийся код только для того, чтобы класс не зависел от других классов. Вместо наследования влепить интерфейс и сделать отдельно. Ах как это не логично, ах как это избыточно, ах на диск не поместится.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории