Здравствуйте, samius, Вы писали:
_>>А, я кажется понял чем вызваны твои странные интерпретации и выводы. Вот смотри, у нас есть некая функция f. А так же функция g, которая вызывает внутри себя функцию g. Правильно ли я понял, что в такой ситуации ты в какой-то степени относишь код функции f к функции g (не говорим сейчас про всякие низкоуровневые вопросы типа инлайнинга и т.п., а обсуждаем только исходные коды)? Если да, то тогда конечно понятно, что ты переносишь особенности реализации функции f на функцию g. Однако в моём понимание к коду функции g в связи с f относится исключительно сама процедура вызова (грубо говоря ассемблерная инструкция call), а как там реализована f вообще не важно.
S>Хорошо, давай оторвемся от низкоуровневых вопросов, хотя я к ним не тяготел. Но недалеко. Представим, что у нас есть идеальный исполнитель программы на неком языке, у которого нет проблем с раздувом кода, скоростью выполнения и т.п. И задача, не требующая новых типов и специальных приседаний с ними. Только комбинация. Тогда в принципе пофигу на классификацию. Полиморфно и хрен с ним. И тогда я не против твоего понимания того как устроена g, каким образом она обращается к f, сколько реализаций у f, как их g будет вызывать — чихать. Никаких возражений.
Ну как бы если размышлять только на уровне исходных кодов, то всё так и есть.
А вот разница в видах полиморфизма на таком уровне как раз очень важна. Потому как если в языке доступен только ad-hoc, то это будет вынуждать программистов заниматься многократным размножением одного и того же исходного кода. Кстати, в таких случая иногда вообще уходят в другую сторону и применяют вместо всего этого технику "стирания типов". Например с помощью void* в C или Object в ранней Java. Это всё или не безопасно или медленно, но в любом случае крайне не удобно. Однако предпочитают использовать даже такое, только чтобы не заниматься "копипастой".
Так что на уровне исходных кодов наличие какой-то разновидности параметрического полиморфизма крайне необходимо. А вот на уровне машинных всё наоборот — там оно по нынешним временам только во вред, но об этом ниже.
S>А теперь возвращаемся в реальную жизнь. Одна специально полиморфная функция заражает другую, которая параметризуется ей, третью и далее. Кстати, я определение equal_to не рассматриваю как функцию (с адресом вызова). А вот equal_to<int> и equal_to<float> — вполне себе функции с адресами. И когда ты параметризуешь с помощью equal_to transform, то код transform раздувается по числу используемых типов (адресов операторов). Вот Wadler (или Blott) привел тупой пример, где нужна функция, вычисляющая тройку квадратов по тройке значений. Учитывая что каждое из значений может быть либо целое, либо плавающее, требуется 9 перегрузок что бы обслужить все случаи всего лишь для такого тупого примера.
S>(Не знаю, видел ли ты статью "How to make ad-hoc polymorphism less ad hoc". Погляди по диагонали, она короткая. И она именно о том, почему появились классы типов, какие проблемы они решают. Кстати, там был упомянут подход, при котором равенство для типов должно было быть полностью полиморфным для любого типа (==):: a -> a -> Bool. Что наводит на мысль о том что equal_to все-таки ad hoc)
S>Так вот, ad hoc — это все о том, как пухнет код при топорном подходе к компиляции. А проблема распухания — она более касается функции g, которая комбинирована f-ом, и вообще, чем больше комбинирования ad-hoc-ов, тем больше разбухание. Т.е. когда Wadler писал об этой проблеме, я полагаю что он подразумевал именно это. Будут другие мысли — охотно выслушаю, но только после прочтения "How to make ad-hoc less ad hoc".
S>Ну и, собственно, к equal_to — какой бы он не был красивый и удобный для программиста, он не препятствует разбуханию. Все что он делает — удобненько подставляет адресок мономорфного сравнения. Таким образом, специальная природа сравнения расползается по всему коду, который использует этот equal_to (без дополнительной косвенности). В итоге, и transform становится как вирус.
S>Знал бы Wadler в 88м году что у transform в 2017м будет параметризован 5ю типами...
К данному тексту у меня есть три возражения разной важности:
1. Мелкое замечание. Не очень хороший пример с размножением transform по специализациям применяемой функции. Дело в том, что transform (да и остальные алгоритмы из STL) принимает в качестве параметра как раз не специализированную, а обобщённую функцию (т.е. f, а не f<int>). А специализация происходит уже внутри, в соответствие с типом элементов переданного контейнера. Так что грубо говоря число "копий" transform(..., ..., ..., f) будет в точности равно числу обрабатываемых типов элементов. Т.е. полиморфная природа f никак не влияет на число копий transform.
2. Про неизбежность распухания кода из-за ad-hoc полиморфизма. Ты всё время при подобных рассуждениях держишь в уме реализацию шаблонов в C++, но это же не единственный вариант. Т.е. понятно что в любом языке на самом низком уровне (в конце концов это АЛУ процессора) лежит ad-hoc полиморфизм, над которым выстраиваются слои параметрического полиморфизма (если он вообще есть). В C++ при компиляции все эти слои интегрируются ради быстродействия. Но мы же уже видели на примере отдельной бинарной сборки apply в Хаскеле, что есть и другие реализации. Т.е. при такой реализации низкоуровневый ad-hoc полиморфизм (оператор равенства в нашем примере) легко изолируется на своём уровне и никак не влияет на природу equal_to. Более того, подобное можно элементарно реализовать и на C++, причём даже без всякой переделки компилятора (а вот переписать всю STL придётся). Только оно будет в разы медленнее текущей реализации, так что и задаром никому не нужно.
3. Главное. Даже если забыть в какое время мы живём (когда я скачиваю с сайта фильм в blu-ray качестве (18 ГБ) за 20 минут), то увеличение быстродействия в разы уже давным давно окупает любые распухания кода. Более того, практически все компиляторы (любых языков) имеющие вменяемые оптимизаторы на максимальных уровнях оптимизации увеличивают размер кода. Без всяких шаблонов, полиморфизма и т.п. Просто потому что в основе эффективной оптимизации лежит агрессивный инлайнинг. Т.е. вся индустрия (ну по крайне мере та её часть, где быстродействие хоть что-то значит) стремятся двигаться по этому пути, хотя и не всегда выходит (в том же Хаскеле как только не извращаются, но всё равно выходит медленно), т.к. задача сложная и часто эмпирическая. А в шаблонах C++ это всё даётся с гарантией и так сказать на халяву, по построению. И тут ты говоришь об этом самом распухании как о неком недостатке. Забавно. )
_>>В C# не удобство, а простота (тривиальность). Это большая разница. Если бы было реальное удобство, то не было бы никаких проблем повторить простейший код (записываемый в лоб и на Хаскеле и на C++ и на Python'е и на множестве других языков) из трёх строчек.
S>А мне не нужно повторять простейший код за C++ на C#. У меня почему-то наоборот. Если я захочу повторить 20К C# строк бизнес логики на C++, то получу 40К строк.
Это миф. Потому как в C# имеется только один механизм языка увеличивающий его выразительность и отсутствующий в C++ (причём это продлится только до выхода следующего стандарта C++), а в C++ имеется целый набор таких механизмов, отсутствующих в C#. Так что описанное тобой выше явление может встретится на практике только в одном редком случае: когда для C# имеется готовая библиотека предметной области, а для C++ нет. Такое бывает в некоторых узких областях, но как ты понимаешь к дизайну языка это никакого отношения не имеет.
P.S. Последний абзац нашей с тобой дискуссии является полный оффтопиком к самой дискуссии, но при этом на 100% соответствует изначальном сообщению данной темы на форуме. Забавно получилось. )))