Skip to the content.

Система типов NewLang

Так NewLang является языком с динамической типизацией, то явное указание типа не влияет на размер памяти, занимаемой переменной и в основном определяет логические ограничения на возможность присвоения переменной значения другого типа.

Информация о типах используется при проверке их совместимости, когда существующему объекту присваивается значение другого типа. Такая операция возможна только когда типы совместимы между собой и допускают автоматическое приведение. Это справедливо как во время парсинга/компиляции исходного теста, так и во время выполнения в режимах интерпретатора и/или скомпилированного файла.

Простые типы

Арифметические типы данных

Арифметические типы данных являются тензорами (кроме рациональных чисел) — массивами одного типа с произвольным количеством измерений и одинаковым размером столбцов в каждом. Единичное число тоже тензор нулевого размера.

Поддерживаются только знаковые целые числа, т.к. в беззнаковых числах особая нужда отсутствует, а проблем с ними можно найти очень много на ровном месте.

Проблемы беззнаковых чисел (из интернета):

Во-первых, вычитание двух беззнаковых чисел, например 3 и 5. 3 минус 5 равно 4294967294, т.к. -2 не может быть представлено как беззнаковое число. > Во-вторых, непредвиденное поведение может возникнуть при смешивании целочисленных значений со знаком и без знака. С++ может свободно преобразовывать числа со знаком и без знака, но не проверяет диапазон, чтобы убедиться, что вы не переполняете свой тип данных.
В C++ всё же есть несколько случаев, когда можно (или необходимо) использовать беззнаковые числа. Во-первых, числа без знака предпочтительнее при работе с битами. Во-вторых, использование беззнаковых чисел связанных с индексацией массивов.

Но это не мой случай, так как индекс может быть отрицательным и даже не числом, а диапазоном или многоточием.

З.Ы. И даже зная об этом, все равно умудрился недавно словить баг с отрицательными индексами у словарей!

Имена встроенных арифметических типов: Int8, Int16, Int32, Int64, Float16, Float32, Float64, Complex16, Complex32, Complex64. Отдельным типом идет логический тип Bool, который может принимать значения только 0 или 1 (false/true соответственно), и в зависимости от выполняемой операции может быть отнесен к целочисленным типам, так и не входить в их состав (данный подход интерпретации логического типа данных был взят из библиотеки Torch).

  // Treat bool as a distinct "category," to be consistent with type promotion
  // rules (e.g. `bool_tensor + 5 -> int64_tensor`). If `5` was in the same
  // category as `bool_tensor`, we would not promote. Differing categories
  // implies `bool_tensor += 5` is disallowed.
  //
  // NB: numpy distinguishes "unsigned" as a category to get the desired
  // `bool_tensor + 5 -> int64_tensor` behavior. We don't, because:
  // * We don't want the performance hit of checking the runtime sign of Scalars.
  // * `uint8_tensor + 5 -> int64_tensor` would be undesirable.

Доступ к элементам тензора происходит по целочисленному индексу, который начинается с 0. Для многомерного тензора, индексы элемента перечисляются в квадратных скобках через запятую. Поддерживается доступ к элементам через отрицательный индекс, который обрабатывается точно так же, как в Python (-1 последний элемент, -2 предпоследний и т.д.).

Литерал тензор в тексте программы записывается в квадратных скобках с обязательной завершающей запятой, т.е. [1, 2,] — это литерал одномерный тензор из двух чисел. После закрывающей скобки тип тензора может быть указан в явном виде. Если тип не указан, то он выводится автоматически на основании указанных данных и выбирается минимально возможный байтовый размер, который позволяет сохранить все значения без потери точности.

Так же существует отдельный тип для представления рациональных чисел неограниченной точности. Они записываются в форме обыкновенной дроби, в которой числитель должен быть целым числом, а знаменатель натуральным. В качестве разделителя дроби используется обратная косая черта, т.е. 1\1 - рациональное число 1, -5\1 - запись рационального числа -5 и т.д.

