Комментарии 30
Всё это, конечно, делается и на голом C++:
unordered_mapс ключом из парыtype_indexи методами для регистрации. Локализация правок будет та же. Только компилятор не будет проверять покрытие всех пар, конкретные типы придётся привести к базовому и в каждом обработчике кастовать обратно вручную — компилятор эти касты также не проверяет. В ППП за этим следит компилятор.
На C++ можно тоже через таблицу реализовать, а с шаблонами не надо в обработчиках ничего кастовать. Проверить таблицу можно во время запуска приложения (перед main), например. А как в вашем ППП-компиляторе реализована проверка?
Про шаблоны соглашусь. Тогда разница в том, кто ведёт таблицу, разработчик или язык.
По проверке. Реализация проверок осуществляется во время компиляции. Пока таблица собирается целиком и проверяется на пропущенные пары при запуске, тоже перед main. Так получается из-за раздельной компиляции. В планах перенести сборку таблицы в свой компоновщик.
Реализация проверок осуществляется во время компиляции. Пока таблица собирается целиком и проверяется на пропущенные пары при запуске, тоже перед main.
Взаимоисключающие утверждения.
В планах перенести сборку таблицы в свой компоновщик.
Больше похоже на правду. Мне вот сложно представить полноценную проверку на этапе компиляции с раздельной компиляцией.
В статье мало технических деталей как это устроено внутри и для чего нужен аж целый компилятор для этого, если это может быть реализовано в C++ библиотеке.
Взаимоисключающие утверждения
Неточно выразился. На этапе компиляции проверяются связь специализаций с обобщением и уникальность специализаций.
Спасибо за обсуждение. Технических деталей правда мало, так как статья изначально планировалась обзорной. Если будет интерес к теме со стороны читателей, можно будет написать статью с разбором технических деталей и осветить все вопросы, которые всплывут в комментариях к этой статье. В том числе подробнее сравненить компилятор с C++ библиотекой. Если есть интерес прямо сейчас - можно почитать здесь.
То есть это "процедурно‑параметрическое программирование" - по сути мультиметоды с динамической диспетчеризацией, то есть свободные функции + какой-то аналог таблицы виртуальных функций?
И интересно как это устроено на низком уровне, то есть те самые таблицы. Ведь если в C++ у нас указатель на vbable хранится в начале объекта, то здесь несколько объектов...
Именно. Матчасть с примерами даже в вике
Лучше конечно в английской вике.
Изобретено в 1995г, под большинство языков программирования есть библиотеки.
Впрочем для геймдева, для которого приводятся примеры, применимо с оговорками невысокой производительности.
Не виртуальные таблицы, а локальные индексные. В объекте лежит не указатель на таблицу, а признак специализации(целое число, индекс). Таблицы принадлежат не типам, а обобщающим функциям. Для мультиметода таблица многомерная.
Как это реализовано в PPC описано в статье в разделе 3 (Отображение процедурно-параметрических конструкций в низкоуровневое представление).
Если вопрос в конце не риторический, то вот пример достаточно большой системы: MyCompany. Она написана на lsFusion, где методов, собственно, тоже нет. Свойства и действия глобальные, а реализация может выбираться по классам всех параметров.
Ваш пример с атакой выглядел бы примерно так:
CLASS ABSTRACT Unit;
CLASS Warrior : Unit;
CLASS Mage : Unit;
CLASS Worker : Unit;
attack ABSTRACT MULTI (Unit, Unit);
attack(Warrior attacker, Worker target) + {
// Warrior attacks Worker
}
attack(Mage attacker, Warrior target) + {
// Mage attacks Warrior
}MULTI обозначает, что все реализации не должны пересекаться по сигнатуре. Но можно сделать LIST, тогда можно добавлять много реализаций.
То есть реализация выбирается не только по атакующему, как у обычного метода, а по фактическим классам обоих параметров. Никакие instanceof и Visitor для этого не нужны. При этом реализации можно добавлять из отдельных модулей, не меняя базовый.
Минус ровно тот, который вы описали: нельзя открыть класс и увидеть всё, что он умеет. Но это решается поиском реализаций и использований в IDE.
А вот тезис "новый тип ничего не ломает" спорный. Если для него подходит базовая реализация, то да. Если нужна отдельная специализация, а разработчик её забыл, всё может прекрасно скомпилироваться и работать неправильно. В lsFusion для абстрактного действия можно потребовать полное покрытие классов. Без такого контроля проблема никуда не исчезает, просто проявляется в другом месте.
Языков, в которых есть мультиметоды, сейчас хватает. Начиная с CLOS. В основном доступ к параметрам реализуется с использованием хеширования, что медленнее чем в ЗЗС. Сравнение и обзор есть в статье: Сравнение производительности для различных способов реализации мультиметодов. Системы анализа и обработки данных. – 2025. – № 4 (100). – С. 69–84.(https://journals.nstu.ru/vestnik/catalogue/contents/view_article?id=41742).
На уровне PPC полиморфные функции могут контролироваться примерно также как при использовании абстрактных классов в C++. Обобщенная функция может быть абстрактной. Тогда все альтернативы должны быть обязательно реализованы. Если обобщенная функция имеет тело, то она, как и виртуальные методы, используется по умолчанию там, где нет ее замены.
Хотелось бы увидеть реакцию бренч предиктора в процессоре на это
Если на виртуальные методы как-то работает - то и тут будет. Насколько я понимаю (так себе) - предиктору вообще пофиг как образуется адрес перехода: что он у тебя захардкодан, что он у тебя из vtable берется, что он по формуле считается. Предсказание пишется в линейку кеша инструкций - типа вот в этой линейке на пятой инструкции полетим на такой-то адрес. Повезло и реально полетели - ок, не повезло - выкидываем спекулятивные результаты до перехода и начинаем с места где был переход.
Если у тебя цикл по зайчикам и белочкам в перемешку - будет вероятность предсказать переход 50%, как ты там не делай.
Мне кажется в реальном мире никто не делает N^2, да и функции с перегрузкой и прочее. Есть объект персонажа, есть миксины - свойства, которые имеет персонаж. Там какой-нибудь один из пяти типов атаки, молификаторы и прочее. Что-то на этапе компиляции можно сделать, что-то будет добавляться в процессе. А подход называется композиция. В итоге там может быть вообще один метод обработки атаки для вообще всех, на вход принимает конфиг с числами и парой енумов, на выходе результат взаимодействия. И енумы это не список вообще всех видов персонажей, а маленькие енумы из тех же пяти типов атак, трёх защит и пачка особых свойств, сводящихся к суммированию или вычитанию числовых значений, и всё. Композиция решает такой вопрос, и не только в играх.
Совершенно непонятным осталось то, почему это в Archer
attack<Warrior attacker, Archer target>();
attack<Worker attacker, Archer target>();
attack<Mage attacker, Archer target>();
В классах warrior/worker/mage что по итогу содержится?
Как понять какой метод где искать ?
В ППП нет классов. warrior/worker/mage - это просто структуры без поведения.
attack<Warrior attacker, Archer target>(); - это не метод Warrior, и не метод Archer, а свободая функция. Она может лежать в любом файле
Минус в том, что нельзя открыть файл с этой структурой и увидеть всё, что эта структура умеет. Но это решается поиском в IDE
Ну т.е. эти функции хаотично размазаны по файлам и заранее непонятно где какая окажется ?
Искать ide это отлично. Но чтобы искать надо знать что искать.
Я тут понял, что одна функция с кучей свичей внутри была бы гораздо-гораздо удобнее и безопаснее "на дистанции" :). Да - уродство. Да - бойлерплейт огромный. Но зато все варианты поведения сразу перед тобой и не надо искать в 100500 файлах, а не притаился ли там нежданчик какой. При этом что-то добавляя ты прям видишь как это в прошлом обрабатывалось в каких случаях.
Потому как одно дело "красиво нафигачить", а другое - исправить потом через 10 лет когда фигачил не ты.
Ну т.е. где-то это безусловно оправдано(для любого подхода можно придумать ситуацию когда нужен именно он), но вот где, так сразу в голову и не приходит :(
При любой парадигме вряд ли кто разбрасывает код по файлам, модулям или классам хаотично. Порядок всегда есть и связан с архитектурой проекта. Поэтому распределение по файлам может быть практически аналогично тому, как распределяются базовый и производные классы. Здесь нет противоречий между парадигмами. Все определяется культурой использования модулей.
Но в тот момент, когда вы начнете складывать весь код скажем "к актору", всё это станет совсем уж "малонужным'.
Я не против подхода, я просто не могу сообразить какой в нём толк. Ощущение, что это только запутывает логику, только отдаляет ее от разработчика.
ЗЫ. Вообще есть ощущение что это должно как-то иначе обрабатываться. Т.е. кроме attack должно быть attackedBy и какой-то контракт чтобы везде не проверять кто как и кого.
Кто, как и кого...
Это как раз тот вопрос, который рассматривается во всех подходах. И каждый выбирает свой вариант. Для перебора используется централизация, мультиметоды, диспетчеризация. Добавление новых сущностей требует модификации кода или специальных приемов. Вы по сути предлагаете ОО обмен сообщения, глядя на сущности как объекты. Но можно на взаимодействие смотреть как на процесс в котором описывается взаимодействие напрямую с анализом данных обоих сущностей определенной комбинации. Выбор техники кодирования определяется разработчиком. Меня больше устраивает процедурно-параметрический вариант. Он более гибкий при добавлении новых функций и новых альтернатив, позволяя при этом не изменять ранее написанный код.
Мне кажется, что в ПО больше проблем от архитектуры, нежели от используемого метода программирования.
И сам по себе выбор какого-то подхода или методологии не уничтожает проблему плохих мыслей.
Можно посадить опытного лысыватого толстечка за Паскаль/Дельфи с приказом использовать процедурный подход и метод водопада из 70-х и он сварганит отличный сопровождаемый код за месяц.
А, можно взять целую прыщавую бригаду с лавандовым рафом, чтобы они использовали канбаны, эджайлы, супер-пупер новомодные подходы и шаблоны с интерфейсами и через месяц получить помойку с кучей не отлаженных ошибок, которую для добавления новой фичи надо переписать наполовину.
Сам по себе синтаксис или парадигма какая не смогут заставить дуболомов написать хороший код.
Если появляется какой-то новый подход, который надо заново изучать, а он позволяет упростить лишь малое и редкое число случаев, при этом ломая все старое и не помогая в др. случаях или даже усложняя их, а то же самое уже можно сделать на существующем условном Си++, пусть и немного большим кол-во символов, то я против!
А, по поводу текущего предложения: Си должен развиваться в сторону метапрограммирования, однозначно.
ООП и Си++ не решил всех проблем, но кучу сложностей нагородил.
Хочу гибкий Си с новыми плюшками, но совместимый с предыдущими. Или почти совместимый, чтобы не переписывать логику, а лишь изменить оформление из-за нового синтаксиса, если изменения уж совсем радикальные.
Парадигмы определяют рациональность техники кодирования, убирая лишние телодвижения и влияют на архитектуру. Они взаимосвязаны. Процедурное программирование привело к структурному проектированию сверху вниз. ООП привело к ОО методологии проектирования. И так далее. Знание техники улучшает проектные решения ("чистый код" влияет на "чистую архитектуру") и наоборот. Процедурно-параметрический код тоже изменяет подходы к разработке и формированию структуры программы.
Метапрограммирование не обеспечивает поддержку динамического полиморфизма. Поэтому оно не решает проблем гибкого развития программ, когда альтернативные данные появляются во время выполнения. Только параметрический полиморфизм. Вряд ли метапрограммирование нужно в Си. Это больше для языков прикладного уровня.
PPC расширяет Си динамическим полиморфизмом. При этом сохраняется полная совместимость с Си. Возможно есть что-то ломающие косяки и синтаксис до конца не отработан. Но разные сишные библиотеки (ncurses, SDL, OpenMP и ряд других) подключались и запускались. Совместимость отслеживается специально.
Хочу гибкий Си с новыми плюшками, но совместимый с предыдущими. Или почти совместимый, чтобы не переписывать логику, а лишь изменить оформление из-за нового синтаксиса, если изменения уж совсем радикальные.
C23, BetterC
А помните как в WarCraft 3 было множество разнообразных юнитов и рас, но у каждого юнита был тип атаки и тип брони. Если не подводит память, то броня была лёгкой, тяжёлой и геройской, в свою очередь атака ближней, дальней и осадной.
Лет 20 назад в последний раз играл. Возможно парочку типов забыл.
Я закончил с играми на втором Варкрафте. Третий только наблюдал в битвах детей. Правда сейчас, кода появляется желание кого-нибудь убить, включаю на полчасика Kingdom Rush Frontiers. Там много "крови". Но в играх, как уже отмечали выше, действительно все упрощают. Нужна скорость. Поэтому обычно в юнитах развитие атаки и защиты проще задавать установкой флагов, увеличивая при этом силу удара и защиты. И эти параметры используют при взаимодействии как числа. То есть нет разных стратегий у разных видов бойцов. Можно было бы этот подход поругать, сославшись на паттерн Декоратор. Однако скорость важнее архитектуры. Поэтому и ориентация часто на Data-Oriented Programming. Процедурно-параметрический подход ориентирован на постепенное порождение новых типов данных и функций, длительный процесс разработки. Например варианты нейтрализации дронов в случае разных комбинаций и разновидностей:
Дроны: БПЛА разных характеристик, разные виды БЭК, ...
Оружие: БПЛА, ракеты, стрелковое оружие, РЭБ...
Внешние факторы: погода, ветер, дождь...
...
Появляются все новые варианты за атаку и против нее. Вот такой дурацкий и умозрительный пример для множественного полиморфизма, когда в реальном времени нужно распознавать тип цели и стратегию защиты...
Дальше весь синтаксис будет на PPC — экспериментальном расширении языка C специально под ППП.
Не надо. Пишите на Rust
Трайты это оно и есть
Можно использовать и динамическую диспетчеризацию и статическую
Трайты (типажи, трейты) - это ближе к интерфейсам Go. Это другое. Проверено на множественном полиморфизме. Имитация интерфейсов есть здесь: http://softcraft.ru/ppp/imitation/
Статья про expression problem и ни одного упоминания Haskell с его handle pattern и final tagless...
Интересно, насколько эти подходы отличаются от того, что в этой статье?
Статья больше о динамическом полиморфизме и его возможностях по эволюционному расширению. Haskell использует для более полной реализации ряда возможностей библиотеки. Например, модуль Data.Dynamic. Но через библиотеки Expression Problem обеспечивают многие языки. ПП механизм обеспечивает безболезненное расширение кода в случаях динамического полиморфизма без дополнительных библиотек, используя только особенности своего механизма.

Свободные функции вместо методов, но с полиморфизмом. Что это даёт на самом деле?