Типы

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

Предопределенные типы

Следующие типы обозначаются предопределенными идентификаторами, значениями данных типов являются:

Тип Множество значений
Байт множество целых чисел от 0 до 255
Цел64 множество всех 64-битных знаковых целых чисел
Слово64 множество всех 64-битных беззнаковых целых чисел
Вещ64 множество всех 64-разрядных чисел с плавающей запятой стандарта IEEE-754
Лог константы ложь и истина
Символ множество всех Unicode символов
Строка множество всех строковых значений
Строка8 множество всех строковых значений
Пусто литерал пусто

Операции над значениями этих типов определены в разделах унарные операции и бинарные операции.

Тип Строка

Строковый тип представляет собой набор строковых значений. Строковое значение – это (возможно, пустая) последовательность Unicode символов в кодировке UTF-8. Количество символов называется длиной строки и никогда не бывает отрицательным. Строки неизменяемы: после создания изменить содержимое строки невозможно. Количество символов строки можно определить с помощью встроенной функции длина.

Операция индексации для строк не определена:

пусть с = "привет"
пусть сим = с[0] // ошибка

Для работы с байтами строки можно использовать тип Строка8.

Тип Строка8

Тип Строка8 введен для удобной и быстрой работы со строками в стандартных библиотеках. Единственным способом получения значения типа Строка8 является преобразование из строки. Это преобразование выполняется только во время компиляции.

пусть с8 = "Привет"(:Строка8)

Значение типа Строка8 – это неизменяемая, индексируемая последовательность байтов. Стандартная функция длина примененная к значению типа Строка8 возвращает число байтов.

Сравнение типов:

  Строка Строка8
Определение последовательность символов последовательность байтов
Индексация нет да
длина(c) число символов число байтов
Можно менять? нет нет
пусть с = "Привет"
пусть с8 = с(:Строка8)

вывод.ф("%v\n", длина(с)) // выведет: 6
вывод.ф("%v\n", длина(с8)) // выведет: 12
вывод.ф("%v\n", с8[0]) // выведет: 0xD0

Значение типа Строка8 неизменяемое, менять байты в нем нельзя

    пусть с8 = "Привет"(:Строка8)
    с8[0] := 1 // ошибка

Указание типа

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

Указ-типа
    : 'мб' Указ-типа
    |  '[' ']' Указ-типа
    | Имя-типа
Имя-типа: (Идентификатор '.')? Идентификатор

Если указание типа начинается с мб, то определяется может быть тип. Множество значений объекта с типом мб T состоит из значения, обозначенного предопределенным идентификатором пусто и значений типа Т.

Если указание типа начинается с [ ], то определяется тип вектора.

Если в качестве имени типа используется пара идентификаторов, то первый идентификатор должен обозначать имя импортированного модуля, а второй идентификатор должен быть идентификатором экспортированного из этого модуля объекта, в данном контексте – идентификатором типа.

Описание типов

Описание типа связывает идентификатор с новым или существующим типом.

Описание-типа: 
    'тип' Идент-оп '=' 
    ( Тип-класса
    | Тип-протокола
    | Тип-функции    
    | Указ-типа
    )

Если в описание типа указан тип класса, тип протокола или тип функции, то создается новый тип и определяется структура данных этого типа, и, как следствие, набор операций над данными этого типа.

Если в описании типа стоит указание на тип, то это определение еще одного имени для уже существующего типа или задание имени для типа вектора или может быть типа.

тип Цел = Цел64
тип Текст= []Строка
тип Строка? = мб Строка

Так как указание типа не создает новый тип, использование типа Цел из примера идентично использованию типа Цел64.

Тип вектора

Вектор – это пронумерованная последовательность элементов одного типа, называемая типом элемента. Количество элементов называется длиной вектора, длина не может быть отрицательной. Элементы вектора доступны через операцию индексации. Для индексации вектора используются целочисленные индексы от 0 до длина-1. Векторы всегда одномерны, но типом элемента может быть вектор, там самым формируя многомерную структуру.

Тип-вектора: '[' ']'  Указ-типа
тип Байты = []Байт
тип Матрица = [][]Вещ64

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

Пример инициализации вектора из трех элементов:

пусть байты = Байты[1, 2, 3]

Тип класса

Тип класса – это структура, состоящая из полей. С типом класса могут быть связаны функции, называемые методами.

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

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

Тип-класса:  'класс' Базовый-класс? '{' Список-полей? '}'
Базовый-класс: '(' Указ-типа ')'
Список-полей: Поле (Разделитель Поле)*
Поле: Идент-оп (':' Указ-типа)? Инициализация
тип Человек = класс {
    имя: Строка := ""
    возраст: Цел64 := 0
}

Если тип поля не указан, то он устанавливается равным типу выражения.

Инициализация полей

Каждое поле класса должно быть явно проинициализировано. Если инициализация задана через лексему =, то значениe поля не может быть изменено (поле с единственным присваиванием), если же в инициализации используется лексема :=, то значение поля может быть изменено (см. также), а поле называется изменяемым.