Примеры:

  $var_char := 123; # Тип Int8 выводится автоматически
  $var_short := 1000; # Тип Int16 выводится автоматически
  $var_bool := [0, 1, 0, 1,]; # Тензор из 4 элементов. Тип Bool выводится автоматически
  $tensor[10,10]:Int32 := 1; # Тензор Int32 размером 2x2 инициализированный 1
  $scalar := $tensor[5,5]; # Присвоить скаляру значение указанного элемента тензора

Строки

Поддерживаются два типа строк, StrWide — символьные (широкие) и StrChar — байтовые. Различия между ними заключается в типе единичного элемента. У символьных строк единичным элементом является широкий символ wchar_t, а у байтовой строки единичным элементом является один байт (точнее Int8, т.е. байт со знаком). Символьные строки литералы в исходном тексте записывается в «двойных кавычках», а байтовые строки в ‘одинарных кавычках’.

Количество элементов символьной строки возвращается в широких символах, а размер байтовой строки в байтах, поэтому и обращение к элементу строки по индексу происходит соответственно либо к символу, либо к байту.

Важный момент. К любой переменной можно обратиться так же, как к функции (записав после её имени круглые скобки). Результатом этой операции будет создание копии/клона объекта. Причем некоторые типы (словари, классы и символьные строки) можно использовать в качестве шаблона при создании копии объекта с модифицированными свойствами, если новые и/или изменяемые значения указать в скобках, как аргументы при вызовах функций. Так, если при создании копии в скобках указать набор новых данных, то результирующая копия будет содержать уже измененные данные.

Например:

  $template := "${name} $1"; # Обычная строка широких символов
  $result := $template("шаблон", name = "Строка"); # result = "Строка шаблон"

Системные типы

:Pointer — указатель на системную область памяти или нативную функцию

Так как любой программе приходится взаимодействовать с внешним миром, то по неволе приходится закладывать возможность использования других библиотек и системы типов данных, и для этих целей служит тип :Pointer. Он создается при импорте функций из внешних библиотек и вручную его создать нельзя. Но можно вывести его значение, например для отладки.

:Plain — указатель на представление данных в бинарном виде

Для взаимодействия с внешними библиотеками требуется еще и обмен данными. И для этих целей служит тип данных :Plain — который также является указателем, но на двоичное представление данных в области памяти. Конечно, если их можно представить в виде одного фрагмента.

Составные типы данных:

Словарь

Словарь (:Dictionary) — набор данных произвольного типа с доступом к отдельным элементам по целочисленному индексу или по имени элемента при его наличии (он похож и на tuple и на структуру одновременно). Словари от тензоров отличаются тем, что являются одномерными, но каждый элемент может содержать произвольное количество элементов любого типа, в том числе и другие словари.

Доступ к элементам словарей происходит по имени элемента, которое записывается через точку от имени переменной, либо по целочисленному индексу. Индекс также начинается с 0 и как у тензоров и тоже может быть отрицательным.

Литерал с типом «словарь» в тексте программы записывается в круглых скобках с обязательной завершающей запятой, т. е. (,) — пустой словарь, (1, 2= «2», name=3,).

Перечисление, структура и объединение

:Enum, :Struct и :Union — это такие же словари, только на их элементы накладываются определённые ограничения. Каждый элемент должен иметь уникальное имя, а его тип данных должен быть простым, т.е. числом или строкой фиксированного размера. Эти типы данных так же относятся к группе :Plain и могут быть представлены в двоичном виде в одной области мишинной памяти.

Классы

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

Функции

Синтаксис NewLang поддерживать несколько типов функций (а в будущем и методов классов): обычные функции, чистые функции и простые чистые функции.

Для всех типов функций поддерживаются аргументы по умолчанию. При создании функции, её аргументы указываются как в Python, т.е. вначале идут обязательные аргументы, потом аргументы со значениями по умолчанию, где имя аргумента отделяется от его значения по умолчанию знаком равно =. Если функция допускает обработку произвольного количества аргументов, то последним в списке параметров указывается многоточие … (три точки подряд) .

Обычная функция

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

