Статьи

Разработка предметно-ориентированного языка в Gremlin

Первоначально автор Стивен Маллетт

Разработка DSL на основе ГремлинСпецифичные для домена языки (DSL) предоставляют программистам возможность повысить выразительность своего кода в конкретном домене . Это позволяет разработчику повысить производительность, скрывая низкоуровневые программные конструкции в пользу высокоуровневых, которые лучше связывают с элементами самого домена. Преимущества DSL также заключаются в том, что непрограммисты могут «говорить» на том же языке, что и их коллеги по программированию, тем самым снижая технические коммуникационные барьеры.

OpenClass образовательный DSL Пирсона

В более раннем посте Аврелия, озаглавленном « Обучение планете с Пирсоном », говорилось о платформе OpenClass и роли Титана в Пирсоне.цель «дать образование кому угодно и где угодно на планете». Он описал пространство образовательного домена и предоставил высокоуровневое объяснение некоторых концептуальных сущностей и типов отношений в графе. Например, график моделировал студентов, записывающихся на курсы, людей, обсуждающих контент, концепции ссылок на контент и другие сущности, относящиеся друг к другу по-разному. Размышляя в терминологии графа, эти «концептуальные сущности и типы отношений» выражаются в виде вершин (например, точек, узлов) и ребер (например, линий, отношений), поэтому, по сути, модель предметной области встраивает концептуальное значение в элементы графа.

Домен над графикомВ Pearson модель предметной области OpenClass расширена до программной конструкции, DSL, основанной на Gremlin , которая абстрагирует язык графа. Инженеры и аналитики могут задавать вопросы графика на языке своего образовательного предмета, в отличие от перевода этих знакомых терминов на язык вершин и ребер. OpenClass DSL определяет схему графа, расширяет язык обхода графа Gremlin на язык обучения, предоставляет автономные функции, которые работают с этими расширениями, и предоставляет алгоритмы, разработанные на основе этих расширений и функций. Вместе эти компоненты образуют крупнозернистый API, который помогает обеспечить общий доступ к сложным обходам графов .

Расширение Гремлин в DSL

Gremlin — это основанный на Groovy DSL для обхода графов свойств, который применим в областях запросов, анализа и манипулирования графами . Он обеспечивает интуитивно понятный способ не только думать в терминах графа, но и программировать в терминах одного. Одним из интересных свойств Gremlin является то, что он позволяет программистам расширять язык для еще большей программной выразительности не только внутри графов, но и в той области, в которой находится сам граф.

Человек Написал Пост МодельВажным аспектом графика Пирсона является понятие «люди, обсуждающие контент». Для целей следующих примеров представьте «контент» как интерактивный дискуссионный форум, где преподаватель назначил темы для студентов, чтобы они могли обсудить их и открыть дебаты. A personзаписывает, postчто может быть связано с другим, postкоторый был написан ранее.

Пересекающиеся обсуждения в терминологии графов

Обход графаУчитывая структуру графа «люди обсуждают контент», новичок из Гремлин может сразу начать навигацию по графе. Запрашивая у графа список всех вершин с ключом свойства typeи значением, postвыдает список постов в графе. Гремлин по такому запросу ниже:

g.V.has(
'type'
,
'post'
)

Обход становится немного более сложным, когда необходимо пройти глубину дерева постов:

g.V.has(
'type'
,
'post'
).out(
'child'
).loop(
1
){it.loops<
25
}{true}

Чтобы проанализировать и сравнить потоки сообщений в дереве, необходимо проанализировать путь каждого потока, так чтобы каждый поток был сведен в a Map, где ключом является userNameпользователь, который написал первый postв потоке, а значение — уникальный список вершин для потоков, которые начал пользователь:

m=[:]
g.V.has('type','post').out('child').loop(1){it.loops<25}{true}
   .path.groupBy(m){it[0].userName}{it}{it.collectMany{it}.unique()}.iterate()

Оценка mпосле выполнения обхода даст postвершины, расположенные следующим образом:

gremlin> m
==>marko=[v[184476380], v[282106584], v[184550536], v[189966816]]
==>josh=[v[173318448], v[188571048]]
==>daniel=[v[186130596], v[308964172]]
...
==>stephen=[v[176281532], v[182440524], v[188572948], v[282049412]]

