Информация об изменениях

Сообщение Re[26]: «Собаку съел» от 28.01.2017 10:17

Изменено 28.01.2017 10:18 vdimas

Re[26]: «Собаку съел»
Здравствуйте, samius, Вы писали:

V>>А только это и важно.

S>Кому как

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


V>>С другой стороны есть механики его обеспечения компилятором — матчинг типа и соответствующей ему ф-ии/метода на этапе компиляции/оптимизации или на этапе исполнения через таблицу ф-ий, как для случая виртуальных ф-ий в ООП или в некоторых сценариях с использованием классов типов в том же Хаскеле.

S>верно. Но цитата из Пирса однозначно отсылает к механике выполнения в рантайме.

Не ведись на это. ))
Хороший теоретик не обязан быть хорошим инженером, и обратное тоже верно.

Не имеет никакой разницы, как в конечном итоге реализован полиморфизм в конечном бинарном образе.
Вернее, это имеет значение, только когда такой полиморфизм реализован ПЛОХО, тогда волей-неволей приходится держать в голове некоторую ЦЕНУ этого полиморфизма. Если же цена реализации близка к 0-лю (в случае С++ или value-type параметров генериков дотнета), то "оно" сразу становится не принципиальным. Ну, т.е. никаких квантов внимания разработчиков не потребляет.


S>И если интерпретатор, рантайм, или еще кто выполняет разные версии кода для разных типов, то это ad hoc.


Хороший оптимизатор в итоге и должен сводить подавляющее большинство сценариев к ad hoc.
Но это, повторюсь, лишь подробности реализации компилятора, которые могут быть несколько ортогональны "внешним" возможностям ЯП.


V>>Неверно. Тут в разные годы пробегали примеры на Хаскель с использованием классов типов, где без динамического матчинга времени исполнения никак — компилятор Хаскель создаёт таблицы ф-ий для их инстансов для конкретных типов.

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

Вот.


V>>По-сути мы зацепились вокруг того, что хотя компилятор C# в 99% случаев был бы способен разресолвить параметрический полиморфизм на этапе компиляции (т.е. приведя его к ad hoc через вывод монотипов, прямо как на шаблонах С++), однако же, ради "экономии" размера бинарника всё будет сведено к динамическому ad hoc-полиморфизму на основе виртуальных вызовов типов-ограничений (интерфейсов или абстрактных/виртуальных баз).

S>не вполне распарсил выделенное

Монотип — это конкретный тип.

В общем, ясно. ))
Попробую разобрать всё это нагромождение "городских легенд" вокруг полиморфизма:

Изначально под "типизированностью" понималось оперирование в неких "окончательно определённых типах" в исходнике. Их в Системе F (которую рекламирует Пирс в своей работе "Типы данных") называют "монотипами". Просто термин такой.

А теперь смотри на трансформацию понятия "типизированности". Когда речь о монотипах, то тип по классике — это лишь некое множество (допустимых) значений. Когда же идёт речь о полиморфизме, т.е. об АБСТРАГИРОВАНИИ от конкретных типов, то под "типизированностью" в полиморфизме понимают сочетание некоего "черного ящика" (абстрактного типа, чьё множество значений нам НЕИЗВЕСТНО, мы же именно от этого и абстрагируемся) и допустимых операций над ним. Итого, под "типизированностью" в абстрактном программировании понимают набор операций над чёрным ящиком, этому набору дано название "концепт". В Системе F (в Хаскеле/ML), дополнительно к списку операций над абстрактным типом есть еще его "открытая" структура (алг.тип, списки и туплы), в свою очередь состоящая из одного или более "концепта".

Итого. Берем обычный ООП-полиморфизм:
foreach(Widget w in widgets)
  w.Draw(context);

Список операций над типом задан нам одним интерфейсом абстрактного класса Widget.

А что делать, если мы хотим задать более одного списка операций над "черным ящиком"? Вот только в этом месте возникает параметрический полиморфизм в ООП. В генериках дотнета надо будет задать более одного "ограничения".

