Как стать автором
Поиск
Написать публикацию
Обновить

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

Время на прочтение3 мин
Количество просмотров4.8K
Вашему вниманию будет представлен паттерн для создания «мини-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».
Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+9
Комментарии7

Публикации

Ближайшие события