Предыстория

Велась как-то работа на одним движком для создания GUI, некоторым подобием Qt. Выделялся он тем, что конечный пользователь мог этот интерфейс кастомизировать с помощью конфига до самых мелочей.

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

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

Начало

Формат получил название IEML. Он частично вдохновлен YAML, поэтому стоит ожидать его частые упоминания в статье.

К нему был предъявлен ряд требований:

  • По максимуму сохранить визуальную простоту YAML

  • Быть строгим (не допускать отклонений от синтаксиса)

  • Быть явным (не иметь синтаксически пересекающихся типов данных)

  • Быть единообразным

  • Быть по минимуму избыточным

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

Формат

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

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

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

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

Были сохранены почти все скалярные типы данных из YAML и добавлен 1 новый:

  • Булевые значения

  • Целые и дробные числа

  • Строки

  • Null

  • Сырые данные

У каждого из этих типов появились свои особенности и отклонения от их традиционного написания.

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

Помимо возможностей и нескалярных типов YAML, образовалось два новых:

  • Списки

  • Словари

  • Якоря

  • Тэги

  • Файлы

Комментарии

Что бы не терять возможности добавления собственного типа для команд или файловых путей (Это мы обсудим позже) было решено обозначать комментарии с помощью #, но снова, что бы не терять возможности добавить тип hex-цвета, обязательным после этого должен идти или ! (Для возможности шебанга).

В итоге комментарии выглядят так:

#!Это комментарий
# Это комментарий
#А это уже ошибка

Булевые значения

Несмотря на практически однозначную распространенность true\false, выбор пал на yes\no.

Это проще для обывателя, это более точно подходит в большинстве контекстов, это короче.

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

Числа

Любое число должно содержать хотя бы одну цифру в целой части:

1 # 1 
1.0 # 1.0
1. # 1.0
.1 # Ошибка

Любое число содержащее . в мантиссе считается дробным. Да, числа поддерживают экпоненциальную запись, которая записывается с помощью буквы e .

Помимо этого стандарт позволяет записывать числа в любой системе счисления от 1 до 36. Для этого используется символ ' перед которым пишется система (в десятичной системе счисления), а после - число. Например:

2'101

Является числом 101 в двоичной системе.

Строки

Формат позволяет записать строку 3 различными способами:

  • Классический с использованием " и поддержкой экранирования

  • Не экранируемый с использованием >> и его короткая запись в одну строку с >

Оба способа поддерживают перенос на следующую строку, но требуют соблюдения уровня отступа.

Примеры:

"Hello Wolrd!
  " # "Hello World!\n "
>> # Здесь можно писать комментарии.
Hello World! # А вот это уже не комментарий, что бы строка кончилась должен быть понижен уровень отступа.

Null

Записывается с помощью null, тут все обыденно.

Сырые данные

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

Нужны, что бы вводить новые типы данных, например, hex-цвета, которые уже упоминались ранее:

#FFFF00

Списки

Требуют перед каждым элементом - . Например:

- null
- -10

Помимо этого присутствует короткая запись с помощью [] и с элементами через , , но список вложенных типов ограничен. (Нельзя использовать многострочные типы или типы поглощающую строку документа до конца)

[null, -10]

Словари

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

Ключ и значение разделяются : .

first: null
second: -1

Тэги

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

Тэг всегда начинается с = и заканчивается : , пишется перед значением, которому присваивает тэг.

- = tag: null
- 
	first: = other tag: -15

Якоря

YAML развил эту концепцию не до конца.

Обозначаются они также с помощью & для взятия и * для получения, но в отличии от YAML они способны находиться в любом порядке и поэтому затенение в одном файле недопустимо.

- *anchor # Здесь тоже находится "Hello"
- &anchor > Hello

И это еще не все.

Файлы

Формат предоставляет механизм по включению файлов. Это позволяет бить большие файлы на меньшие, тем самым делая чтение проще.

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

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

Что бы включить файл необходимо написать < , затем путь к файлу, но без расширения, расширения файлов фиксированы, поэтому необходимость их указывать отпала. Если нужно передать якоря, то сразу после включения файла на соответствующем уровне отступа пишется словарь, в котором имена ключей являются именами якорей, а значения - содержимым якорей.