Ты уже увидел тот прикол, что в случае единственного "ограничения" для генериков дотнета смысла в генериках получается немного? ))
У нас там остаётся всего одна "параметрически полиморфная" операция, помимо заданных в списке операций указанного "ограничения" — это копирование "типизированной" ссылки на объект (т.е. экономия на "обслуживании" приведений типов в аргументах и возвращаемых результатах публичных методов, т.е. на генерации бинарного кода выброса исключения в случае неудачного такого приведения). Остальное не сильно отличается от приведённого примера "обычного ООП-полимофизма".

А увидел ли еще тот прикол, что в случае Системы F для аналогичного приведённому сниппету уже требуется механизм параметрического полиморфизма + единственное ограничение — Wiget? ))

Т.е., прикол в том, что в ООП такой сценарий всё еще НЕ считается параметрическим полиморфизмом, хотя его бинарная механика идентична происходящему в Хаскель в этом же сценарии — в обоих случаях идёт рантайм-матчинг конкретного типа и соответствующей ф-ии Draw по сгенерённой компилятором таблице ф-ий.

Далее. Параметрический полиморфизм в ограничениях называют "типизированным параметрическим полиморфизмом". В этом смысле шаблоны С++ дают технику "нетипизированного параметрического полиморфизма". Однако, перегрузка имен ф-ий в области видимости шаблонного кода (и/или перегрузка операторов) таки дают своего рода "ограничения" в том смысле, что если операции из используемых в теле шаблона найти не удалось, то компиляция будет не успешной. Но "открытость" области видимости представляет из себя некоторую опасность, угу. Держим этот момент в уме.

Дополнительно в шаблонах С++ доступны типы, определённые внутри других типов (непосредственным образом или через typedef), что дополнительно позволяет определять и использовать отношения м/у типами, где этот механизм с определённой натяжкой можно приравнять к трюку с "открытой структурой" типов в Хаскеле/ML/Системе F. "С натяжкой" — потому что любой матчинг типов должен быть произведён в compile-time. Аналогично с доступом к открытым полям структур (в т.ч. классов и объединений).

Я думаю, всего сказанного должно быть достаточно, чтобы понять, как даже на С++ сделать тот самый "типизированный параметрический полиморфизм". Можно использовать операции с явным указанием области видимости — из специального нейспейса или некоего типа — "словаря операций". Т.е., достаточно лишь "закрыть" область поиска перегруженных ф-ий. Список операций над типом из указанной области видимости и будет обеспечивать ту самую "типизированность" в обобщённом программировании, т.е. задавать однозначность ресолвинга нужной ф-ии. И такая техника тоже используется — например, в контейнеры подаётся специальный "словарь" — allocator<T>, где операции над типизированным выделением/освобождением памяти производятся через такой "словарь". Причём, "словарь" может реализовать как статические методы, так и методы экземпляра. Тут в С++ сильно помогает то, что через имя переменной и точку ( al.alloc() ) можно вызывать не только методы экземпляра, но и статические. Т.е., на самом деле всё вместе получается не так уж и грустно, а местами очень даже удобно.


V>>Разные виды полиморфизма ни в коем случае не ортогональны друг другу, а часто друг через друга выражаются или некие случаи представляют из себя одновременно несколько разновидностей полиморфизма. Поэтому, ИМХО, — это рассуждения вникуда, когда пытаются один вид полиморфизма "тщательно отделить" от другого.

S>Не вполне согласен. Есть чистые случаи (пусть они крайности), есть комбинированные.

Ну вот я тебе дал сниппет ООП-кода. Это тот самый частный случай параметрического полиморфизма, реализованный через динамический ad hoc.

