Учитывая строгую систему типов Scala, у меня был амбициозный проект, от которого я сейчас отказываюсь, потому что соотношение усилий и полезности кажется слишком высоким.
В основном у меня есть некоторые графические элементы (GE
), и они соответствуют звуковым процессам, которые выполняются с заданной скоростью вычисления . Элементы графа составлены из других элементов графа, формирующих их входы. Теперь есть довольно произвольные ограничения на входные ставки. В исходном языке (SuperCollider) цены проверяются во время выполнения, естественно, потому что это язык с динамической типизацией. Я хотел посмотреть, смогу ли я применить проверку во время компиляции.
Некоторые ограничения довольно просты и могут быть выражены в виде «скорость arg1 должна быть, по крайней мере, такой же высокой, как скорость arg2». Но другие становятся запутанными, например
"если скорость arg0 равна 'спросу', скорость args1 должна быть либо 'спросом', либо 'скалярной', либо равна скорости вмещающей GE '.
Вопрос в том, должен ли я отказаться от этого? Вот как это выглядит с проверкой во время выполнения:
sealed trait Rate
case object demand extends Rate
case object audio extends Rate
case object control extends Rate
case object scalar extends Rate
trait GE { def rate: Rate }
// an example GE:
case class Duty(rate: Rate, in0: GE, in1: GE) extends GE {
def checkRates(): Unit =
require(in0.rate != demand || (in1.rate != demand &&
in1.rate != scalar && in1.rate != rate))
}
И в отличие от того, как это могло бы выглядеть с параметрами типа для ставок:
sealed trait Rate
trait audio extends Rate
trait demand extends Rate
trait control extends Rate
trait scalar extends Rate
trait GE[R <: Rate]
object Duty {
trait LowPri {
implicit def con1[R, T]: RateCons[R, audio , T] = new ConImpl[R, audio , T]
implicit def con2[R, T]: RateCons[R, control, T] = new ConImpl[R, control, T]
implicit def con3[R, T]: RateCons[R, scalar , T] = new ConImpl[R, scalar , T]
implicit def con4[R, T]: RateCons[R, demand , demand] =
new ConImpl[R, demand, demand]
implicit def con5[R, T]: RateCons[R, demand , scalar] =
new ConImpl[R, demand, scalar]
}
object RateCons extends LowPri {
implicit def con6[R]: RateCons[R, demand, R] = new ConImpl[R, demand, R]
}
private class ConImpl[ R, S, T ] extends RateCons R, S, T ]
sealed trait RateCons[ R, S, T ]
def ar[S <: Rate, T <: Rate](in0: GE[S], in1: GE[T])(
implicit cons: RateCons[audio, S, T]) = apply[audio, S, T](in0, in1)
def kr[S <: Rate, T <: Rate](in0: GE[S], in1: GE[T])(
implicit cons: RateCons[control, S, T]) = apply[control, S, T](in0, in1)
}
case class Duty[R <: Rate, S <: Rate, T <: Rate](in0: GE[S], in1: GE[T])(
implicit con: Duty.RateCons[R, S, T]) extends GE[R]
Тесты:
def allowed(a: GE[demand], b: GE[audio], c: GE[control], d: GE[scalar]): Unit = {
Duty.ar(b, c)
Duty.kr(b, c)
Duty.ar(b, a)
Duty.ar(b, d)
Duty.ar(a, b)
Duty.kr(a, c)
}
def forbidden(a: GE[demand], b: GE[audio], c: GE[control], d: GE[scalar]): Unit = {
Duty.kr(a, b)
Duty.ar(a, c)
}
Стоит ли идти по пути? Еще три вещи, которые говорят против этого, кроме раздувания кода:
- Вероятно, есть пара десятков
GE
s, для которых потребуются пользовательские ограничения
- Составление
GE
s становится все более сложным: код может нуждаться в передаче десятков параметров типа
- Преобразования могут стать трудными, например, представьте себе
List[GE[_<:Rate]].map( ??? )
. Я имею в виду, как бы Duty.RateCons
перевести на TDuty.RateCons
(где TDuty
- это другое GE
) ...
Я уже потратил немало времени на этот проект, поэтому я не хочу так легко сдаваться. Итак ... убедите меня, что я делаю что-то полезное здесь, или скажите, что мне следует вернуться к динамически проверенной версии.