Внутри обычной функции можно обращаться к локальным и глобальным объектам, и они могут содержаться вставки на языке реализации*, например, для вызова функций из внешних библиотек.

Вставки на языке реализации оформляются в виде %{%} и могут содержать любой текст на С/С++, а прямо из него можно обращаться к локальным и глобальным объектам NewLang так же, как и в обычном синтаксисе, указывая первым символом имени соответствующий модификатор ($ для локальных объектов и @ для глобальных).

Технически, такая программная вставка просто переносится трансплайтером непосредственно в исходный текст генерируемого файла, а все идентификаторы NewLang специальным образом декорируются (добавляются специальные маркеры для их идентификации), после этого исходный текст подается на вход обычному компилятору С++. Для локальных объектов трансплайтер может генерировать код для прямого доступа к объекту на этапе компиляции, а для работы с глобальными объектами вынужден использовать runtime вызовы функции поиска в таблице символов. *** *) — Программные вставки на языке реализации обрабатываются только во время компиляции

Например:

  print(str) := { 
      %{ 
          printf("%s", static_cast<const char *>($str)); /* Прямой вызов С функции */ 
      %} 
  };

Чистые функции

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

Программные вставки на языке реализации внутри чистых функций не запрещены и могут использоваться, например, для отладки. Но делается это на страх и риск разработчика. Именно он отвечает за их «чистоту», например при вызове функций из внешних библиотек.

  Sum1(arg1, arg2) :- {$arg1+$arg2;}; # Создать или переопределить простую функцию, которая возвращает сумму аргументов
  Sum2(arg1, arg2) ::- {$arg1+$arg2;}; # Тоже самое, но если функция с таким именем уже существует, то будет ошибка

Простые чистые функции

Простые чистые функции — отдельный класс чистых функций, которые предназначены только для вычисления логического результата (т.е. они являются предикатами) и их отличает упрощенная формой записи. Тело простой чистой функции состоит из последовательности операторов, которые разделяются запятыми и заканчивается, как и любое выражение, точкой с запятой. Все операторы простой чистой функции приводятся к булевому значению, а итоговый результат функции вычисляется по одной из возможных логических операций: И, ИЛИ и исключающее ИЛИ.

Например:

  func_and(arg1, arg2) :&&= arg1==3, arg2 > 0;  # Простая чистая функция Логическое И
  func_or(arg1, arg2) :||= arg1==3, arg2 > 0; # Простая чистая функция Логическое ИЛИ
  func_xor(arg1, arg2) :^^= arg1==3, arg2 > 0;  # Простая чистая функция Исключающее ИЛИ

Специальные типы данных:

Пусто (:None)

:None (пусто) — не содержит значения (точнее имеет одно значение None). Указывается в тексте программы как один подчерк «_». Значение None имеют не инициализированные переменные и при попытке чтения из такой переменной возникает ошибка.

Тип переменной может быть явно указан или выведен автоматически из присваиваемого значения. Присвоить новое значение уже инициализированной переменной можно только для совместимого типа, так как неявное преобразование типов не допускаются.

  $var := _; # Создать не инициализированную переменную
  $var2 := var; # Ошибка!!! Нельзя прочитать неинициализированную переменную var
  $var = 1000; # У переменной будет тип Short (минимальный размер для хранения значения)
  $var = 0,5; # Ошибка!!! Short ← Float не совместимы
  $var = _; # Очистить значение переменной
  $var = 0,5; # Теперь можно, т. к. None совместим с любым типом

Диапазон (:Range)

Диапазон — специальный тип данных, являющейся приблизительным аналогом типа «генератор» в Python. К диапазону можно обращаться как к итератору и он будет поочередно выдавать элементы в указанном интервале с заданным шагом. Диапазон в тексте программы указывается как два или три элемента через две точки, например 1..5 — диапазон от единицы до пяти с шагом по умолчанию 1. В качестве параметров диапазона можно указывать не только литералы, но и имена переменных и даже рациональные числа. Например, 0,1..$stop..0,1 — диапазон от 0,1 до значения, указанного в переменной $stop с шагом 0,1 или 0..1\10 - диапазон рациональных числе от 0 до 10.