А разве принято, скажем, считать, что в Объектном Паскале присутствует параметрический полиморфизм? )) Вот. Поэтому я лишь скромно советую не забивать себе голову попытками тщательного отделения одной разновидности полиморфизма от другой. Тем более, что хороший оптимизатор запросто умеет сводить одно к другому "унутре" (я тут рядом жаловался на то, что компилятор С++ слишкком часто генерит прямые вызовы вместо виртуальных, распространяя своё "зрение" транзитивно, т.е. и в вызываемом коде опять имеет возможность повторить такой же трюк, коль ранее свёл абстрактный тип к монотипу).


V>>Например, в Хаскеле предопределены некоторые базовые классы типов, необходимые для использования операторов языка: Eq, Ord, Num, Real, Integral, Monad или для базовых операций преобразования в строку — Show. Т.е., совсем "из ничего" не получится.

S>Вот все что я выделил — это https://wiki.haskell.org/Polymorphism#Ad-hoc_polymorphism
S>С этим есть разногласия? А я говорил о параметрическом.

Это всё параметрический полимофизм тоже. ))
Ты прямо "с самого низа" сразу можешь описывать параметрически полиморфные ф-ии, используя уже предопределённые в рамках стандарта языка "ограничения" — классы Eq, Ord, Num, Show и т.д. Т.е., тебе не всегда нужны свои (пользовательские) классы типов.


V>>А вот не надо пытаться тщательно отделять мух от котлет, когда речь о полиморфизме. ))

V>>Потому что это зачастую банально невозможно.
S>Что тут сложного? equal_to — ad hoc, Eq a — ad hoc.

А тут:
equal_to :: (Eq a) => a -> a -> Bool
equal_to a b = (a == b)

?

S>Но если внезапно из рассмотрения выкинуть все типы, кроме тех, у которых определен оператор == или инстанс Eq, то тогда они выглядят как параметрические.


Ну вот я против таких заморачиваний и выступал в прошлом сообщении, что это всё условности. Ты можешь считать так, что моё определение equal_to для Хаскеля — это всегда параметрический полиморфизм. Такой подход можно назвать "строгим", теоретикам навроде Пирса нравится. А тот факт, что Хаскель способен в 99% случаев привести его к ad hoс через оптимизацию и/или вывод типов в конкретном контексте — лишь приятный бонус, эдакий побочный эффект. ))
Re[26]: «Собаку съел»
Здравствуйте, samius, Вы писали:

V>>А только это и важно.

S>Кому как

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


V>>С другой стороны есть механики его обеспечения компилятором — матчинг типа и соответствующей ему ф-ии/метода на этапе компиляции/оптимизации или на этапе исполнения через таблицу ф-ий, как для случая виртуальных ф-ий в ООП или в некоторых сценариях с использованием классов типов в том же Хаскеле.

S>верно. Но цитата из Пирса однозначно отсылает к механике выполнения в рантайме.

Не ведись на это. ))
Хороший теоретик не обязан быть хорошим инженером, и обратное тоже верно.

Не имеет никакой разницы, как в конечном итоге реализован полиморфизм в конечном бинарном образе.
Вернее, это имеет значение, только когда такой полиморфизм реализован ПЛОХО, тогда волей-неволей приходится держать в голове некоторую ЦЕНУ этого полиморфизма. Если же цена реализации близка к 0-лю (в случае С++ или value-type параметров генериков дотнета), то "оно" сразу становится не принципиальным. Ну, т.е. никаких квантов внимания разработчика не потребляет.


S>И если интерпретатор, рантайм, или еще кто выполняет разные версии кода для разных типов, то это ad hoc.


Хороший оптимизатор в итоге и должен сводить подавляющее большинство сценариев к ad hoc.
Но это, повторюсь, лишь подробности реализации компилятора, которые могут быть несколько ортогональны "внешним" возможностям ЯП.


V>>Неверно. Тут в разные годы пробегали примеры на Хаскель с использованием классов типов, где без динамического матчинга времени исполнения никак — компилятор Хаскель создаёт таблицы ф-ий для их инстансов для конкретных типов.

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

Вот.