Не важно понимать механику Гремлин выше. Его цель — продемонстрировать некоторую путаницу в том смысле, что даже эксперту из Гремлин, возможно, потребуется время, чтобы разобраться в том, что делает этот код. Подумайте на мгновение о том, сколько придется узнать незнакомому с графиками человеку, чтобы получить набор сглаженных веток сообщений в этом формате. Ценность DSL внезапно становится очевидной.

Обсуждение в терминологии предметной области

Обход доменаРазработка DSL может начинаться с автономного скрипта Groovy, на который можно ссылаться при запуске REPL Gremln или инициализировать на Rexster или Titan Server через init-scriptэлемент конфигурации rexster.xml. В случае OpenClass, DSL развился далеко за рамки потребностей ранней разработки, которые удовлетворяет «сценарий», и теперь проектируется как проект на основе Maven, развернутый в виде отдельного файла JAR .

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

class S {
  public static final String EDGE_CHILD = "child"
  public static final String PROPERTY_POST_ID = "postId"
  public static final String PROPERTY_TYPE = "type"
  public static final String TYPE_POST = "post"
}

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

Сначала определите класс, который будет отвечать за содержание определений шагов и за инициализацию их в Gremlin:

class Steps {
  def load() {
    // this method will call methods that will initialize each step definition.
    // from the Gremlin REPL or other code base that utilizes the steps, simply
    // call new Steps().load() to make the steps available. 
  }
}

С этим Stepsклассом можно добавить определение первого шага для инкапсуляции postфильтрации:

class Steps {
  def load() {
    defineStepPost()
  }
 
  private def defineStepPost() {
    Gremlin.defineStep('post', [Vertex, Pipe], { _().has(S.PROPERTY_TYPE, S.TYPE_POST) })
  }
}

Включение этого шага упрощает три оператора Gremlin, написанные в предыдущем разделе, для:

g.V.post
 
g.V.post.out('child').loop(1){it.loops<25}{true}
 
m=[:]
g.V.post.out('child').loop(1){it.loops<25}{true}
   .path.groupBy(m){it[0].userName}{it}{it.collectMany{it}.unique()}.iterate()

Выпускник ГремлинpostШаг заменяет использование has(S.PROPERTY_TYPE, S.TYPE_POST). Это изменение не делает код намного более читабельным, но это только начало. В продолжение примера включены два дополнительных шага: один для обхода дерева postвершин и один для выравнивания каждого потока (или пути обсуждения):

class Steps {
  public static final int CONTROL_MAX_DEPTH = 25
  def load() {   
    defineStepPost()
    defineStepPostTree()
    defineStepFlattenThread()
  }
 
  private def defineStepPost() {
    Gremlin.defineStep('post', [Vertex, Pipe], { _().has(S.PROPERTY_TYPE, S.TYPE_POST) })
  }
 
  private def defineStepPostTree() {
    Gremlin.defineStep('postTree', [Vertex, Pipe], { depth = CONTROL_MAX_DEPTH ->
            _().post.out(S.EDGE_CHILD).loop(1){it.loops<depth}{true}
    })
  }
 
  private def defineStepFlattenThread() {
    // the addition of .transform{it[0]}.dedup to the end of this Gremlin statement
    // makes flattenThread a pure side-effect in that it converts the output back to
    // the original vertices passed in.
    Gremlin.defineStep('flattenThread', [Vertex, Pipe], { m, depth = CONTROL_MAX_DEPTH, keyOn = null ->
            _().postTree(depth).path.groupBy(m){keyOn == null ? it[0] : keyOn(it[0])}{it}
            {it.collectMany{it}.unique()}.transform{it[0]}.dedup
    })
  }
}

Добавление этих шагов упрощает обход и расширяет их гибкость:

g.V.post
 
// traverses to the default depth of 25
g.V.postTree
 
// traverse to the assigned depth of 256
g.V.postTree(256)
 
m=[:];g.V.flattenThread(m).iterate()
 
// traverse to depth 256
m=[:];g.V.flattenThread(m, 256).iterate()
 