Обычное включение файла:

< path/file-name

Включение файла с передачей якорей:

< path/file-name
first-anchor: null
second-anchor: -15

Сравнение

IEML

YAML

JSON5

TOML

Числа в различных системах исчисления

+

+

+

-

Числа в любых системах исчисления

+

-

-

-

Null

+

+

-

-

Неэкранируемые строки

+

-

-

+

Тэги

+

?

-

-

Якоря

+

+

-

-

Файлы

+

-

-

-

Полноценная графовая структура документа

+

-

-

-

Единообразность

+

-

+

-

Явность

+

-

+

+

Высокая скорость работы

-

-

+

-

Легковесность

-

-

+

-

Тип Дата и время

-

+

-

+

Реализации

На данный момент существует всего одна реализация на C++, но уже пишется реализация для Rust. В будущем планируется добавить реализацию для PHP. Но остальные языки - надежда лишь на энтузиастов из комьюнити.

Итог

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

Комментарии (20)


  1. BugM
    03.07.2023 21:29
    +8

    Чтобы что?


    1. loginoffvich
      03.07.2023 21:29
      +2

      больше форматов новых и разных


    1. hedgeberry Автор
      03.07.2023 21:29

      Написал, YAML нам не хватило. Но раз уж делать свой формат, то стоит постараться.


  1. Hardcoin
    03.07.2023 21:29
    +2

    Это проще для обывателя

    А что за обыватели у вас в качестве целевой аудитории? Они знают про null или 36-ричную систему исчисления?

    И что случилось с датой-временем?


    1. ermouth
      03.07.2023 21:29

      или 36-ричную систему исчисления?

      На мой вкус это тут наименее спорная часть, удобно же.

      36-ричная система вполне себе ОК в некоторых специфичных случаях, напр Date.now().toString(36) даёт таймстамп как ascii-строку в 8 символов (до 2059 года). Это иногда лучше подходит, чем число – в localStorage больше поместится если туда таймсерии например кладутся, или как часть составного ключа в БД, или в json если сериализуется массово, а надо место экономить.


      1. masai
        03.07.2023 21:29

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


        1. ermouth
          03.07.2023 21:29
          +1

          формат сериализации <...> это просто строка. Для пользователя не важно, что это именно число в 36-ричной системе

          Да, это представление. Если пользователю удобнее записывать 41215 как 16'a0ff (или 0xa0ff), почему бы не дать ему такую возможность? Если по пути используется js-парсер, почему бы не расширить на все поддерживаемые в js из коробки основания (2…36)?

          Мой поинт был не конкретно про основание 36, а про саму фичу, которая легко реализуется и точно имеет применение, хотя и очень ограниченное.


          1. masai
            03.07.2023 21:29

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


            1. ermouth
              03.07.2023 21:29

              где экзотические системы счисления нужны, всё же очень узкая

              Конечно. Но я двумя руками за подход, когда кроме decimal нужен ещё и hex, и по пути приходит в голову мысль, что можно же не только основания 10 и 16 делать, а сразу 2…36, и это ничего не будет стоить сверху.

              По мне, вполне рациональный подход для нового формата – синтаксическая мелочь, не стоит ничего в смысле затрат на кодинг, но вдруг кому пригодится.


  1. dopusteam
    03.07.2023 21:29
    +3

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

    Вот тут не хватает подробностей


    1. hedgeberry Автор
      03.07.2023 21:29

      Файлы выходили большимии, в них было много повторящихся шаблонных фрагментов.


      1. BugM
        03.07.2023 21:29

        Шаблонизатор логично делать отдельно от языка. Jinja2 отлично работает с любым языком. И понятный при этом.


        1. hedgeberry Автор
          03.07.2023 21:29

          В теории у внешнего шаблонизатора 0 обработанной информации о структуре документа. Если писать его простым путем, то он будет работать в том числе в неэкранируемых строках, что крайне нежелательно. А найти эти строки без структуры документа крайне сложно, так как последовательность >> может встречаться еще и внутри комментариев и других строк.


          1. BugM
            03.07.2023 21:29
            +1

            И отлично. Нужно человеку собрать строку из чего-то пусть собирает.

            Шаблонизатор не думает про структуру, он собирает файлик. Который потом логично чем-то провалидировпть.


  1. ya_ne_znau
    03.07.2023 21:29
    +1


  1. masai
    03.07.2023 21:29

    Рассмотрим декларируемые преимущества перед YAML:

    • Числа в любых системах счисления. — Не уверен, что это очень востребованная возможность. Можно добавить в YAML как пользовательский тип.

    • Неэкранируемые строки. — Если речь про строковые блоки, то они поддерживаются в YAML. Даже синтаксис почти такой же.

    • Тэги. — Не очень понял, для чего они нужны. Вроде бы пользовательские типы YAML могут их заменить для юзкейсов, которые я могу представить.

    • Файлы. — Да, этого нет (но можно добавить как пользовательский тип на уровне парсера). Вообще, есть причина, почему это не поддерживается. Просто если добавлять такой синтаксис, то нужно добавлять абстракцию файлов (а лучше URI), делать возможность загрузки данных, а это делает парсер уязвимым, а время обработки неопределённым.

    • Полноценная графовая структура. — Не понял, что имеется в виду.

    • Легковесность. — Субъективно.

    • Скорость работы. — Зависит от реализации, к формату не относится.


    1. hedgeberry Автор
      03.07.2023 21:29
      -2

      • Да, но это уже обсуждалось в другой цепочке комментариев, можете почитать.

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

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

      • Файлы встроенны в формат для интеграции с якорями, крайне сложно ввести файлы отдельно от языка, что бы они могли работать вместе с якорями, обычно парсеры не дают доступа к подобной информации. Насчет безопасности - в тот же парсер на C++ встроенна точка расширения, за счет которйо можно подменить код загружающий файлы как вам нужно.

      • За счет якорей можно описывать полноценные графы, например можно сделать цикл из узлов ссылающихся друг на друга.

      • Это в минусах формата, он достаточно плохо сжимается по размеру файла.

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

      Но также в преимуществах перед YAML отсутвие Norway Problem и синтаксическая строгость.


      1. masai
        03.07.2023 21:29

        в другой цепочке комментариев, можете почитать

        Я один из участников того обсуждения. :)

        В любом случае, это несложно реализуется в YAML.

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

        В блочных скалярах YAML нет спецсимволов и экранирования.

        Например,

        example: |
          First \n line,
          "second" line

        распарсится как

        {
          "example": "First \\n line,\n\"second\" line\n"
        }

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

        Теги есть в YAML. Их часто используют для создания кастомных типов.

        Например, можно объявить свой тип !color и использовать его так:

        background: !color {r: 47, g: 36, b: 66}

        Парсер может преобразовать цвет в какое-то внутреннее преставление и сразу выдать его.

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

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

        Да, это так, но это решается введением специальных тегов для таких якорей и для импорта.

        Честно говоря, идея глобального пространства имён для многих файлов выглядит не очень. Я бы решал задачу объединения конфигов на более высоком уровне абстракции.

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

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

        Так в любом формате это можно сделать. Разве нет? Можете пример привести?

        Но также в преимуществах перед YAML отсутвие Norway Problem и синтаксическая строгость.

        Norway problem решается линтером (и её нет, например, в Strict YAML).

        Про синтаксическую строгость не понял. Например, JSON и YAML синтаксически строги в том смысле, что есть стандарты, описывающие их синтаксис.


  1. Homyakin
    03.07.2023 21:29
    +1

    Самый непонятный выбор для меня это yes/no в качестве true/false из-за ЦА, потому что по остальным фичам формат явно не только для обывателей.

    А так как я не являюсь экспертом в различных форматах, то не хватило в статье прямых сравнений с другими форматами. Например, почему строки у вас реализованы именно так, чем варианты TOML или YAML хуже.


    1. hedgeberry Автор
      03.07.2023 21:29

      Можете уточнить про строки, их как-никак 3 вида.