Для работы через MTProto нужно передавать в двоичном виде (т.е. сериализовывать) простые и составные типы данных, а также запросы, аргументами или результатами которых являются такие типы данных. Для описания типов сериализуемых данных используется язык TL.
Для наших целей можно отождествить тип с множеством его (сериализованных) значений, понимаемых как строки (конечные последовательности) 32-битных чисел (передаваемых в формате little-endian).
Итак:
0x01000000..0xffffff00
. Верхние 256 значений зарезервированы для т.н.временных комбинаторов, используемых для передачи функций. Мы часто обозначаем имя комбинатора combinator с помощью одинарных кавычек: ‘*combinator*’.combinator_name type_arg_1 ... type_arg_N = type_res;
, где N
- арность комбинатора, type_arg_i
— тип i-ого аргумента (вернее, строка с названием комбинатора), type_res
— тип значения комбинатора.int_tree
с описанием int_tree IntTree int IntTree = IntTree
, наряду с комбинатором empty_tree = IntTree
, может быть использован для определения составного типа данных “IntTree”, значениями которого являются бинарные деревья с целыми числами в узлах.IntTree
:int_tree IntTree int IntTree = IntTree;
empty_tree = IntTree;
List alpha
, где List
— это полиморфный тип арности 1 (т.е. зависящий от одного аргумента), alpha
— переменная типа, появляющаяся как необязательный параметр конструкторов (в фигурных скобках):cons {alpha:Type} alpha (List alpha) = List alpha;
nil {alpha:Type} = List alpha;
constr_num arg1 ... argN
, где constr_num — номер некоторого конструктора C, принимающего значение типа T, arg_i — значение типа T_i, который является типом i-ого аргумента конструктора C. Предположим, например, что комбинатор int_tree имеет номер 17, а комбинатор empty_tree - 239. Тогда значением типа IntTree
является, например, 17 17 239 1 239 2 239
, которое лучше записывать в виде 'int_tree' 'int_tree' 'empty_tree' 1 'empty_tree' 2 'empty_tree'
; с точки зрения языка высокого уровня это int_tree (int_tree (empty_tree) 1 (empty_tree)) 2 (empty_tree) : IntTree
.X
— одетый тип, имеющий не более одного конструктора, то %X
означает соответствующий голый тип. Значения голого типа совпадают с множеством последовательностей чисел, получающихся откидыванием первого числа (т.е. номера внешнего конструктора) из множества значений соответствующего одетого типа (который является типом результата выбранного конструктора), начинающихся с номера выбранного конструктора. Например, 3 4
является значением голого типа int_couple
, определенного с помощью int_couple int int = IntCouple
. Соответствующим одетым типом является IntCouple
; если номером конструктора int_couple
является 404, то 404 3 4
— это значение одетого типа IntCouple
, соответствующее значению голого типа int_couple
(он же %int_couple
и %IntCouple
; последняя форма предпочтительнее с концептуальной точки зрения, но длиннее).С концептуальной точки зрения следовало бы везде использовать только одетые типы. Однако из соображений производительности и компактности приходится использовать голые типы (например, массив из 10000 голых int‘ов занимает 40000 байтов, а из одетых Int’ов - вдвое больше; поэтому при передаче, скажем, большого массива целочисленных идентификаторов выгоднее использовать тип Vector int
, а не Vector Int
). Кроме того, все базовые типы (int, long, double, string) — голые.
Если одетый тип является полиморфным типовой арности r, то это же верно и для любого голого типа, полученного из него. Иначе говоря, если определить intCouple {alpha:Type} int alpha = IntCouple alpha
, то после этого в описаниях комбинаторов (а значит, конструкторов и типов) идентификатор intCouple также является полиморфным голым типом арности 1. Записи intCouple X
, %(IntCouple X)
и %IntCouple X
эквивалентны.
Базовые типы присутствуют как и в голом (code, long, double, string), так и в одетом (Int, Long, Double, String) вариантах. Названия их конструкторов совпадают с названиями соответствующих голых типов. Псевдоописания выглядят так:
int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;
Соответственно, например, номер конструктора int
— это CRC32 от строки "int ? = Int"
.
Значения голого типа int
— это в точности все одноэлементные последовательности, т.е. числа от -231 до 231-1, которые в данном случае представляют сами себя. Значения типа long — это двухэлементные последовательности, представляющие собой 64-битные числа со знаком (снова little-endian). Значения типа double — это снова двухэлементные последовательности, содержащие 64-битные вещественные числа в стандартном формате double. Наконец, значения типа string выглядят по-разному в зависимости от длины передаваемой строки L. Если L= 253, то кодируется один байт L, затем L байтов строки, затем от 0 до 3 символов с кодом 0, чтобы общая длина значения делилась на 4, после чего все это интерпретируется как последовательность из int(L/4)+1 32-битных чисел. Если же L=254, то кодируется байт 254, затем — 3 байта с длиной строки L, затем — L байтов строки, затем — от 0 до 3 нулевых байтов выравнивания.
Псевдотип Object
— это “тип”, значениями которого могут быть значения любых одетых типов схемы. Это дает возможность быстро определять типы вроде список чего попало, не используя для этого полиморфные типы. Лучше не злоупотреблять этой возможностью, т.к. она приводит к применению динамической типизации. Тем не менее, структуры данных, привычные по PHP и JSON, довольно сложно представить без применения псевдотипа Object
.
Полиморфный псевдотип Vector t
— это “тип”, значение которого — последовательность значений любого типа t
, одетого или голого.
vector {t:Type} # [ t ] = Vector t;
При сериализации используется всегда используется один и тот же конструктор “vector” (константа 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t")
, не зависящий от конкретного значения переменной типа t. Значением типа Vector t
является номер соответствующего конструктора, за которым следует число N — количество элементов в векторе и далее — N значений типа t
. Значение необязательного параметра t
не участвует в сериализации, так как он выводится из типа результата (всегда известного до десериализации).
Полиморфные псевдотипы IntHash t
и StrHash t
представляют собой ассоциативные массивы, отображающие целочисленные или строковые ключи в значения типа t. Фактически являются вектором, содержащим голые пары (int,t)
или (string,t)
:
coupleInt {t:Type} int t = CoupleInt t;
intHash {t:Type} (vector %(CoupleInt t)) = IntHash t;
coupleStr {t:Type} string t = CoupleStr t;
strHash {t:Type} (vector %(CoupleStr t)) = StrHash t;
Знак процента в данном случае означает, что берётся голый тип, соответствующий одетому типу в скобках; при этом у одетого типа должно быть не более одного конструктора при любых значениях параметров.
Ключи могут быть отсортированы, а могут идти в каком-то другом порядке (как в PHP-массивах). Для случая ассоциативных массивов с отсортированными ключами используется синоним IntSortedHash
или StrSortedHash
:
ntSortedHash {t:Type} (intHash t) = IntSortedHash t;
strSortedHash {t:Type} (strHash t) = StrSortedHash t;
Номер конструктора полиморфного типа не зависит от того, к каким именно типам применяется полиморфный тип. При его вычислении необязательные параметры (обычно содержащие переменные типа и заключённые в фигурные скобки) делаются обязательными (убираются фигурные скобки) и, кроме того, удаляются все круглые скобки. Таким образом,
vector {t:Type} # [ t ] = Vector t;
соответствует номер конструктора crc32("vector t:Type # [ t ] = Vector t") = 0x1cb5c415
. При (де)сериализации конкретные значения необязательной переменной t выводятся из всегда известного типа результата (т.е. сериализуемого или десериализуемого объекта) и никогда не сериализуются явно.
Раньше для каждого полиморфного типа было нужно знать, к каким именно переменным типам он будет применяться. Для этого в описании системы типов использовались строки вида
polymorphic_type_name type_1 ... type_N;
Например,
Vector int;
Vector string;
Vector Object;
Сейчас они игнорируются. См. также полиморфизм в TL.
Псевдотип Object
в данном случае позволяет применять Vector Object
для хранения списков чего угодно (значений любых одетых типов). Поскольку голые типы эффективны тогда, когда они коротки, на практике едва ли понадобятся более сложные случаи, чем приведенные выше.
Предположим, нам надо описывать пользователей с помощью троек, состоящих из одного целого числа (идентификатора пользователя) и двух строк (имени и фамилии). Нужная структура данных — это тройка int, string, string, которая может быть объявлена следующим образом:
user int string string = User;
С другой стороны, группа может описываться похожей тройкой, состоящей из идентификатора группы, ее названия и описания:
group int string string = Group;
Для того, чтобы было понятно, чем отличается User
от Group
, удобно задать для некоторых или для всех полей некоторые названия:
user id:int first_name:string last_name:string = User;
group id:int title:string description:string = Group;
Если потом понадобится расширить тип User
, добавив в него записи с некоторым дополнительным полем, то это можно сделать так:
userv2
id:int
unread_messages:int
first_name:string
last_name:string
in_groups:vector int
= User;
Помимо всего прочего, такой подход позволяет правильно сопоставлять друг другу поля, принадлежащие разным конструкторам одного и того же типа, конвертировать их друг в друга, а также преобразовывать значения типа в ассоциативный массив со строчными ключами (в качестве которых естественно брать названия полей, если они заданы).