// traverse to depth 256, key the Map on the postId of the root vertex instead of the vertex itself
m=[:];g.V.flattenThread(m, 256, {it.getProperty(PROPERTY_POST_ID)}).iterate()

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

Шаблоны разработки DSL

В приведенном ниже списке представлены рекомендуемые шаблоны, которым необходимо следовать при создании DSL с Gremlin:

  • Централизуйте имена свойств, метки ребер и другие строковые значения как глобальные переменные. Не вставляйте строковые литералы в DSL.
  • Включите класс схемы с некоторой функцией инициализации, которая принимает экземпляр Blueprints Graph в качестве аргумента и настраивает индексы графа. Схема класс особенно важен при использовании Titan и его типа определений .
  • Автономные скрипты Groovy являются лишь отправной точкой для DSL. Эти сценарии быстро усложняются и становятся неуправляемыми. Рассматривайте DSL как свой собственный проект. Используйте зависимость систему управление и сборок , как Maven или Gradle для плодоовощного скомпилированного кода , который можно ссылаться в других проектах, запряженный в Gremlin РЕПЛ с виноградом или скопированным на путь Rexster или Titan сервера и сконфигурированный для использования с importsи static-importsнастройкой. Обратите внимание, что прямая поддержка Grape в REPL будет заменена в Gremlin 2.5.0 Gremlin.use()функцией, которая оборачивает Grape и выполняет аналогичную функцию.
  • При развертывании DSL на Rexster или Titan Server код на стороне клиента больше не должен передавать длинные сложные скрипты Gremlin в виде строк для удаленного выполнения через REST или RexPro . Клиентские приложения могут просто вызывать параметризованные функции из DSL, выставленного на сервере.
  • Пишите тесты . Используйте вышеупомянутый класс схемы для «настройки» Graphэкземпляра при каждом запуске тестов, чтобы убедиться, что изменения в схеме не вызывают проблем и что новые дополнения к DSL будут правильно работать в рабочем графе с учетом этой конфигурации. Используйте TinkerGraph для облегчения тестирования DSL-операций.
  • Напишите встроенную документацию для схемы, пользовательских шагов и других функций, но не используйте Javadoc . Используйте такой инструмент, как Groc , который обрабатывает форматированный текст Markdown и создает документацию, включающую исходный код.
  • Разработайте компоненты DSL как компонуемые блоки, чтобы один или несколько блоков могли использоваться вместе для операций даже более высокого уровня. Когда это возможно, обдумайте в общих чертах и ​​спроектируйте функции, которые можно изменить во время их вызова посредством параметризации с настройками и замыканиями.
  • DSL — это не просто расширение Gremlin с помощью пользовательских шагов. Используйте язык Groovy и напишите автономные функции, которые работают с графом, внутри пользовательских шагов или в любом месте, где имеет смысл инкапсулировать функциональность графа.
  • Используйте IDE , например, IntelliJ . Поскольку Gremlin — это Groovy, такие функции IDE, как проверка синтаксиса и завершенность кода, помогают сделать Gremlin более продуктивным.

Вывод

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

OpenClass DSL от Pearson продолжает расширяться, позволяя реализовать следующие преимущества:

  • Вся логика, связанная с графиком, централизована в DSL, обеспечивая стандартный интерфейс для любой части организации, которая хочет получить доступ к графику для получения информации.
  • Непрограммисты используют DSL в своей работе, так как в них меньше языка «Gremlin-graph» и больше языка «Education-Graph».
  • Специальный анализ графика имеет тенденцию быть менее подверженным ошибкам и более продуктивным, так как функции высшего порядка в DSL являются проверенными версиями обычных и иногда мирских обходов (например, обход дерева).
  • Интересные и непреднамеренные открытия происходят при исследовании графа путем смешивания и сопоставления функций DSL.

Внедрение DSL поверх Gremlin будет полезно для проектов любого размера, но быстро станет требованием по мере того, как возрастает сложность концептуальной модели домена. Инвестирование в DSL, чтобы сделать его ключевым компонентом стратегии графического проектирования, следует рассматривать в качестве общего шаблона для производства Gremlin в технологических стеках TinkerPop и Aurelius.

Подтверждения

Доктор Марко А. Родригес прочитал черновые версии этого поста и предоставил полезные комментарии.