JetBrains MPS для интересующихся #2

в 20:14, , рубрики: java, jetbrains, language design, mps, programming, Программирование

Йо-хо-хо!

В прошлом посте мы остановились на том, что мы умеем добавлять массив входных погодных данных, а точнее данные "Время + температура", слегка попробовали использовать Behavior и разобрались с концептами.
Пришло время делать что-то полезное, ведь пока все, что мы реализовали, можно было реализовать на любом другом языке, за исключением прикольного синтаксиса.
Первым делом, введем ограничения на время. Сейчас мы ограничим его, чтобы часы были в пределе 0-24, а минуты 0-60, иначе будет выдаваться ошибка компиляции.

Constraints

Constraints это аспект языка, который отвечает за валидность реализации концепта. В нашем случае нам нужно ограничить property hours и minutes, поэтому мы создаем Constraints аспект концепта Time.
image
Здесь мы видим 3 пункта, которые отвечают за структуру AST.

  • can be child: получаем на вход данные об узле, родительском узле, дочернем и все, что только можно и решаем, может ли реализация концепта в данном контексте быть дочерней или нет
  • can be parent: то же самое, что и с child, только проверка на возможность быть родительским узлом
  • can be ancestor: все то же самое, что с parent, но более вложенно: в данном случае мы можем идти как угодно выше по AST, дословно — может ли узел быть предком

    Дальше мы можем определить какие то характеристики для properties. Это то, что нам нужно, и пока этого знать вполне достаточно, возиться с областью видимости нам пока не нужно.
    image
    Все, в принципе, просто и понятно: мы можем переопределить геттер и сеттер, но нас интересует is valid. В нем мы на вход получаем propertyValue и текущий node. У нас простое условие, так и пишем, как писали бы на Java.
    image
    Теперь если мы соберем язык и потыкаем в sandbox, то у нас должно показывать, если значение часа >= 24 или <0.
    image
    Написать реализацию для minutes не составит труда Вам самим.
    Так, здорово, работает. Теперь стоит попробовать перевести это на Java, ведь не зря же мы это все делали!
    Сначала добавляем аспект Generator и называем его main.
    image
    Пустовато, поэтому создадим новый root mapping rule. Выбираем в поле concept наш рутовый концепт PredictionList, а справа от стрелочки — нажимаем Alt + Enter -> New Root Template -> Java class. По логике должно получиться что-то вроде этого:
    image
    Если мы откроем map_PredictionList, то у нас будет что-то вроде Java класса с мета информацией над ним.
    image
    Это наш темплейт, шаблон и вообще самый главный отправной пункт. Но есть один момент: мы же не хотим преобразовывать наш WeatherTimedData в какие то примитивы, верно? Мы хотим чтобы у нас объект класса WeatherTimedData, но тут бамс! У нас же нет такого класса! Так что мы можем написать его вручную. Для этого создадим новый Solution, называем его, как хотим, и самое главное — добавляем в Used Languages jetbrains.mps.baseLanguage, чтобы мы могли написать наши классы.
    image

    У меня они получились такие:

image

image

image

image

Теперь нужно научиться использовать это в наших генерациях в Java код. Заходим в model properties генератора и добавляем в dependencies WeatherClasses и нажимаем export=true.
Теперь мы можем использовать эти классы в генерации, что мы сейчас и сделаем.

image

Выглядит как простой класс, но нам нужно добиться того, что name будет иметь значение имени PredictionList. Прогноз погоды для питера — там будет питер. Для Москвы — будет москоу. Нажимаем на строку "Here should be city" и юзаем хоткей Alt + Enter и в выпадающем списке выбираем property macro.

image

Суть очень простая — нам дается node типа PredictionList и мы должны вернуть строку. Нажимаем на знак доллара, который появился рядом со строкой и редактируем код в инспекторе.
image

Собираем проект, открываем наш Sandbox solution, где у нас есть только входные погодные данные для Санкт Петербурга, нажимаем ПКМ -> Preview Generated Text, и у нас откроется настоящая Java! Теперь мы можем ее не скринить, а спокойно скидывать копипастом.

package WeatherPrediction.sandbox;

/*Generated by MPS */

import WeatherClasses.structure.PredictionList;

public class PredictionListImpl extends PredictionList {

