Язык TL (Type Language или Time Limit) служит для описания используемой системы типов, конструкторов и существующих функций. Фактически используется формат описания комбинаторов, приведенный в статье двоичная сериализация данных.
Программа на TL обычно состоит из двух секций, разделенных ключевым словом «---functions---
». Первая секция состоит из описаний встроенных и составных типов (т.е. их конструкторов). Вторая секция состоит из объявлений функций — т.е. функциональных комбинаторов.
Фактически и первая, и вторая секция состоят из описаний комбинаторов, каждое из которых завершается точкой с запятой, однако в первой секции участвуют только конструкторы, а во второй — только функции. Каждый комбинатор описывается с помощью ‘’описания комбинатора‘’ в формате, объясненном выше. Однако есть возможность явно задавать номер комбинатора, а также названия полей.
Если нужно после объявлений функций вернуться к объявлению типов, используется ключевое слово (разделитель секций) «---types---
». Кроме того, в разделе типов можно объявить и функциональный комбинатор, если тип его результата предварить восклицательным знаком (фактически раздел функций — это такой раздел, при интерпретации которого этот восклицательный знак добавляется автоматически).
Для явного задания 32-битных имен комбинаторов сразу после имени комбинатора дописывается диез (#), за которым следует 8 шестнадцатеричных цифр.
В качестве идентификатора конструктора или типа можно также использовать составные конструкции вида <namespace_identifier>.<constructor_identifier>
и <namespace_identifier>.<Type_identifier>
.
При этом идентификатор, стоящий слева от точки, называется пространством имен (namespace). Правило про первую заглавную букву в идентификаторах типов и строчную букву в идентификаторах конструкторов распространяется при этом на часть конструкции после точки. Например, auth.Message
может быть типом, а auth.std_message
может быть конструктором.
Пространства имен не нуждаются в специальном объявлении.
Комментарии такие же, как в C++.
// built-in types
int#a8509bda ? = Int;
long ? = Long;
double ? = Double;
string ? = String;
null = Null;
vector {t:Type} # [ t ] = Vector t;
coupleInt {alpha:Type} int alpha = CoupleInt<alpha>;
coupleStr {gamma:Type} string gamma = CoupleStr gamma;
/* name of type variable is irrelevant: might replace 'gamma' with 'alpha';
however, combinator number will depend on the specific choice */
intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;
strHash {alpha:Type} (vector (coupleStr alpha)) = StrHash alpha;
intSortedHash {alpha:Type} intHash<alpha> = IntSortedHash<alpha>;
strSortedHash {alpha:Type} (strHash alpha) = StrSortedHash alpha;
// instantiating polymorphic types ('templates')
// outdated, ignored altogether
Vector int;
// with syntactic sugar as well:
Vector<string>;
Vector Object;
IntHash int;
IntHash string;
IntHash Object;
StrHash int;
StrHash string;
StrHash Object;
// custom types
pair x:Object y:Object = Pair;
triple x:Object y:Object z:Object = Triple;
user#d23c81a3 id:int first_name:string last_name:string = User;
no_user#c67599d1 id:int = User;
group id:int title:string last_name:string = Group;
no_group = Group;
---functions---
// maybe some built-in arithmetic functions; inverse quotes make 'identifiers'
// from arbitrary non-alphanumeric strings
`+` Int Int = Int;
`-` Int Int = Int;
`+` Double Double = Double;
// ...
// API functions (aka RPC functions)
getUser#b0f732d5 int = User;
getUsers#2d84d5f5 (Vector int) = Vector User;
В данном случае для конструктора user
явно задан номер (0xD23C81A3); в действительности можно было бы этого не делать, так как это значение есть CRC32 от строки 'user id:int first_name:string last_name:string = User'
, которое было бы использовано по умолчанию.
Специальные конструкторы для Vector int
, Vector User
, Vector Object
и т.д. не нужны — везде можно использовать один универсальный конструктор:
vector#1cb5c415 {t:Type} # [ t ] = Vector t;
Обратите внимание, что при вычислении номера конструктора getUsers (Vector int) = Vector User;
вычисляется CRC32 от строки getUsers Vector int = Vector User
, из которой удалены все скобки.
Запись T0<T1,T2,...,Tn>
является синтаксическим сахаром для (T0 T1 T2 ... Tn)
. Например, Vector<User>
и (Vector User)
полностью равнозначны.
Предположим, мы хотим запросить getUsers([2,3,4])
. Этот запрос будет сериализован следующим образом:
0x2d84d5f5 0x1cb5c415 0x3 0x2 0x3 0x4
Ответ на него может выглядеть примерно так (для наглядности добавлены переносы строк):
0x1cb5c415 0x3
0xd23c81a3
0x2 0x76615005 0x6c65
0x72754405 0x766f
0xc67599d1 0x3
0xd23c81a3
0x4 0x6b694e07 0x79616c6f
0x72754405 0x766f
Это примерно соответствует
[
{"id":2,"first_name":"Peter", "last_name":"Parker"},
{},
{"id":4,"first_name":"John","last_name":"Doe"}
]
Обратите внимание, что в обоих случаях используется один и тот же универсальный конструктор vector#1cb5c415
, в запросе для сериализации значения типа Vector int
, в ответе — типа Vector User
. Путаницы не происходит, так как в обоих случаях тип (де)сериализуемого значения известен до начала его десериализации. Например, сервер, получив запрос, видит, что первая его компонента — это 0x2D84D5F5
, что соответствует комбинатору getUsers#2d84d5f5 (Vector int) = Vector User
, поэтому понятно, что дальше будет следовать значение именно типа Vector int
. Клиент же, получив ответ на этот запрос, знает, что ему должны вернуть значение типа Vector User
, и десериализует его соответственно.