Обобщенное программирование — одна из серых, но важных и интересных сторон daScript. “Серость” темы связана с тем, что, во-первых, система типов не очень детально описана в документации, во-вторых — в рассуждениях о типизации можно от практики быстро уйти в дебри академических терминов, в-третьих, тема плохо укладывается в голову C++-программисту.
Поддержка обобщенного программирования в языке, если “на пальцах” — совокупность способов вызывать одну функцию для разных типов.
Перегрузка функций
Перегруженные функции (ad-hoc полиморфизм) — простейший способ определить функцию для двух различных типов
def func(a : int) |
Константность
Напечатаем тип параметра-аргумента:def func(a : int)
print("{typeinfo(typename a)}\n")
//Output:
int const
По умолчанию к типу был добавлен спецификатор const
, который не позволяет поменять значение аргумента. Его можно убрать, добавив ключевое слово var
:def func(var a: int)
print("{typeinfo(typename a)}\n")
//Output
int
При выборе перегрузки, константная и неконстантная версия, в отличие от C++, не имеют приоритета друг перед другом и при нахождении двух вариантов функции daScript
выдаст ошибку (Правила выбора функции).var a: int
func(a)
//Output
30304: too many matching functions or generics func
candidates:
func ( a : int const ) : void at generics.das:3:4 //принимает int и int const
func ( a : int -const ) : void at generics.das:9:4 //-const читается как "удалить у типа спецификатор const"
Для того, чтобы daScript различил функции, можно добавить спецификатор типа == const
(“константность аргумента должна совпадать).def func(a : int ==const)
print("{typeinfo(typename a)}\n")
def func(var a : int ==const)
print("{typeinfo(typename a)}\n")
[export]
def main
var a: int
func(a)
func(1)
//Output:
int ==const
int const ==const
Ссылки
В предыдущем примере аргумент передавался по значению, поэтому даже var int
не позволяет изменить переданную переменную (меняется значение аргумента, а не оригинальная переменная). Возможно передать аргумент по ссылке:def func(var a : int&)
a = 42
[export]
def main
var a: int
func(a)
print("{a}\n")
//Output: 42
Все непримитивные типы передаются по ссылке, независимо от того, был ли описан аргумент со спецификатором &
или без него.
struct A |
(причём можно описать 2 перегруженные функции с аргуметами типа A и A&, несмотря на то, что для структур семантически это будет идентичная запись)
При этом, как и с константностью, компилятор не различает приоритета перегрузки функций с аргументом-ссылкой и значением, и выдаёт ошибку неоднозначности разрешения перегрузки.
def func(var a: int) |
Контракты
Макросы работают раньше разрешения перегрузки, что позволяет реализовать паттерн contracts — произвольную функцию, которая предварительно проверяет тип аргументов:
require daslib/contracts |
https://github.com/GaijinEntertainment/daScript/blob/master/examples/test/misc/contracts_example.das
вывод typeinfo, кажется, содержит баг
Временные ссылки
Кроме обычных ссылок в daScript есть временные ссылки, которые позволяют работать с объектами из C++-кода внутри блоков. Временная ссылка доступна только внутри блока, и не может быть сохранена вне его (но может быть передана в другую функцию, принимающую временные объекты).
Рассмотрим для примера C++ тип Color
из туториала к daScript. Для него создаётся daScript-обёртка, в которую можно добавить декларацию конструктора и инициализатора с помощью паттерна using
— в этом случае можно создать временную ссылку на тип, которая будет доступна только внутри блока:
//cpp |
Если тип нельзя скопировать или переместить, то using
не будет не будет создавать временный тип — аргумент и так не сможет покинуть блок
Чаще всего нет необходимости в раздельной обработке обычных и временных ссылок, в этом случае можно добавить к типу аргумента спецификатор implicit
:
def printColor(c:Color implicit) |
Небольшое отличие в том, как будет трактоваться аргумент:def printColor(c:Color implicit) // accepts Color and Color#, a will be treated as Color
def printColor(c:Color# implicit) // accepts Color and Color#, a will be treated as Color#
Указатели
Как и в C++, указатели — это ссылки, которые могут указывать на null
, также имеют чуть другую семантику, что позволяет уже без шаманства иметь перегрузки для значения и указателя.
require daslib/safe_addr |
Приведение базовых типов
Базовые типы не приводятся друг другу неявно, требуется явный вызов конструктора типа (Explicit is better than implicit).
def func(a : int) {} |
Приведение классов/структур
Для типов, поддерживающих наследование, неявно выполняется приведение указателей и ссылок от дочернего к родительскомму типу (LSP).
struct A |
Приведение типов структур (cast/upcast/reinterpret
):var a : A
var b : B
var refA : A& = a
var refB : B& = b
//downcast, safe
refA = cast<A&> refB
//upcase, unsafe
unsafe
refB = upcast<B&> refA
//reinterpret cast, VERY unsafe, can cast any
unsafe
refA = reinterpret<A&>(1) //will crash
При выборе перегрузки функции выбирается та, для которой нужно выполнить наименьшее количество преобразований (при равном количестве daScript выдаст ошибку неоднозначности выбора)
struct A |
explicit
Для того, чтобы отключить LSP приведение типа аргумента, можно добавить ключевое слово explicit
. Так
struct A |
Приведение generic-типов
В документации не описана работа с generic-типами (и не дано общее определение для них, также пока отсутствует возможность создания своих типов), но поиском по коду находятся такие встроенные типы (исключая те, которые связаны с оператором typeinfo и кастами):
Функциональные объекты: |
Для таких типов, возможно явное LSP-приведение для типов их аргументов (ковариантность
). Пример для функций:
struct A |
Generic-функции
Вернёмся к самому первому примеру — если мы хотим написать функцию, семантически одинаково обрабатывающую различные типы (например, выводящую значение типа с помощью функции print
) для типов. Чтобы не реализовывать её для каждого нового типа, в языках программирования используется понятие generic-функций, которые могут производить конкретные функции для новых типов автоматически.
Шаблонные функции в C++ производят код конкретных функций на уровне текста, который отдаётся компилятору (если не ошибаюсь, компилятор visual studio в этом плане действительно генерирует полные копии, не остлеживаю возможных повторов, чтобы иметь больше простора для частных оптимизаций функции под конкретные типы, а clang чуть раньше начинает отслеживать потенциально идентичные реализации для экономии памяти).
Другой возможный вариант реализации в Java — “изображать” generic на высоком уровне для контроля типов, но оставлять одну реализацию (все объекты передаются по ссылке, добавляется overhead при работе с value-типами по боксингу/анбоксингу в обёртку).
Третий путь из C# — добавить поддержку generic-функций в виртуальную машину, в этом случае возможна комбинированная реализация — value-типы получают свои сгенерированные копии функций, а reference-типы — общую функцию. Также возможно инстанцировать новые версии функций в runtime. daScript близок к такому типу реализации generic-функций.
Автоматический вывод типов
Если не указан тип аргумента функции, daScript выводит его автоматически, пример функции id принимающей аргумент любого типа и возвращающий его:
options log=true, optimize=false |
По выводу текста сгенерированной программы понятна реализация. Символы подчёркивания перед именем функции __::id
означают “взять реализацию функции только из текущего модуля” (линк), идея будет рассмотрена далее.
Большая часть фич, связанных с generic-функциями, связана с тем, чтобы так или иначе задать или использовать информацию о типах.
auto
Определение для id более развернуто выглядит так:def id(a:auto): auto
return a
Такая форма синтаксиса позволяет задать для каждого из выводимых типов псевдоним, который можно использовать для сравнения типа или получения rtti информации. Несколько примеров:
//print typename |
Использование типа в качестве аргумента
Можно передать информацию о типе в качестве аргумента шаблона, как обычный auto
аргумент.
//generic linear interpolation between int types via cast to float type |
Для того, чтобы тип не передавался в runtime, существует макрос template, который в compile-time убирает такие аргументы.
Шаблоны для auto
Различные формы ограчений для типов аргументов auto. Примеры из доки
def foo( a : auto&) // accepts any type, passed by reference |
Еще раз приведу ссылку на правила выбора функций при наличии нескольких специализаций и перегрузок.
Контракты
Так же, как и к аргументам обычным функциям, к аргументам generic-функциям могут быть применены контракты, позволяющие в более общем виде описать ограничения для типа аргумента. Именно c generic-функциями видна вся мощь контрактов.
require daslib/contracts |
Контракты для одного аргумента могут комбинироваться с помощью операторов !, &&, || и ^^
require daslib/contracts |
Сумма типов
Еще один способ задать ограничения для типа — перечислить разрешенные типы через символ |
(options в доках):
def foo(var a : int | float | string) //accept int or float or string |
Порядок проверки соответствия опций — слева направо:
def foo(var a : auto | int&) { a = 84; } |
static_if
Проверка наличия методов или полей структуры выполняется в момент инстанцирования generic-функции
struct S |
Ошибка возникнет только в момент инстанциирования foo
со структурой, не имеющей поля a
. Проверить наличие полей или другую информацию о типе в время компиляции можно с помощью оператора static_if
:
struct S |
Вызываемые макросы
Более сложные конструкции вроде “вызвать конструктор того же типа, что и поле структуры s.a
можно выразить с помощью макросов
//generics macro |
[generic]
daScript распознаёт обычные или generic-функции по синтаксису, но можно также явно обозначить функцию как generic:
options log=true |
В таком случае вызов func
будет преобразован в __::
func` - вызов версии функции только из текущего модуля. Это используется в некоторых функциях стандартной библиотеки daslib, потому что если компилятор знает, что функция находится в том же модуле, что и вызывающий код, то может её оптимизировать — при AoT-компиляции генериуется не полноценный вызов через ABI (который может вести в другой не-AoT daScript модуль), а прямой вызов, что быстрее.
[instance_function]
С помощью макроса [instance_function]
можно попросить явно специализировать generic-функцию с определенными типами:
require daslib/instance_function |
Видимость модулей
Для generic функций, которые подразумевают переопределение для новых кастомных типов в других модулях, необходимо добавлять префикс _::
или __::
, чтобы обозначить, что функций должна искаться в том модуле, который её вызывает.
//module1.das |
__::
— подразумевает возможность определения функции только в том же модуле, что и вызывающий код (main)_::
— допускает определение как в том же модуле, что и вызывающий код, так и в других модулях (main, module1 или другие модули)