Здравствуйте, samius, Вы писали:
S>Дада, я вот тоже для тебя западню заготовил, но вот беда (и смешно и обидно вышло... ) переоценил уровень твоего бэкграунда. И ты пролетел мимо этой засады, даже не поморщился. Ну что ж, попробую тебя оттормозить, оттащить назад и все-таки посадить в эту лужу.
S>Хорошо, зафиксируем. Пирс говорит о поведении куска кода. Не знаю, станешь спорить или нет, но поведение куска кода определяется в том числе машинным кодом (если мы говорим о некой машине как исполнителе). Но не каждой инструкцией. Таким образом, есть инструкции, не влияющие на поведение куска кода, а есть — непосредственно влияющие.
S>Когда люди из академической/научной и околонаучной среды говорят о поведении кода, они говорят не о том, как ведет себя исполнитель кода, они говорят о некой процедурной абстракции, которая есть мысленная модель того, что люди ожидают от кода, но в виде того, что они хотят получить и из чего, а не каким образом. Именно опираясь на ожидаемое поведение, компиляторы могут заниматься компиляцией и оптимизацией, суперкомпиляторы могут заменять конкретные вызовы значением или таблицей результатов. Одну и ту же функцию, описанную текстом, компилятор имеет право вставить в нескольких местах вызова совершенно различными наборами инструкций. В одном месте заинлайнить, в другом заменить, в третьем подставить код вызова. Таким образом, функция в тексте может быть одна, но наборов инструкций, ей соответствующих может быть несколько. С другой стороны, комипляторы вольны нагородить лишнего говна, которое работать будет, но менять поведение, при этом, не должно.
S>Чем определяется поведение кода? Для этого, например, можно использовать математическую абстракцию функции — соответствие между множеством определения и множеством значений. Не для всякого кода такая абстракция удобна, потому можно перейти к множествам состояний исполнителя до/после/или даже во время выполнения кода. С точностью до побочных эффектов, если они есть. Но все равно используется некий абстрактный исполнитель, и чем выше уровень языка, тем более значительна разница между абстрактным исполнителем и конкретным исполнителем. Вторичнымм вопросом по отношению к поведению кода является оценка кол-ва шагов, требуемая для его выполнения. Почему вторичным? Потому что это не влияет на возмножность подмены одной процедуры другой.
S>И вот тут я с тобой соглашусь в том, что программистам (по большей части) пофигу на машинные коды, сколько их вариантов, что они там вызывают, а что инлайнят и т.д. Но без машинных кодов конкретный исполнитель "машина" не может обеспечить корректную подмену текста исходника кодом исполнителя. Т.е. код исполнителя имеет значение. Но лишь тот, который ответственен за поведение. То есть, если код функции определения длины списка length по ходу вычислений длины вычислит еще и число пи до тысячного знака и выкинет его, то это не повлияет на поведение кода с точки зрения процедурной абстракции, но, вероятно, при этом введет кого-то в заблуждение в отношении полиморфизма функции length.
S>Так вот, возвращаясь к фиксации (выше я просто подстелил соломку, но в другом месте, не в том, где ты мне яму нарыл): Пирс использует в определении поведение куска кода. Но ты мне предложил лишь два варианта, полагаю что мне нужно выбрать из двух. Я выбираю 2, но с оговоркой. Лишь тот машинный код имеет значение, который влияет на поведение функции. Да, при этом, на всю глубину. Если влияет, то на всю глубину. Если что-то не влияет на поведение — не имеет значение, что там.
Давно не видел такого длинного и при этом бессмысленного (одна вода) текста на данном форуме. Ну да ладно, попытаюсь вычленить из него хоть какие-то тезисы.
Для начала напомню, что все последние сообщения у нас идёт дискуссия исключительно о полиморфизме на уровне машинных кодов. Потому что на уровне исходных кодов мы уже давно всё обсудили и пришли к консенсусу, что все версии equal_to (включая шаблонную std::equal_to, с которой всё началось) реализуют параметрический полиморфизм. Сейчас же мы уже обсуждаем исключительно генерируемый этими функциями машинный код, т.е. совсем низкий уровень (ассемблерные инструкции и т.п.). И если ты не готов к дискуссии на таком уровне (например оказалось, что ты даже смутно не представляешь какой код генерируется по тем примерам на Хаскеле, что мы обсуждали), то возможно и не стоило её начинать?
Далее, если говорить про анализ машинных кодов, то там опять же не всё так страшно (произвольно), как тебе мерещится. Во-первых все ключевые вопросы чётко зафиксированы в документациях (открой например стандарт C++ — там чётко указано какие нюансы отдаются на откуп оптимизатору, а каких должны быть фиксированы — на них можно рассчитывать и при программирование под микроконтроллер и под мейнфрейм). А во-вторых, даже если говорить о нюансах зависящих от оптимизатора, то их точно так же можно без проблем обсуждать в деталях. Только при этом необходимо фиксировать в разговоре версии компилятора и опции его запуска, а так же не бояться такой штуки как дизассемблер. И кстати на этом форуме было уже множество дискуссий на таком уровне, с собеседниками имеющими нормальные познания в области оптимизации. Однако мы с тобой в данной дискуссии даже отдалённо не приблизились к подобному уровню — все наши примеры полностью укладывались в самые базовые сценарии генерации кода.
Ну и возвращаясь к твоему ответу на мой вопрос. И так, ты значит выбрал позицию, что для анализа вида полиморфизма некой функции, надо проанализировать не только её машинный код, но и машинный код всех функций в её стеке вызова. Ну правда с какой-то там оговоркой, но это даже веселее в итоге будет. )))
Раз так, то тогда тебя конечно же не затруднит расшифровать почему ad hoc оператор сложения (находящийся в стеке вызова функции apply из нашего примера на Хаскеле) не должен учитываться при анализе функции apply (она же у нас параметрически полиморфная в том числе и по твоему мнению). Видимо это действие той самой магической оговорки? Или нет?
Ну а после твоего ответа я конечно же ещё спрошу почему эти же аргументы не применимы для абсолютно аналогичного случая ad hoc оператора равенства, находящегося в стеке вызова функции my_equal_to.
_>>Где-то в моём примере объявлена или используется функция вида "g = apply f"? ) Не надо подобных дешёвых фокусов.
S>В том, что касается дешевых фокусов, используя понятие процедурной абстракции, можно верить тому, что поведение функции apply f не отличимо от поведения функции f, и оно же неотличимо от "g = apply f". Если бы это было не так, то функцию apply с аргументом f нельзя было бы использовать вместо функции f. Но ты ее как раз используешь исходя из сохранения поведения. Значит, должен быть удовлетворен любой подстановкой с сохранением его. Если нет — выполняй f сам.
Объясняю ещё раз на доступном языке. Мы здесь изучаем поведение
машинных кодов. Если ты нигде не использовал явно функцию вида "apply f" (а в моём примере такого не было), то в машинных кодах её не будет существовать в принципе. Более того, даже если ты где-то в проекте и используешь её (так что в итоге подобный код будет существовать в итоговом исполняемом файле), то в процессе выполнения моих примеров она всё равно вызываться не будет (всё равно будет прямой вызов нормальной apply с двумя параметрами). Теперь понятно? )
_>>А, ну да) Остроумно. ))) И много таких функций встречается в твоих приложениях? )
S>О, рад что ты оценил. Да, параметрически полиморфных функций в моих приложениях встречается много.
Ну если следовать твоему определению (с исследованием всего стека вызова), то скорее всего ровно ноль. Потому что я что-то сомневаюсь в наличие в твоём приложение этой очаровательной константной функции. )))
S>>>Вообще не получается. Это твои выдумки. Весь стек вызовов функции apply заканчивается на возврате функции, которой ты подашь int или float. И ты либо об этом знаешь, либо никудышный знаток Хаскеля.
_>>
_>>Ты вот что, серьёзно верил, что каррирование реально существует на уровне машинных кодов? Да если бы кто-то и попытался сделать подобное, то оно работало бы не просто медленно (как нынешний Хаскель), а вообще почти не шевелилось бы (наверное хуже чем какой-нибудь bash).
S>Ну причем тут "медленно"?
Притом что:
1. Именно по это причине Хаскель работает не так, как тебе воображалось
2. Именно по этой причине любой вменяемый (знающий хотя бы основы функционирования современных ЦПУ) программист знал бы правильный ответ без обращения к документации ghc. Так что твои заблуждения в данной области наводят на определённые мысли...
_>>https://ghc.haskell.org/trac/ghc/wiki/Commentary/Rts/HaskellExecution/FunctionCalls — на вот, просвещайся как обстоят дела в реальности, чтобы не писать больше подобных глупостей.
S>Вот видишь, согласно понятию процедурной абстракции хаскель может генерить несколько различных тел для одной функции и вызывать их в зависимости от обстоятельств вызова. Сохраняя, при этом поведение, соответствующее execution model. Если завтра технические детали реализации поменяются с "просто медленно" на "вообще почти не шевелилось бы", но сохранится execution model, то на поведение функций apply, f, apply f это не повлияет. Следовательно, сохранится их параметрическая классификация. Будь уверен в этом.
Эм, ты даже такой очевидный текст не можешь нормально воспринять? ) Генерируется ровно один машинный код данной функции. Самый обычный (не с одним аргументом), который ты можешь положить в отдельный бинарный файл. И все вызовы вида "apply f 1" и т.п. (с полным набором аргументов) будут осуществлять прямой вызов этой функции. 80% кода на Хаскеле попадает именно в эту категорию (по этому и более менее шевелится). Плюс к этому, если где-то в приложение встретится отдельная конструкция вида "apply f", то будет сгенерирована (в уже другом бинарном модуле) ещё одна функция, реализующая частичное применение функции apply (естественно через вызов изначальной).