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