LOQI

LOQI - это специально созданный для данного проекта текстовый язык записи данных, хранимых моделью предметной области.

Note

Поскольку это кастомный язык, в блоках кода с его описанием будет использоваться подсветка для языка 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 записываются только утверждения).

Note

Технически, можно было бы придумать, как записывать в 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, что позволяет записывать данные в произвольные места: например, в отдельную строку-буфер или в файл.

Note

Данный метод также принимает третий аргумент - набор так называемых опций записи. Они немного изменяют итоговый вид 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 будут эквивалентны.
Note

Это не жесткое правило синтаксиса LOQI, но разные идентификаторы стоит записывать по разному, для понятности.
Предлагаемые автором конвенции:

В различных местах языка также может упоминаться список идентификаторов. Чаще всего это подразумевает запись произвольного кол-во идентификаторов, разделенных запятой , (также допускается запятая в конце)

<идентификатор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 ;
}