Уточненные и экзистенциальные типы для значений времени выполнения

Предположим, я хочу отобразить между некоторыми строками и целочисленными идентификаторами, и я хочу, чтобы мои типы делали невозможным получение ошибки во время выполнения, потому что кто-то пытался найти идентификатор, который был вне диапазона. Вот один простой API:

trait Vocab {
  def getId(value: String): Option[Int]
  def getValue(id: Int): Option[String] 
}

Это раздражает, однако, если пользователи обычно получают свои идентификаторы от getId и поэтому знаю, что они действительны. Следующее улучшение в этом смысле:

trait Vocab[Id] {
  def getId(value: String): Option[Id]
  def getValue(id: Id): String
}

Теперь у нас может быть что-то вроде этого:

class TagId private(val value: Int) extends AnyVal

object TagId {
  val tagCount: Int = 100

  def fromInt(id: Int): Option[TagId] =
    if (id >= 0 && id < tagCount) Some(new TagId(id)) else None
}

И тогда наши пользователи могут работать с Vocab[TagId] и не нужно беспокоиться о проверке, getValue поиск не удался в типичном случае, но они все равно могут искать произвольные целые числа, если это необходимо. Это все еще довольно неловко, так как мы должны написать отдельный тип для каждого вида вещей, для которых мы хотим словарь.

Мы также можем сделать что-то подобное с уточненным:

import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval.ClosedOpen
import shapeless.Witness

class Vocab(values: Vector[String]) {
  type S <: Int
  type P = ClosedOpen[Witness.`0`.T, S]

  def size: S = values.size.asInstanceOf[S]

  def getId(value: String): Option[Refined[Int, P]] = values.indexOf(value) match {
    case -1 => None
    case i  => Some(Refined.unsafeApply[Int, P](i))
  }

  def getValue(id: Refined[Int, P]): String = values(id.value)
}

Теперь, хотя S не известно во время компиляции, компилятор все еще может отслеживать тот факт, что идентификаторы, которые он нам дает, находятся между нулем и S, так что нам не нужно беспокоиться о возможности сбоя, когда мы вернемся к значениям (если мы используем тот же vocab экземпляр, конечно).

Я хочу иметь возможность написать это:

val x = 2
val vocab = new Vocab(Vector("foo", "bar", "qux"))

eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)

Так что пользователи могут легко искать произвольные целые числа, когда им действительно нужно. Это не компилируется, хотя:

scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
<console>:17: error: could not find implicit value for parameter v: eu.timepit.refined.api.Validate[Int,vocab.P]
       eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
                                          ^

Я могу сделать это скомпилировать, предоставив Witness экземпляр для S:

scala> implicit val witVocabS: Witness.Aux[vocab.S] = Witness.mkWitness(vocab.size)
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon$1@485aac3c

scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res1: scala.util.Either[String,String] = Right(qux)

И, конечно, происходит сбой (во время выполнения, но безопасно), когда значение выходит за пределы диапазона:

scala> val y = 3
y: Int = 3

scala> println(eu.timepit.refined.refineV[vocab.P](y).map(vocab.getValue))
Left(Right predicate of (!(3 < 0) && (3 < 3)) failed: Predicate failed: (3 < 3).)

Я мог бы также поместить определение свидетеля в мой Vocab класс, а затем импортировать vocab._ сделать его доступным, когда мне это нужно, но я действительно хочу предоставить refineV поддержка без дополнительного импорта или определения.

Я пробовал разные вещи, как это:

object Vocab {
  implicit def witVocabS[V <: Vocab](implicit
    witV: Witness.Aux[V]
  ): Witness.Aux[V#S] = Witness.mkWitness(witV.value.size)
}

Но это все еще требует четкого определения для каждого vocab пример:

scala> implicit val witVocabS: Witness.Aux[vocab.S] = Vocab.witVocabS
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon$1@1bde5374

scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res4: scala.util.Either[String,String] = Right(qux)

Я знаю, что мог бы реализовать witVocabS с макросом, но я чувствую, что должен быть более хороший способ сделать такие вещи, так как это кажется довольно разумным вариантом использования (и я не очень знаком с усовершенствованным, так что вполне возможно, что я скучаю что-то очевидное).

2 ответа

Решение

Оказывается, это работает так, как вы хотели бы, если бы мы сделали параметр типа S конкретный, назначив его синглтон тип values.size с помощью shapeless.Witness:

import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval.ClosedOpen
import shapeless.Witness

class Vocab(values: Vector[String]) {
  val sizeStable: Int = values.size
  val sizeWitness = Witness(sizeStable)

  type S = sizeWitness.T
  type P = ClosedOpen[Witness.`0`.T, S]

  def size: S = sizeWitness.value

  def getId(value: String): Option[Refined[Int, P]] = values.indexOf(value) match {
    case -1 => None
    case i => Some(Refined.unsafeApply[Int, P](i))
  }

  def getValue(id: Refined[Int, P]): String = values(id.value)
}

Если бы Scala позволяла одноэлементные типы AnyValс, мы могли бы удалить sizeWitness и определить type S = sizeStable.type, Это ограничение снято в реализации SIP-23.

С помощью refineV теперь работает даже с зависимым от пути типом vocab.P:

scala> val vocab = new Vocab(Vector("foo", "bar", "baz"))
vocab: Vocab = Vocab@5fae6bb9

scala> refineV[vocab.P](2)
res0: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Right(2)

scala> refineV[vocab.P](4)
res1: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Left(Right predicate of (!(4 < 0) && (4 < 3)) failed: Predicate failed: (4 < 3).)

scala> refineV[vocab.P](2).map(vocab.getValue)
res2: scala.util.Either[String,String] = Right(baz)

Это работает, так как теперь компилятор может найти неявный Witness.Aux[vocab.S] выходит за рамки Vocab экземпляры:

scala> val s = implicitly[shapeless.Witness.Aux[vocab.S]]
s: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon$1@16cd7aa2

scala> s.value
res2: s.T = 3

уточненный теперь использует этот неявный экземпляр для создания Validate[Int, vocab.P] экземпляр которого refineV использует, чтобы решить, если Int действительный индекс для vocab,

Поскольку предикат, который вы используете для уточнения Ints зависит от VocabОдним из решений является добавление неявного Witness.Aux[S] и псевдоним для refineV к этому классу:

import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval.ClosedOpen
import shapeless.Witness

class Vocab(values: Vector[String]) {
  type S <: Int
  type P = ClosedOpen[Witness.`0`.T, S]

  def size: S = values.size.asInstanceOf[S]

  def getId(value: String): Option[Refined[Int, P]] = values.indexOf(value) match {
    case -1 => None
    case i  => Some(Refined.unsafeApply[Int, P](i))
  }

  def getValue(id: Refined[Int, P]): String = values(id.value)

  implicit val witnessS: Witness.Aux[S] = Witness.mkWitness(size)

  def refine(i: Int): Either[String, Refined[Int, P]] =
    refineV[P](i)
}

С помощью Vocab.refine теперь не нужно никакого дополнительного импорта:

scala> val vocab = new Vocab(Vector("foo", "bar", "baz"))
vocab: Vocab = Vocab@490b83b3

scala> vocab.refine(1)
res4: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Right(1)

scala> vocab.refine(3)
res5: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Left(Right predicate of (!(3 < 0) && (3 < 3)) failed: Predicate failed: (3 < 3).)
Другие вопросы по тегам