V>>По-сути мы зацепились вокруг того, что хотя компилятор C# в 99% случаев был бы способен разресолвить параметрический полиморфизм на этапе компиляции (т.е. приведя его к ad hoc через вывод монотипов, прямо как на шаблонах С++), однако же, ради "экономии" размера бинарника всё будет сведено к динамическому ad hoc-полиморфизму на основе виртуальных вызовов типов-ограничений (интерфейсов или абстрактных/виртуальных баз).

S>не вполне распарсил выделенное

Монотип — это конкретный тип.

В общем, ясно. ))
Попробую разобрать всё это нагромождение "городских легенд" вокруг полиморфизма:

Изначально под "типизированностью" понималось оперирование в неких "окончательно определённых типах" в исходнике. Их в Системе F (которую рекламирует Пирс в своей работе "Типы данных") называют "монотипами". Просто термин такой.

А теперь смотри на трансформацию понятия "типизированности". Когда речь о монотипах, то тип по классике — это лишь некое множество (допустимых) значений. Когда же идёт речь о полиморфизме, т.е. об АБСТРАГИРОВАНИИ от конкретных типов, то под "типизированностью" в полиморфизме понимают сочетание некоего "черного ящика" (абстрактного типа, чьё множество значений нам НЕИЗВЕСТНО, мы же именно от этого и абстрагируемся) и допустимых операций над ним. Итого, под "типизированностью" в абстрактном программировании понимают набор операций над чёрным ящиком, этому набору дано название "концепт". В Системе F (в Хаскеле/ML), дополнительно к списку операций над абстрактным типом есть еще его "открытая" структура (алг.тип, списки и туплы), в свою очередь состоящая из одного или более "концепта".

Итого. Берем обычный ООП-полиморфизм:
foreach(Widget w in widgets)
  w.Draw(context);

Список операций над типом задан нам одним интерфейсом абстрактного класса Widget.

А что делать, если мы хотим задать более одного списка операций над "черным ящиком"? Вот только в этом месте возникает параметрический полиморфизм в ООП. В генериках дотнета надо будет задать более одного "ограничения".

Ты уже увидел тот прикол, что в случае единственного "ограничения" для генериков дотнета смысла в генериках получается немного? ))
У нас там остаётся всего одна "параметрически полиморфная" операция, помимо заданных в списке операций указанного "ограничения" — это копирование "типизированной" ссылки на объект (т.е. экономия на "обслуживании" приведений типов в аргументах и возвращаемых результатах публичных методов, т.е. на генерации бинарного кода выброса исключения в случае неудачного такого приведения). Остальное не сильно отличается от приведённого примера "обычного ООП-полимофизма".

А увидел ли еще тот прикол, что в случае Системы F для аналогичного приведённому сниппету уже требуется механизм параметрического полиморфизма + единственное ограничение — Wiget? ))

Т.е., прикол в том, что в ООП такой сценарий всё еще НЕ считается параметрическим полиморфизмом, хотя его бинарная механика идентична происходящему в Хаскель в этом же сценарии — в обоих случаях идёт рантайм-матчинг конкретного типа и соответствующей ф-ии Draw по сгенерённой компилятором таблице ф-ий.

Далее. Параметрический полиморфизм в ограничениях называют "типизированным параметрическим полиморфизмом". В этом смысле шаблоны С++ дают технику "нетипизированного параметрического полиморфизма". Однако, перегрузка имен ф-ий в области видимости шаблонного кода (и/или перегрузка операторов) таки дают своего рода "ограничения" в том смысле, что если операции из используемых в теле шаблона найти не удалось, то компиляция будет не успешной. Но "открытость" области видимости представляет из себя некоторую опасность, угу. Держим этот момент в уме.

Дополнительно в шаблонах С++ доступны типы, определённые внутри других типов (непосредственным образом или через typedef), что дополнительно позволяет определять и использовать отношения м/у типами, где этот механизм с определённой натяжкой можно приравнять к трюку с "открытой структурой" типов в Хаскеле/ML/Системе F. "С натяжкой" — потому что любой матчинг типов должен быть произведён в compile-time. Аналогично с доступом к открытым полям структур (в т.ч. классов и объединений).

