Статьи

Домен-специфический язык для манипуляции с юнитами

Специфичные для предметной области языки — это горячая тема, и такие языки, как Groovy и Ruby, популяризируются благодаря их гибкому синтаксису, что делает их подходящими для этой цели. В частности, Groovy позволяет создавать внутренние DSL : бизнес-языки, размещаемые в Groovy. В недавней исследовательской работе Tiago Antão решил использовать Groovy для моделирования устойчивости к лекарствам против малярии . В двух сообщениях в блоге Тьяго рассказывает о некоторых тактиках, которые он использовал , и о том, как их объединить.создать мини-язык для исследований, связанных со здоровьем. В этой работе ему нужно было представить количество лекарственного средства, например 300 миллиграмм хлорохинина, лекарства, используемого против маралии. Groovy позволяет добавлять свойства к числам, и вы можете представлять такие количества просто 300.mg. Вдохновленная этой идеей, цель этой статьи — изучить, как создать мини-DSL для управления мерами и единицами, используя библиотеку JScience .

Прежде всего, поговорим о JScience. JScience — это библиотека Java, использующая обобщенные методы для представления различных измеримых величин. JScience также является эталонной реализацией для JSR-275: javax.measure. * . Будь то для измерения массы, длины, времени, ампер или вольт (и многих других), вычисления, которые вы можете сделать, являются типобезопасными и проверяются во время компиляции: вы не можете добавить секунду к килограмму, ваша программа не будет компиляции. Это определенно одна из сильных сторон библиотеки. Как бы ни была свободна библиотека, нотация, используемая для представления количества некоторой единицы, все еще не так удобна для чтения, как мог бы пожелать ученый.

Как вы представляете массу с JScience?

    import static javax.measure.unit.SI.*;
import javax.measure.*
import org.jscience.physics.amount.*;

// ...

Amount m3 = Amount.valueOf(3, KILO(GRAM));
Amount m2 = Amount.valueOf("2 kg");
Amount sum = m3.plus(m2);

Первое выражение использует статический импорт для представления КИЛО ( ГРЫ блока), в то время как вторые просто разбирает массу из строки. Последняя строка делает просто сложение между двумя массами. Тем не менее, это не похоже на то, что написал бы физик. Разве мы не хотим использовать математическую запись, как 3 kg + 2 kg? Посмотрим, как это можно сделать в Groovy.

Нашим первым шагом будет добавление единиц к числам. Мы не можем писать 2 kg, так как это не действительно Groovy, вместо этого мы напишем 2.kg. Для этого мы добавим некоторые динамические свойства к числам благодаря механизму ExpandoMetaClass .

    import javax.measure.unit.*
import org.jscience.physics.amount.*

// Allow ExpandoMetaClass to traverse class hierarchies
// That way, properties added to Number will also be available for Integer or BigDecimal, etc.
ExpandoMetaClass.enableGlobally()

// transform number properties into an mount of a given unit represented by the property
Number.metaClass.getProperty = { String symbol -> Amount.valueOf(delegate, Unit.valueOf(symbol)) }

// sample units
println( 2.kg )
println( 3.m )
println( 4.5.in )

Посмотрите, как мы создали килограммы, метры и дюймы? «Метакласс» — это то, что представляет поведение класса во время выполнения. При назначении замыкания свойству getProperty все запросы свойств на Numbers будут привязаны к этому замыканию. Это закрытие затем использует классы JScience для создания модуля и Amout. Переменная делегата, которую вы видите в этом замыкании, представляет текущий номер, к которому обращаются свойства.

Хорошо, хорошо, но в какой-то момент вам нужно умножить эти суммы на некоторый коэффициент, или вы захотите добавить к длинам вместе. Поэтому нам нужно использовать перегрузку операторов Groovy для выполнения некоторых арифметических операций. Если у вас есть такие методы, как multiply (), plus (), minus (), div () или power (), Groovy позволит вам использовать операторы *, +, -, / или **. Некоторые соглашения для некоторых из этих операций немного отличаются от соглашений Groovy, для некоторых из этих операций мы должны добавить несколько новых операторных методов:

    // define opeartor overloading, as JScience doesn't use the same operation names as Groovy
Amount.metaClass.multiply = { Number factor -> delegate.times(factor) }
Number.metaClass.multiply = { Amount amount -> amount.times(delegate) }
Number.metaClass.div = { Amount amount -> amount.inverse().times(delegate) }
Amount.metaClass.div = { Number factor -> delegate.divide(factor) }
Amount.metaClass.div = { Amount factor -> delegate.divide(factor) }
Amount.metaClass.power = { Number factor -> delegate.pow(factor) }
Amount.metaClass.negative = { -> delegate.opposite() }

// arithmetics: multiply, divide, addition, substraction, power
println( 18.4.kg * 2 )
println( 1800000.kg / 3 )
println( 1.kg * 2 + 3.kg / 4 )
println( 3.cm + 12.m * 3 - 1.km )
println( 1.5.h + 33.s - 12.min )
println( 30.m**2 - 100.ft**2 )

// opposite and comparison
println( -3.h )
println( 3.h < 4.h )

Мы также можем делать сравнения, как показано в последней строке выше, так как эти типы сопоставимы. Опять же бесплатно. Что-то, что мы уже рассмотрели, это сложные единицы, такие как скорость, которая представляет собой смесь расстояния и длительности. Итак, если вы хотите использовать ограничение скорости, вы хотели бы писать, 90.km/hно наш DSL в его текущем состоянии позволит вам только писать 90.km/1.h, что на самом деле выглядит не очень хорошо. Чтобы обойти эту проблему, мы могли бы создать столько переменных, сколько единиц. У нас может быть hпеременная, kmпеременная и т. Д. Но я бы предпочел что-то более автоматическоеПозволяя самому сценарию предоставить эти блоки. В скриптах Groovy у вас могут быть локальные переменные (когда вы определяете переменную, это локальная переменная), но вы также можете передавать или получать доступ к переменным через привязку. Это удобный способ передачи данных, когда вы интегрируете Groovy в приложение Java, например, для совместного использования определенного контекста данных. Мы собираемся создать новый Binding под названием UnitBinding, который переопределит getVariable()метод, так что все нелокальные переменные, которые используются в скрипте Groovy, ищутся в этой привязке. Вы заметите специальную обработку для переменной ‘out’, в которой println()метод ищет выходной поток для использования.

    // script binding to transform free standing unit reference like 'm', 'h', etc
class UnitBinding extends Binding {
def getVariable(String symbol) {
if (symbol == 'out') return System.out
return Amount.valueOf(1, Unit.valueOf(symbol))
}
}

// use the script binding for retrieving unit references
binding = new UnitBinding()

// inverse units
println( 30.km/h + 2.m/s * 2 )
println( 3 * 3.mg/L )
println( 1/2.s - 2.Hz )

Скорость теперь выглядит намного больше как математическое обозначение, которое будут использовать все. Теперь, когда все это волшебство сделано, мы можем сделать еще одну вещь. Иногда вы можете конвертировать различные единицы измерения, например, футы и метры или дюймы и сантиметры. Итак, в качестве последнего шага наших экспериментов с DSL, мы добавим to()метод для преобразования.

    // define to() method for unit conversion
Amount.metaClass.to = { Amount amount -> delegate.to(amount.unit) }

// unit conversion
println( 200.cm.to(ft) )
println( 1.in.to(cm) )

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