Pull to refresh

Паттерн для cоздания DSL на Scala для оперирования единицами измерения

Reading time3 min
Views4.7K
Вашему вниманию будет представлен паттерн для создания «мини-DSL» на Scala для оперирования единицами измерения. Одну из реализаций этого паттерна можно увидеть в стандартной библиотеке Scala, а именно — в scala.concurrent.duration._. Пример из документации по Akka[1]:

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».
Tags:
Hubs:
+9
Comments7

Articles

Change theme settings