Если в инициализации задано Выражение, то начальным значение поля является значение выражения. Если при этом тип поля явно задан, то выражение должно быть _совместимо по присваиванию с типом поля.

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

Значение поля с поздней инициализацией должно быть задано при создании экземпляра класса в конструкторе [экземпляра класса] (../expr/#class-composite).

Экспорт полей

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

Наследование

Наследование позволяет определить новый (расширенный) класс на основе существующего (базового) класса.

тип Работник = класс (Человек) {
    зарплата := 0.0
}

Базовый класс Человек, указанный в описании класса Работник, называется прямым базовым классом. Так как Человек может быть, в свою очередь, расширением другого базового класса, для каждого класса определен список базовых классов, возможно, пустой. Термин базовый класс будет использовать для обозначения любого класса из этого списка. Циклы в списке базовых классов запрещены.

Расширенный класс наследует поля и методы базового класса и может добавлять новые поля и методы. Обращение к полям и методам базового класса ничем не отличается от обращения к полям и методам самого класса. Методы базового класса можно переопределять, сохраняя те же самые типы параметра и тип результата.

Может быть тип

Как правило, современные языки программирования ограничивают работу со объектами ссылочных (reference) типов для того, чтобы сделать явными все места в программе, в которых может возникнуть ошибка использования нулевой ссылки (null pointer exception). Так как русская терминология в этой области не устоялась, приходится обращаться к англоязычным терминам.

Язык Тривиль следует уже выработанному в современных языках программированию подходу (но не синтаксису):

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

Ссылочными типами являются Строка, типы вектора и типы класса, к остальным типам мб не может применятся.

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

Пример указания типа и использования объекта:

пусть кличка: мб Строка := пусто
...
если кличка # пусто { вывод.ф("%v\n", кличка^}

Тип протокола

Тип протокола задает множество заголовков функций.

Тип-протокола: 'протокол' { Заголовок-функции* }
Заголовок-функции: 'фн' Идентификатор Сигнатура-функции

Значением переменной типа протокол П может быть значение любого класса К, который

  • содержит все методы с именами функций, заданными в протоколе,
  • сигнатура каждого такого метода совпадает с сигнатурой функции протокола с тем же именем.

Если выполнены условия, мы будем говорить, что класс К реализует протокол П, как в этом примере:

тип П = протокол { фн мин(а: Цел64, б: Цел64): Цел64 }
тип К = класс {}

фн (к: К) мин(а: Цел64, б: Цел64): Цел64 {
    надо а <= б иначе вернуть б
    вернуть а
}

вход{
    пусть п: П = К{}
}

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

Неявная проверка на то, что класс значения реализует протокол (другими словами, на совместимость значения класса и протокола), происходит при присваивании во время выполнения программы. Для проверки используется динамический тип значения класса (объекта), а не статический (заданный при описании) тип переменной. Если значение класса не совместимо с протоколом, происходит авария.

Для явной проверки совместимости можно использовать проверку типа или преобразование типа.

В данном примере, проверка на совместимость проходит (авария не происходит):

тип П = протокол { фн мин(а: Цел64, б: Цел64): Цел64 }
тип К = класс {}
тип К1 = класс (К) {}

фн (к: К1) мин(а: Цел64, б: Цел64): Цел64 {
    надо а <= б иначе вернуть б
    вернуть а
}

вход{
    пусть к: К := К1{}
    пусть п: П := к
}

Присваивание п := к выполняется, так как к указывает на объект класса К1, который реализует протокол. Присваивание п := К{} приведет к аварии, так как объект класса К1 не реализует протокол.

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

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

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

Сигнатура метода значения не совпадает с сигнатурой функции протокола

тип К = класс {}
фн (к: К) сообщить(ошибка: Строка) {}

тип П = протокол { фн сообшить(ошибка: Цел64) }

вход{
    пусть п: П := К{} // несовместимость типов
}

Использование функций протокола

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

тип Ввод-Вывод = протокол { 
    фн прочитать(): Символ
    фн записать(с: Символ)
}

фн прочитать строку(вв: Ввод-Вывод): Строка {
    пусть сб = строки.Сборщик{}
    пока истина {
        пусть с = вв.прочитать()
        надо с # '\n' иначе вернуть сб.строка()
        сб.добавить символ(с)
    }
    авария("")
}

Тип функции

Тип функции задает набор всех функций с одинаковыми типами параметров и результатов.

Тип-функции: 'фн' Сигнатура-функции

Переменная типа функции совместима по присваиванию с любой функцией или методом с эквивалентной сигнатурой.

Пример корректного использования:

тип Сообщить = фн (с: Строка) 

фн записать(с: Строка) {}

тип Консоль = класс {}
фн (к: Консоль) напечатать(с: Строка) {}

вход{
    пусть сообщить: Сообщить := записать
    сообщить("Привет")
    сообщить := Консоль{}.напечатать
    сообщить("Еще привет")
}

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