LOQI
LOQI - это специально созданный для данного проекта текстовый язык записи данных, хранимых моделью предметной области.
Поскольку это кастомный язык, в блоках кода с его описанием будет использоваться подсветка для языка Dart - просто потому что она относительно неплохо подходит.
Пример записи на LOQI:
//класс "Питомец"
class Pet {
//"Возраст" - Объектное свойство данного класса
obj prop age: int;
}
//класс "Человек"
class Human {
//"Возраст" - Объектное свойство данного класса
obj prop age: int;
//"Имеет питомца" - отношение между объектами класса "Человек"
// и объектами класса "Питомец"
rel hasPet(Pet) : {1 -> *} ;
}
//Объект класса "Питомец", представляющий конкретного питомца "Мурзик"
obj murzik : Pet {
//Утверждение о значении свойства "Возраст" данного объекта
age = 2;
}
//Объект класса "Человек", представляющий конкретного человека "Алиса"
obj alice : Human {
//Утверждение о значении свойства "Возраст" данного объекта
age = 22;
//Утверждение о связи данного объекта
//с другим объектом "Мурзик" по отношению "имеет питомца"
hasPet(murzik);
}
LOQI был создан из-за трудности чтения RDF-записей, а также из-за их неспособности описать определения данных (в RDF записываются только утверждения).
Технически, можно было бы придумать, как записывать в RDF все те же данные, что и в LOQI, но это бы требовало большого усложнения и без того трудночитаемых конструкций.
Тем не менее, подобная необходимость еще может возникнуть, например, чтобы оптимизировать графовые операции над моделью предметной области.
Общая информация о языке
- LOQI написан на ANTLR4. В коде его грамматику можно посмотреть в файле
LoqiGrammar.g4
. - LOQI не имеет значимых пробелов (whitespaces) - это значит, что, в теории, любую модель можно записать в одну (длинную и сложночитаемую) строку
- Файлы LOQI имеют расширение
.loqi
. (Но, строго говоря, это ни на что не влияет - можете скормить в парсер хоть.txt
) - Т.к. LOQI был создан специально для представления модели предметной области в тексте, то он поддерживает весь функционал, предоставляемый моделью. Поэтому для хранения, обмена, и отладки модели рекомендуется использовать именно LOQI - т.к. он полностью отображает сохраняемую в модели информацию.
- Единственным (незначительным) исключением из этого правила являются метаданные - поскольку в коде в них можно положить произвольные значения (т.е. любые объекты). Запись метаданных в LOQI поддерживает только те типы данных, которые могут иметь свойства.
Работа с LOQI в коде
Работа с LOQI в коде представлена пакетом its.model.definition.loqi
.
По сути, LOQI представляет собой только механизм сериализации и десериализации модели DomainModel
.
Для десериализации (преобразование LOQI текста в объект DomainModel
) используйте класс DomainLoqiBuilder
, а точнее его статический метод DomainLoqiBuilder.buildDomain()
. Данный метод принимает объект Reader
, что позволяет считывать данные из произвольных мест - например, из строки в коде, из файла, или из ресурсов приложения.
Примеры использования:
//Чтение из строки
DomainModel modelA = DomainLoqiBuilder.buildDomain(new StringReader(loqiString));
//Чтение из файла
DomainModel modelB = DomainLoqiBuilder.buildDomain(new FileReader("domain.loqi"));
//Чтение из ресурсов приложения
DomainModel modelC = DomainLoqiBuilder.buildDomain(new InputStreamReader(
this.getClass().getClassLoader().getResource("domain.loqi").openStream()
));
Для сериализации (преобразование объекта DomainModel
в LOQI текст) используйте класс DomainLoqiWriter
, а точнее его статический метод DomainLoqiWriter.saveDomain()
.
Данный метод принимает саму модель DomainModel
, а также объект Writer
, что позволяет записывать данные в произвольные места: например, в отдельную строку-буфер или в файл.
Данный метод также принимает третий аргумент - набор так называемых опций записи. Они немного изменяют итоговый вид LOQI-текста (подробнее см. в коде), но эти изменения незначительны, и связаны с редко используемым функционалом, который мы рассмотрим в конце данной статьи.
В большинстве случаев третий аргумент стоит оставлять как пустой набор.
Примеры использования:
//Запись в строку
StringWriter stringWriter = new StringWriter();
DomainLoqiWriter.saveDomain(domainModel, stringWriter, Set.of());
String loqiString = stringWriter.toString();
//Запись в файл
DomainLoqiWriter.saveDomain(domainModel, new FileWriter("domain.loqi"), Set.of());
Далее в этой статье будут рассмотрены детали записи языка и его спецификация.
Комментарии
LOQI поддерживает однострочные (// ...
) и многострочные (/* ... */
) комментарии, которые не преобразовываются в модель, и нужны просто для человеческого понимания.
Идентификаторы (Имена)
Все сущности в LOQI имеют некоторое закрепленное за ними имя. Здесь и далее подобные имена будут технически называться идентификаторами.
Идентификатор может иметь две формы:
- Простой идентификатор - строка, соответствующая регулярному выражению
[a-zA-Z$_][a-zA-Z$_0-9]*
.- Подобные идентификаторы весьма распространены во многих языках программирования - это строка, содержащая буквы, цифры, а также знаки
$
и_
, но не начинающаяся с цифры. Имена переменных в Java придерживаются тех же правил. - Обратите внимание, что простой идентификатор не может пересекаться с ключевым словом LOQI. Например, в LOQI записи не может существовать идентификатор
true
или идентификаторclass
.
- Подобные идентификаторы весьма распространены во многих языках программирования - это строка, содержащая буквы, цифры, а также знаки
- Произвольный или экранированный идентификатор - строка, соответствующая регулярному выражению
`\W+`
- Т.е. это произвольная непустая строка без пробелов, окруженная символами
`
(символ backtick, бэктик) - Экранированные идентификаторы позволяют большую свободу в названиях ваших сущностей, в том числе допускаются пересечения с ключевыми словами - например, может существовать идентификатор
`true`
или идентификатор`class`
. - Обратите внимание, что при переводе в Java-объекты экранированные идентификаторы "разворачиваются": в коде строка, соответствующая LOQI идентификатору
`class`
, будет иметь просто значениеclass
. В связи с этим, например, идентификаторы`Name`
иName
будут эквивалентны.
- Т.е. это произвольная непустая строка без пробелов, окруженная символами
Это не жесткое правило синтаксиса LOQI, но разные идентификаторы стоит записывать по разному, для понятности.
Предлагаемые автором конвенции:
- Для классов:
PascalCase
- Для перечислений:
PascalCase
- Для значений перечислений:
MACRO_CASE
- Для объектов:
camelCase
- Для свойств и отношений:
camelCase
В различных местах языка также может упоминаться список идентификаторов. Чаще всего это подразумевает запись произвольного кол-во идентификаторов, разделенных запятой ,
(также допускается запятая в конце)
<идентификатор1> , <идентификатор2> ... [,]
Здесь и далее в подобных записях синтаксиса квадратными скобками []
обозначены необязательные участки.
Значения
Под значениями понимаются конкретные литералы различных типов, используемых в разных местах языка.
Запись в LOQI использует значения тех типов, которые могут иметь свойства.
Значения перечислений
Строка вида <идентификатор перечисления>:<идентификатор значения>
.
Примеры: State:EVALUATED
, Position:LEFT
.
Целые числа
Последовательность десятичных цифр, не начинающаяся с нуля (за исключением самого числа 0
).
Примеры: 0
, 9
, 1024
.
Дробные числа
- Целые числа с суффиксом
d
илиD
на конце. Примеры:0d
,9D
,1024d
- Десятичная дробь, разделенная точкой
.
. Целая часть может отсутствовать (считается равной нулю). Примеры:0.0
,0.05
,.05
,100.75
- Целые числа или десятичные дроби с экспоненциальной частью вида
e<число>
илиE<число>
. Примеры:1e7
,0.5E10
,5e-2
Булевы значения
true
или false
Строки
- Однострочный текст, обернутый в кавычки
"..."
либо одинарные кавычки'...'
- В строке допускаются эскейп-последовательности
\n
,\t
,\r
,\b
,\f
,\'
,\"
и\\
, а также неэкранированные кавычки "противоположного" типа
- В строке допускаются эскейп-последовательности
- Многострочный текст, обернутый в троекратные кавычки
"""..."""
либо троекратные одинарные кавычки'''...'''
- В строках допускаются эскейп-последовательности (см. выше), переносы строки, а также неэкранированные кавычки "противоположного" типа
Метаданные
Объявление метаданных в LOQI имеет одинаковый для разных сущностей синтаксис, следующего вида:
[
<свойство метаданных>*
]
Здесь и далее в подобных записях синтаксиса звездочкой `обозначено произвольное количество сооветствующих участков (включая 0).* ***Конкретно в данном случае квадратные скобки
[]` не обозначают необязательность, а являются элементом синтаксиса.***
<свойство метаданных>
имеет форму
[<идентификатор кода локализации> . ] <идентификатор свойства метаданных> = <значение> ;
Таким образом, пример записи метаданных может быть таким:
[
index = 0 ;
RU.localizedName = "операнд _Char_value на позиции 1" ;
EN.localizedName = "variable _Char_value at position 1" ;
]
В дальнейших секциях запись метаданных поясняться не будет, поскольку она везде такая, как это написано здесь.
Классы
Объявление класса в LOQI имеет следующий синтаксис:
class <идентификатор класса> [: <идентификатор родительского класса>]
[<тело класса>]
[<метаданные класса>]
<тело класса>
имеет форму
{
<данные о классе>*
}
При этом <данные о классе>
могут включать в себя:
Таким образом, пример записи класса может быть таким:
class Operator : Element {
//... данные о классе ...
} [
//... метаданные ...
]
Или таким:
class Element //и все!
Объекты
Объявление объекта в LOQI имеет следующий синтаксис:
obj <идентификатор объекта> : <идентификатор класса объекта>
[<тело объекта>]
[<метаданные объекта>]
<тело объекта>
имеет форму
{
<данные об объекте>*
}
При этом <данные об объекте>
могут включать в себя:
Таким образом, пример записи объекта может быть таким:
obj alice : Human {
//... данные об объекте ...
} [
//... метаданные ...
]
Или таким:
obj murzik : Pet //и все!
Отношения
Объявления отношений
Объявление отношения в LOQI имеет следующий синтаксис:
rel <идентификатор отношения>(<список идентификаторов классов объектов отношения>) [: <вид отношения>] [<метаданные отношения>] ;
<вид отношения>
указывает на соответствующую характеристику данного отношения, и может иметь две формы: <независимый вид отношения>
и <зависимый вид отношения>
В отсутствии указания вида отношения, оно считается независимым, не задающим шкалу, и не имеющим квантификатора.
<независимый вид отношения>
имеет следующий синтаксис:
<тип шкалы отношения>
ИЛИ
<квантификатор отношения>
ИЛИ
<тип шкалы отношения> <квантификатор отношения>
Здесь
<тип шкалы отношения>
это либоlinear
для обозначения линейной шкалы, либоpartial
для обозначения частичной шкалы. Подробнее про шкалы здесь<квантификатор отношения>
это строка вида{ subjCount -> objCount }
, где subjCount и objCount это либо целые числа, либо звездочка*
. Подробнее про квантификаторы здесь
<зависимый вид отношения>
имеет следующий синтаксис:
<тип зависимости> to <ссылка на основное отношение>
Здесь
<тип зависимости>
это одна из следующих строк, обозначающая соответствующий тип зависимости отношения от другого основного отношения:opposite
transitive
between
closer
further
<ссылка на основное отношение>
может иметь две формы:- Идентификатор отношения, если ссылаемся на отношение в том же классе
- Строка вида
<идентификатор класса> -> <идентификатор отношения>
, если ссылаемся на отношение в другом классе
Примеры различных объявлений отношений:
//Независимое отношение со шкалой и квантификатором
rel isDirectlyLeftOf(Token) : linear {1 -> 1} [
//... метаданные ...
] ;
//Независимое отношение со шкалой
rel isOperandOf(Element) : partial ;
//Простое независимое отношение
rel belongsTo(Element) ;
//Зависимое отношение со ссылкой на отношение другого класса
rel has(Token) : opposite to Token->belongsTo ;
//Независимосе тернарное отношение (2 класса объектов)
rel isComplexOperandWith(Token, Token,) ;
Утверждения о связях по отношениям
Утверждение о связи объектов по отношению имеет следующий синтаксис:
<идентификатор отношения>(<список идентификаторов объектов отношения>) ;
Например:
//бинарная связь
belongsTo(element_1);
//тернарная связь
isComplexOperandWith(tokenA, tokenB);
Свойства
Объявления свойств
Объявление свойства в LOQI имеет следующий синтаксис:
<вид свойства> prop <идентификатор свойства> : <тип свойства> [<метаданные свойства>] ;
<вид свойства>
это либо class
для обозначения классового свойства, либо obj
для обозначения объектного свойства. Подробнее о видах свойств здесь
<тип свойства>
может иметь следующие формы:
<идентификатор перечисления>
- перечислимый типint <диапазон целых чисел>
- целочисленный тип<диапазон целых чисел>
показывает допустимые значения в данном типе, и имеет одну из следующих форм:- строка вида
[a, b]
, показывающая диапазон-промежуток от целого числаa
до целого числаb
, включительно. При этом какa
, так иb
могут быть опущены - в таком случае считается, что у промежутка нет верхней и нижней границы соответственно. - строка вида
{a, b, c ... }
, показывающая диапазон-множество конкретных допустимых значений, гдеa
,b
,c
и т.д. - целые числа
- строка вида
double <диапазон дробных чисел>
- дробночисленный тип<диапазон дробных чисел>
- аналогично<диапазон целых чисел>
выше, но указываемые значения могут быть как целыми, так и дробными.
bool
- тип булево значениеstring
- строковый тип
Примеры различных объявлений свойств:
//объектное свойство перечислимого типа, с метаданными
obj prop state : State [
//... метаданные ...
] ;
//классовое свойство целочисленного типа с диапазоном-множеством
class prop countOfTokens : int{1, 2} ;
//объектное свойство дробного типа с диапазоном-промежутком
obj prop probablity : double[0.0, 1.0]
Утверждения о значениях свойств
Утверждение о значении свойства в LOQI имеет следующий синтаксис:
<идентификатор свойства> = <значение> ;
Например:
countOfTokens = 2;
probablity = 0.7;
state = State:UNEVALUATED;
Одновременное объявление и утверждение
В случае классовых свойств для краткости записи можно также одновременно и объявить свойство, и задать ему значение для текущего класса.
Подобная запись имеет следующий синтаксис:
<вид свойства> prop <идентификатор свойства> [: <тип свойства>] = <значение> [<метаданные своства>] ;
Например следующая запись:
class prop countOfTokens : int{1, 2} = 2;
Эквивалентна следующим:
class prop countOfTokens : int{1, 2} ;
countOfTokens = 2;
Также, как видно по синтаксису, тип свойства в данном случае можно опустить - он определяется на основе присваемого значения (но для числовых типов диапазон определяться не будет).
Например:
//свойство имеет тип int, определенный по значению
class prop countOfTokens = 2;
Перечисления (Enum)
Объявление перечисления в LOQI имеет следующий синтаксис:
enum <идентификатор перечисления>
[<тело перечисления>]
[<метаданные перечисления>]
<тело перечисления>
имеет следующую форму:
{
<список значений перечисления>
}
Здесь <список значений перечисления>
подразумевает запись произвольного кол-во значений перечисления, разделенных запятой ,
(также допускается запятая в конце)
<значение перечисления 1> , <значение перечисления 2> ... [,]
<значение перечисления>
имеет следующий синтаксис:
<идентификатор значения перечисления> [<метаданные значения перечисления>]
Таким образом, пример записи перечисления может быть таким:
enum State {
UNEVALUATED [
//... метаданные ...
],
EVALUATED [
//... метаданные ...
],
USED [
//... метаданные ...
],
OMITTED [
//... метаданные ...
],
} [
//... метаданные ...
]
Или таким:
enum State { UNEVALUATED, EVALUATED, USED, OMITTED }
Или таким:
enum State
(хотя не особо понятно, зачем может пригодиться перечисление без значений)
Свободные данные
"Свободные" данные - это концепт, который был придуман для возможности хранить метаданные и значения свойств отдельно от их определений - например, метаданные можно было бы выделить в отдельный файл с локализацией, а значения свойств - в теги.
С момента их ввода уже появилась возможность объединять несколько различных моделей, что по сути приводит к тем же возможностям, в связи с чем этот функционал практически не используется.
(Но справедливости ради, "свободные данные" позволяют разделить информацию даже внутри одного LOQI файла, в то время как объединение моделей этого не позволяет)
Тем не менее, данный функционал присутствует в системе, поэтому для полноты он будет описан.
Свободные метаданные
Свободные метаданные - это возможность определить метаданные сущности отдельно от ее основного объявления.
Эта конструкция имеет следующий синтаксис:
meta for <ссылка на сущность> <метаданные для сущности>
Здесь <ссылка на сущность>
может иметь следующие формы:
[obj] <идентификатор объекта>
для ссылки на объектclass <идентификатор класса>
для ссылки на класс<идентификатор класса>.<идентификатор свойства>
для ссылки на свойство<идентификатор класса> -> <идентификатор отношения>
для ссылки на отношениеenum <идентификатор перечисления>
для ссылки на перечисление<идентификатор перечисления>:<идентификатор значения>
для ссылки на значение перечисления
Например:
meta for State:EVALUATED [
RU.localizedName = "вычислен" ;
EN.localizedName = "evaluated" ;
]
Свободные значения свойств классов
Свободные значения свойств классов - это возможность задать значения свойствам класса отдельно от его основного определения.
Эта конструкция имеет следующий синтаксис:
values for class <идентификатор класса> {
<утверждения о значении свойства>*
}
Например:
values for class `operator_&&` {
arity = Arity:BINARY ;
precedence = 14 ;
associativity = Associativity:LEFT ;
needsLeftOperand = true ;
needsRightOperand = true ;
needsInnerOperand = false ;
countOfTokens = 1 ;
}