Диапазон целых чисел можно использовать в качестве индекса у тензоров (точнее, у любых объектов, которые допускают доступ к своим элементам по индексу, т.е. тензоры, словари и текстовые строки). Фактический, это поведение аналогично slice в языке Python и array[1:5] в Python означает тоже самое, что и array[1..5] в NewLang.

В качестве индекса у тензоров еще можно указать произвольное количество измерений с помощью многоточия, т.е.

  $tensor[…, 0] = 0; # Обнулить все первые элементы в каждом измерении.

Итераторы

Итераторы в NewLang, как и в остальных языках программирования, предназначены для перебора элементов. Но, в отличии от итераторов в С++, итераторы NewLang являются самостоятельными объектаим, а не уаказателями на отдельные элементы объекта-контейнера. Итераторы NewLang поддерживают фильтрацию элементов по имени или функциональный аналог LINQ за счет использования функций обратного вызова для сравнения элементов во время работы итератора.

Для работы с итераторами используются следующий синтаксис:

Так как использование итераторов сложно показать на паре примеров, то подробное описание сделано в виде отдельного материала который можно посмотреть тут:

Преобразование типов

Явное приведение типов

Несмотря на динамическую типизацию языка, если тип переменной указан явно, то автоматическое приведение типов не выполняется, и чтобы присвоить переменой значение не совместимого типа, требуется его явное преобразование.

Так как символьные названия типов относятся к деталям реализации, то явное преобразование в конкретный тип данных производится с помощью вызова функции с именем типа, т.е. :Bool(), :StrWide(), :Int64() и т.д.

Примеры:

  # Создать тензор из строки широких символов с автоматическим выводом типа
  #(тип Int32 будет на системах с Linux, а под Windows тип тензора будет Int16)
  > tstr := :Tensor("Тест");    
  [1058, 1077, 1089, 1090,]:Int32

  # Тоже самое, но тензор заданной размерности
  > t2 := :Tensor[2,2]("Тест");
  [
    [1058, 1077,], [1089, 1090,],
  ]:Int32

  # Создать символьную строку из тензора
  > :StrWide(tstr) 
  Тест

  # Изменить тип данных тезора без изменения размерности
  > :Float64(t2)    
  [
    [1058, 1077,], [1089, 1090,],
  ]:Float64

  # Изменить размерность тензора и его тип 
  # (в данном случае с частичной потерей данных)
  > t3 := :Int8[4]( t2 ) 
  [34, 53, 65, 66,]:Int8

  # Создать тензор из словаря
  >:Tensor( (1,2,3,) ); 
  [1, 2, 3,]:Int8

  # Преобразовать байтовую строку в тензор
  >:Tensor( 'first second' )  
  [102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Int8

  # Получаем тензор из словаря с такими же данными
  >:Tensor( (first='first', space=32, second='second',) )  
  [102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Int8

  # Тензор заданного формата с нулями,
  # где многоточие повторяет последнее значение до требуемого размера
  >:Float64[10,2]( 0, ...)   
  [
    [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,],
  ]:Float64

  # Тензор со случайными данными, где между многоточиями указана функция,
  # которую следует вызывать каждый раз при получении нового элемента тензора
  #
  # Тут пришлось придумывать новую синтаксическую конструкцию 
  # для вызова функции для каждого нового элемента, т.к. многоточие 
  # перед идентификатором, это оператор раскрытия словаря, 
  # а многоточие после идентификатора, это повторение последнего 
  # значения до конца заданной размерности.
  >:Int32[3,2]( ... rand() ...)  
  [
    [1804289383, 846930886,], [1681692777, 1714636915,], [1957747793, 424238335,],
  ]:Int32

  # Создание тензора из диапазона
  >:Int32[5,2]( 0..10 );  
  [
    [0, 1,], [2, 3,], [4, 5,], [6, 7,], [8, 9,],
  ]:Int32

  # Или даже так
  >:Tensor( 0..0.99..0.1 );  
  [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,]:Float64