Обобщенное программирование — одна из серых, но важных и интересных сторон daScript. “Серость” темы связана с тем, что, во-первых, система типов не очень детально описана в документации, во-вторых — в рассуждениях о типизации можно от практики быстро уйти в дебри академических терминов, в-третьих, тема плохо укладывается в голову C++-программисту.
Поддержка обобщенного программирования в языке, если “на пальцах” — совокупность способов вызывать одну функцию для разных типов.
Перегруженные функции (ad-hoc полиморфизм) — простейший способ определить функцию для двух различных типов
Константность
Напечатаем тип параметра-аргумента:
По умолчанию к типу был добавлен спецификатор const, который не позволяет поменять значение аргумента. Его можно убрать, добавив ключевое слово var:
При выборе перегрузки, константная и неконстантная версия, в отличие от C++, не имеют приоритета друг перед другом и при нахождении двух вариантов функции daScript выдаст ошибку (Правила выбора функции).
Для того, чтобы daScript различил функции, можно добавить спецификатор типа== const (“константность аргумента должна совпадать).
Ссылки
В предыдущем примере аргумент передавался по значению, поэтому даже var int не позволяет изменить переданную переменную (меняется значение аргумента, а не оригинальная переменная). Возможно передать аргумент по ссылке:
Все непримитивные типы передаются по ссылке, независимо от того, был ли описан аргумент со спецификатором & или без него.
(причём можно описать 2 перегруженные функции с аргуметами типа A и A&, несмотря на то, что для структур семантически это будет идентичная запись)
При этом, как и с константностью, компилятор не различает приоритета перегрузки функций с аргументом-ссылкой и значением, и выдаёт ошибку неоднозначности разрешения перегрузки.
Контракты
Макросы работают раньше разрешения перегрузки, что позволяет реализовать паттерн contracts — произвольную функцию, которая предварительно проверяет тип аргументов:
Кроме обычных ссылок в daScript есть временные ссылки, которые позволяют работать с объектами из C++-кода внутри блоков. Временная ссылка доступна только внутри блока, и не может быть сохранена вне его (но может быть передана в другую функцию, принимающую временные объекты).
Рассмотрим для примера C++ тип Color из туториала к daScript. Для него создаётся daScript-обёртка, в которую можно добавить декларацию конструктора и инициализатора с помощью паттерна using — в этом случае можно создать временную ссылку на тип, которая будет доступна только внутри блока:
Если тип нельзя скопировать или переместить, то using не будет не будет создавать временный тип — аргумент и так не сможет покинуть блок
Чаще всего нет необходимости в раздельной обработке обычных и временных ссылок, в этом случае можно добавить к типу аргумента спецификатор implicit:
Небольшое отличие в том, как будет трактоваться аргумент:
Указатели
Как и в C++, указатели — это ссылки, которые могут указывать на null, также имеют чуть другую семантику, что позволяет уже без шаманства иметь перегрузки для значения и указателя.
Приведение базовых типов
Базовые типы не приводятся друг другу неявно, требуется явный вызов конструктора типа (Explicit is better than implicit).
Приведение классов/структур
Для типов, поддерживающих наследование, неявно выполняется приведение указателей и ссылок от дочернего к родительскомму типу (LSP).
Приведение типов структур (cast/upcast/reinterpret):
При выборе перегрузки функции выбирается та, для которой нужно выполнить наименьшее количество преобразований (при равном количестве daScript выдаст ошибку неоднозначности выбора)
explicit
Для того, чтобы отключить LSP приведение типа аргумента, можно добавить ключевое слово explicit. Так
Приведение generic-типов
В документации не описана работа с generic-типами (и не дано общее определение для них, также пока отсутствует возможность создания своих типов), но поиском по коду находятся такие встроенные типы (исключая те, которые связаны с оператором typeinfo и кастами):
Для таких типов, возможно явное LSP-приведение для типов их аргументов (ковариантность). Пример для функций:
Вернёмся к самому первому примеру — если мы хотим написать функцию, семантически одинаково обрабатывающую различные типы (например, выводящую значение типа с помощью функции print) для типов. Чтобы не реализовывать её для каждого нового типа, в языках программирования используется понятие generic-функций, которые могут производить конкретные функции для новых типов автоматически.
Шаблонные функции в C++ производят код конкретных функций на уровне текста, который отдаётся компилятору (если не ошибаюсь, компилятор visual studio в этом плане действительно генерирует полные копии, не остлеживаю возможных повторов, чтобы иметь больше простора для частных оптимизаций функции под конкретные типы, а clang чуть раньше начинает отслеживать потенциально идентичные реализации для экономии памяти).
Другой возможный вариант реализации в Java — “изображать” generic на высоком уровне для контроля типов, но оставлять одну реализацию (все объекты передаются по ссылке, добавляется overhead при работе с value-типами по боксингу/анбоксингу в обёртку).
Третий путь из C# — добавить поддержку generic-функций в виртуальную машину, в этом случае возможна комбинированная реализация — value-типы получают свои сгенерированные копии функций, а reference-типы — общую функцию. Также возможно инстанцировать новые версии функций в runtime. daScript близок к такому типу реализации generic-функций.
Автоматический вывод типов
Если не указан тип аргумента функции, daScript выводит его автоматически, пример функции id принимающей аргумент любого типа и возвращающий его:
По выводу текста сгенерированной программы понятна реализация. Символы подчёркивания перед именем функции __::id означают “взять реализацию функции только из текущего модуля” (линк), идея будет рассмотрена далее.
Большая часть фич, связанных с generic-функциями, связана с тем, чтобы так или иначе задать или использовать информацию о типах.
auto
Определение для id более развернуто выглядит так:
Такая форма синтаксиса позволяет задать для каждого из выводимых типов псевдоним, который можно использовать для сравнения типа или получения rtti информации. Несколько примеров:
Использование типа в качестве аргумента
Можно передать информацию о типе в качестве аргумента шаблона, как обычный auto аргумент.
Для того, чтобы тип не передавался в runtime, существует макрос template, который в compile-time убирает такие аргументы.
Шаблоны для auto
Различные формы ограчений для типов аргументов auto. Примеры из доки
Еще раз приведу ссылку на правила выбора функций при наличии нескольких специализаций и перегрузок.
Контракты
Так же, как и к аргументам обычным функциям, к аргументам generic-функциям могут быть применены контракты, позволяющие в более общем виде описать ограничения для типа аргумента. Именно c generic-функциями видна вся мощь контрактов.
Контракты для одного аргумента могут комбинироваться с помощью операторов !, &&, || и ^^
Сумма типов
Еще один способ задать ограничения для типа — перечислить разрешенные типы через символ | (options в доках):
Порядок проверки соответствия опций — слева направо:
static_if
Проверка наличия методов или полей структуры выполняется в момент инстанцирования generic-функции
Ошибка возникнет только в момент инстанциирования foo со структурой, не имеющей поля a. Проверить наличие полей или другую информацию о типе в время компиляции можно с помощью оператора static_if:
Вызываемые макросы
Более сложные конструкции вроде “вызвать конструктор того же типа, что и поле структуры s.a можно выразить с помощью макросов
[generic]
daScript распознаёт обычные или generic-функции по синтаксису, но можно также явно обозначить функцию как generic:
В таком случае вызов func будет преобразован в __::func` - вызов версии функции только из текущего модуля. Это используется в некоторых функциях стандартной библиотеки daslib, потому что если компилятор знает, что функция находится в том же модуле, что и вызывающий код, то может её оптимизировать — при AoT-компиляции генериуется не полноценный вызов через ABI (который может вести в другой не-AoT daScript модуль), а прямой вызов, что быстрее.
[instance_function]
С помощью макроса [instance_function] можно попросить явно специализировать generic-функцию с определенными типами:
Видимость модулей
Для generic функций, которые подразумевают переопределение для новых кастомных типов в других модулях, необходимо добавлять префикс _:: или __::, чтобы обозначить, что функций должна искаться в том модуле, который её вызывает.
__:: — подразумевает возможность определения функции только в том же модуле, что и вызывающий код (main) _:: — допускает определение как в том же модуле, что и вызывающий код, так и в других модулях (main, module1 или другие модули)