Я думаю, всего сказанного должно быть достаточно, чтобы понять, как даже на С++ сделать тот самый "типизированный параметрический полиморфизм". Можно использовать операции с явным указанием области видимости — из специального нейспейса или некоего типа — "словаря операций". Т.е., достаточно лишь "закрыть" область поиска перегруженных ф-ий. Список операций над типом из указанной области видимости и будет обеспечивать ту самую "типизированность" в обобщённом программировании, т.е. задавать однозначность ресолвинга нужной ф-ии. И такая техника тоже используется — например, в контейнеры подаётся специальный "словарь" — allocator<T>, где операции над типизированным выделением/освобождением памяти производятся через такой "словарь". Причём, "словарь" может реализовать как статические методы, так и методы экземпляра. Тут в С++ сильно помогает то, что через имя переменной и точку ( al.alloc() ) можно вызывать не только методы экземпляра, но и статические. Т.е., на самом деле всё вместе получается не так уж и грустно, а местами очень даже удобно.


V>>Разные виды полиморфизма ни в коем случае не ортогональны друг другу, а часто друг через друга выражаются или некие случаи представляют из себя одновременно несколько разновидностей полиморфизма. Поэтому, ИМХО, — это рассуждения вникуда, когда пытаются один вид полиморфизма "тщательно отделить" от другого.

S>Не вполне согласен. Есть чистые случаи (пусть они крайности), есть комбинированные.

Ну вот я тебе дал сниппет ООП-кода. Это тот самый частный случай параметрического полиморфизма, реализованный через динамический ad hoc.

А разве принято, скажем, считать, что в Объектном Паскале присутствует параметрический полиморфизм? )) Вот. Поэтому я лишь скромно советую не забивать себе голову попытками тщательного отделения одной разновидности полиморфизма от другой. Тем более, что хороший оптимизатор запросто умеет сводить одно к другому "унутре" (я тут рядом жаловался на то, что компилятор С++ слишкком часто генерит прямые вызовы вместо виртуальных, распространяя своё "зрение" транзитивно, т.е. и в вызываемом коде опять имеет возможность повторить такой же трюк, коль ранее свёл абстрактный тип к монотипу).


V>>Например, в Хаскеле предопределены некоторые базовые классы типов, необходимые для использования операторов языка: Eq, Ord, Num, Real, Integral, Monad или для базовых операций преобразования в строку — Show. Т.е., совсем "из ничего" не получится.

S>Вот все что я выделил — это https://wiki.haskell.org/Polymorphism#Ad-hoc_polymorphism
S>С этим есть разногласия? А я говорил о параметрическом.

Это всё параметрический полимофизм тоже. ))
Ты прямо "с самого низа" сразу можешь описывать параметрически полиморфные ф-ии, используя уже предопределённые в рамках стандарта языка "ограничения" — классы Eq, Ord, Num, Show и т.д. Т.е., тебе не всегда нужны свои (пользовательские) классы типов.


V>>А вот не надо пытаться тщательно отделять мух от котлет, когда речь о полиморфизме. ))

V>>Потому что это зачастую банально невозможно.
S>Что тут сложного? equal_to — ad hoc, Eq a — ad hoc.

А тут:
equal_to :: (Eq a) => a -> a -> Bool
equal_to a b = (a == b)

?

S>Но если внезапно из рассмотрения выкинуть все типы, кроме тех, у которых определен оператор == или инстанс Eq, то тогда они выглядят как параметрические.


Ну вот я против таких заморачиваний и выступал в прошлом сообщении, что это всё условности. Ты можешь считать так, что моё определение equal_to для Хаскеля — это всегда параметрический полиморфизм. Такой подход можно назвать "строгим", теоретикам навроде Пирса нравится. А тот факт, что Хаскель способен в 99% случаев привести его к ad hoс через оптимизацию и/или вывод типов в конкретном контексте — лишь приятный бонус, эдакий побочный эффект. ))