Подтвердить что ты не робот

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

Предположим, что я хочу сопоставить некоторые строки и целые идентификаторы, и я хочу, чтобы мои типы делали невозможным получение сбоя во время выполнения, потому что кто-то пытался найти идентификатор, который был вне диапазона. Вот простой 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 в типичном случае, но они все равно могут искать произвольные целые числа, если это необходимо. Это все еще довольно неудобно, поскольку мы должны написать отдельный тип для каждого типа вещей, для которого мы хотим использовать словарь.

Мы также можем сделать что-то подобное с помощью 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)
}

Теперь, даже если 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] = [email protected]

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] = [email protected]

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

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

4b9b3361

Ответ 1

Оказывается, что это работает так, как вам хотелось бы, если бы мы сделали конкретный тип 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 s, мы можем удалить sizeWitness и определить type S = sizeStable.type. Это ограничение снимается в SIP-23.

Использование refineV теперь просто работает даже с зависимым от пути типом vocab.P:

scala> val vocab = new Vocab(Vector("foo", "bar", "baz"))
vocab: Vocab = [email protected]

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] = [email protected]

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

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

Ответ 2

Поскольку предикат, который вы используете для уточнения Int, зависит от 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 = [email protected]

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).)