  /*package*/ String name = "Saint Petersburg";
}

Стоит заметить import statement, MPS сгенерировал импорт PredictionList, которые мы писали отдельным Solution. Такие solution можно называть "помогающими", "support solutions". Ну мне лично очень нравится их так называть.
Ну давайте добавим еще реализацию для массива входных данных.

image
Мы создаем пустой linkedlist(почему бы и нет), в котором мы будем хранить входные данные.
В конструкторе мы используем сразу 2 крутых макроса.
Макрос $LOOP$ делает следующее: он проходит по данной коллекции, и для каждого элемента выполняет что-то. В итоге получается массив сгенерированных данных, которые просто идут друг за другом. В данном случае мы итерируем по node.weatherData.items
Макрос $COPY_SRC$ используется, чтобы получить результат преобразования в другую модель какого-то концепта. В данный момент у нас есть только 1 шаблон: он "главный" и он является шаблоном для PredictionList, и откуда же MPS поймет, что и как делать..?
В такие моменты MPS смотрит в конфигурацию генератора main, а точнее в reduction rules, где у нас сейчас пусто.
image
Делаем абсолютно то же самое, что делали с PredictionList.
image
Переходим в reduce_WeatherTimedData, нажимаем Shift + Space, выбираем Expression. Теперь просто пишем

new WeatherTimedData(0, 0, null)

Теперь нужно завернуть этот Expression в TemplateFragment, чье содержимое как раз таки и будет использовано в нашем главном PredictionListImpl.
Выделяем весь expression с помощью хоткея ctrl + w, нажимаем Alt+Enter -> Create Template Fragment.
image
Теперь нам нужно заменить нули на property macro, которые мы уже умеем делать(нажимаем на 0, нажимаем Alt Enter -> Add property macro. В инспекторе пишем

(templateValue, genContext, node, operationContext)->int { 
  node.time.hours; 
}

что соотвествует замене 0 на актуальное значение часов. Проделываем то же самое с минутами, а null заменять не будем — мне сейчас лень, но идея та же — мы добавляем reduction rule для концепта Temperature, и вместо property macro используем макрос $COPY_SRC$
После чего заменяем 0 и 0 на property macro, которое мы уже умеем вызывать(Выделяем 0, Alt + Enter ->Add Property Macro -> node.time.minutes).
Собираем язык и идем в Sandbox solution.
Итак, для исходного кода на языке Weather

Weather prediction rules for Saint Petersburg 
[ 0 : 23 ] { 
  temperature = 23.3 °C 
}  
[ 12 : 24 ] { 
  temperature = 100.0 °F 
}
[ 23 : 33 ] { 
  temperature = 4.4 °C 
}  

Получается такой Java код:

package WeatherPrediction.sandbox;

/*Generated by MPS */

import WeatherClasses.structure.PredictionList;
import java.util.Deque;
import WeatherClasses.structure.WeatherTimedData;
import jetbrains.mps.internal.collections.runtime.LinkedListSequence;
import java.util.LinkedList;

public class PredictionListImpl extends PredictionList {

  /*package*/ String name = "Saint Petersburg";
  /*package*/ Deque<WeatherTimedData> input = LinkedListSequence.fromLinkedListNew(new LinkedList<WeatherTimedData>());

  public PredictionListImpl() {
    LinkedListSequence.fromLinkedListNew(this.input).addElement(new WeatherTimedData(0, 23, null));
    LinkedListSequence.fromLinkedListNew(this.input).addElement(new WeatherTimedData(12, 24, null));
    LinkedListSequence.fromLinkedListNew(this.input).addElement(new WeatherTimedData(23, 33, null));
  }
}

Можно было бы заменить linkedlist на обычный массив, но это было бы сложнее, так как нужно было бы пихать индексы. Можно было бы заменить на ArrayList, но MPS первым посоветовал linkedlist. Можно было бы написать отдельный support-класс для WeatherData, а не добавлять данные в конструкторе, существует много способов сделать генерируемый код лучше. Или хуже. В общем, теперь мы умеем генерить (!)Java код из нашего языка. А вообще, теперь мы можем генерировать Java код для любого другого языка, ведь мы овладели знанием Generator!
Спасибо за внимание.

Автор: enchantinggg

Источник

Поделиться

* - обязательные к заполнению поля