Вашему вниманию будет представлен паттерн для создания «мини-DSL» на Scala для оперирования единицами измерения. Одну из реализаций этого паттерна можно увидеть в стандартной библиотеке Scala, а именно — в scala.concurrent.duration._. Пример из документации по Akka[1]:
В данном случае Int неявно конвертируется в объект с методом «seconds», который затем возвращает требуемый функции тип.
Далее будет рассмотрено пошаговое создание «мини-DSL» для оперирования частотой. В конечном итоге планируется получить возможность задавать частоту естественным образом, например, 5 kHz.
Перед тем, как приступить к реализации паттерна, необходимо создать класс для хранения единицы измерения, будь то время, уровень сигнала или частота. Например:
После создания класса для хранения единицы измерения необходимо обозначить все возможные её представления и правила конвертации для них друг в друга. Для частоты — это Hz, kHz, MHz, GHz. Пример:
Выше представлена реализация только для Hz. Остальные делаются аналогично. Их можно посмотреть на гитхабе по ссылке в конце статьи. В случае со стандартной библиотекой Scala правила конвертации заданы в enum (java.util.concurrent.TimeUnit).
Добавим классу Frequency объект-компаньон с методом apply для создания частоты:
Теперь, когда у нас есть класс для хранения единицы измерения, а также правила для её конвертации, нужно создать способ неявной конвертации и добавить его в область видимости. Удобнее будет создать «package-object»:
Каждый неявный класс добавляет тип, из которого может быть получена частота. Теперь мы можем использовать частоту естественным образом. Пример:
В дальнейшем можно добавлять операции сложения и умножения. В этом случае синтаксис будет выглядеть не так естественно, так как придётся ставить скобки вокруг каждого выражения или точку после цифры:
Полный исходный код с примерами можно посмотреть в репозитории.
UPDATE
1. Использование постфиксной нотации для вызова методов небезопасно и не рекомендуется. Добавил вариант с обычной нотацией. Спасибо Googolplex.
2. Добавил примесь FrequencyConversions в статью. Спасибо velet5.
1. Futures. Akka Documentation. Секция «Use With Actors».
implicit val timeout = Timeout(5 seconds)
В данном случае Int неявно конвертируется в объект с методом «seconds», который затем возвращает требуемый функции тип.
Далее будет рассмотрено пошаговое создание «мини-DSL» для оперирования частотой. В конечном итоге планируется получить возможность задавать частоту естественным образом, например, 5 kHz.
Перед тем, как приступить к реализации паттерна, необходимо создать класс для хранения единицы измерения, будь то время, уровень сигнала или частота. Например:
class Frequency(val hz: BigInt) { require(hz >= 0, "Frequency must be greater or equal to zero!") def +(other: Frequency) = new Frequency(hz + other.hz) override def toString: String = hz.toString + " Hz" }
После создания класса для хранения единицы измерения необходимо обозначить все возможные её представления и правила конвертации для них друг в друга. Для частоты — это Hz, kHz, MHz, GHz. Пример:
sealed trait FrequencyUnitScala { def toHz(n: BigInt): BigInt def toKHz(n: BigInt): BigInt def toMHz(n: BigInt): BigInt def toGHz(n: BigInt): BigInt def convert(n: BigInt, unit: FrequencyUnitScala): BigInt } object Hz extends FrequencyUnitScala { override def toHz(n: BigInt): BigInt = n override def toGHz(n: BigInt): BigInt = toMHz(n) / 1000 override def toKHz(n: BigInt): BigInt = n / 1000 override def toMHz(n: BigInt): BigInt = toKHz(n) / 1000 override def convert(n: BigInt, unit: FrequencyUnitScala): BigInt = unit.toHz(n) } …… }
Выше представлена реализация только для Hz. Остальные делаются аналогично. Их можно посмотреть на гитхабе по ссылке в конце статьи. В случае со стандартной библиотекой Scala правила конвертации заданы в enum (java.util.concurrent.TimeUnit).
Добавим классу Frequency объект-компаньон с методом apply для создания частоты:
object Frequency { def apply(value: BigInt, unit: FrequencyUnitScala): Frequency = unit match { case frequency.Hz => new Frequency(value) case u => new Frequency(u.toHz(value)) } }
Теперь, когда у нас есть класс для хранения единицы измерения, а также правила для её конвертации, нужно создать способ неявной конвертации и добавить его в область видимости. Удобнее будет создать «package-object»:
trait FrequencyConversions { protected def frequencyIn(unit: FrequencyUnitScala): Frequency def Hz = frequencyIn(frequency.Hz) def kHz = frequencyIn(frequency.kHz) def MHz = frequencyIn(frequency.MHz) def GHz = frequencyIn(frequency.GHz) } package object frequency { implicit final class FrequencyInt(private val n: Int) extends FrequencyConversions { override protected def frequencyIn(unit: FrequencyUnitScala): Frequency = Frequency(n, unit) } }
Каждый неявный класс добавляет тип, из которого может быть получена частота. Теперь мы можем использовать частоту естественным образом. Пример:
scala> import org.nd.frequency._ import org.nd.frequency._ scala> println(1 Hz) 1 Hz scala> println(1 kHz) 1000 Hz scala> println(1 MHz) 1000000 Hz scala> println(1 GHz) 1000000000 Hz
В дальнейшем можно добавлять операции сложения и умножения. В этом случае синтаксис будет выглядеть не так естественно, так как придётся ставить скобки вокруг каждого выражения или точку после цифры:
scala> val sum = (3000 kHz) + (2 MHz) sum: org.nd.frequency.Frequency = 5000000 Hz scala> println("3000 kHz + 2 MHz equals " + sum.toKHz) 3000 kHz + 2 MHz equals 5000 kHz scala> 10.Hz + 5.Hz res1: org.nd.frequency.Frequency = 15 Hz
Полный исходный код с примерами можно посмотреть в репозитории.
UPDATE
1. Использование постфиксной нотации для вызова методов небезопасно и не рекомендуется. Добавил вариант с обычной нотацией. Спасибо Googolplex.
2. Добавил примесь FrequencyConversions в статью. Спасибо velet5.
Список использованных источников
1. Futures. Akka Documentation. Секция «Use With Actors».
