Вашему вниманию будет представлен паттерн для создания «мини-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».