Уточненные (refined) типы

Арсений Жижелев, Праймтолк / zhizhelev@primetalk.ru

Зачем нужны типы?

  • Валидация кода на этапе компиляции
  • Доказательство программы
  • Сохранение валидности кода в ходе рефакторинга/развития программы
  • Поддержка intelli sense

Чем точнее типы, тем меньше кода и тестов

Неточные типы


def getElement[A](vec: Vector[A])(index: Int): Option[A] = {
  if(index >= 0 && index < arr.length)
    Some(arr(index))
  else
    None
}
					
  • функция - "тотальная" (total) - возвращает значение при любых значениях аргументов
  • обязательна runtime-проверка границ массива
  • функция делает три вещи сразу
    • проверяет входные значения
    • выполняет свою работу
    • возвращает ошибку (None)

Точные типы


  def getElement[A, N](vec: Vector[A] Refined Size[Equal[N]])
                   (i: Int Refined And[NonNegative, Less[N]]): A =
    vec.value(i)
                        
  • функция - "тотальная" (total) - возвращает значение при любых значениях аргументов
  • проверка границ массива исключена
  • ошибки исключены
  • функия соответствует SRP (single responsibility principle)

Вывод (inference, widening)


  val i: Int Refined Greater[W.`1`.T] = ???
  def sqrt(z: Int Refined Greater[W.`0`.T]) = ???
  import eu.timepit.refined.auto._
  sqrt(i)
                            
  • более узкий диапазон приводится к более широкому
  • похоже на отношение
    
    Int Refined Greater[W.`1`.T] <:< Int Refined Greater[W.`0`.T]
                                

Возможности из коробки

  • составные предикаты ( 0 <= x && x <= 65535)
    
    type PortNumber = Int Refined And[ Not[Less[W.`0`.T]],
                                       Less[W.`65536`.T] ]
                            
  • составные предикаты со списком условий
    
    type AllowedChar = Char Refined
                       AnyOf[Digit :: Letter ::
                             Whitespace :: HNil]
                            
  • автоматическая валидация по предикату, заданному типом
  • пользовательские пометки типов

Возможности из коробки 2

  • предикаты на элементы коллекций
    
    type ListOfNonEmptyStrings = List[String] Refined
                                 Forall[NonEmpty]
    type MyListOfInts          = List[Int] Refined
                                 Tail[Exists[Positive]]
                            
  • предикаты на размер коллекции
    
    type ListOfSize3[T]    = List[T] Refined
                             Size[Equal[W.`3`.T]]
    type ListOfEvenSize[T] = List[T] Refined
                             Size[Even]
    type ListOfSizeGt2[T]  = List[T] Refined
                             MinSize[W.`2`.T]
                            

Возможности из коробки 3

  • поддержка RegEx-типов
    
    type EMail = String Refined
      MatchesRegex[W.`"^[a-zA-Z0-9.!#$%&’*+/=?^_\`{|}~-]+@[a-zA-Z0-9-]+(?:\\\\.[a-zA-Z0-9-]+)*$"`.T]
                            
  • поддержка нескольких стандартных форматов - IPv4, IPv6, URL, URI, UUID, SHA256, Xml, etc.

Применения: circe


type PortNumber = Int    Refined
                  Interval.Closed[_0, W.`65535`.T]
type StringUrl  = String Refined Url

case class EndPoint(url: StringUrl, port: PortNumber)

implicit val decodeEndPoint: Decoder[EndPoint] =
  Decoder.forProduct2("url", "port")(EndPoint.apply)
                        
  • декодирование даст обычную ошибку разбора, если предикат не выполнится
  • кодирование игнорирует Refined

Применения: doobie


"throw exception if the refinement fails" in {
  secondaryValidationFailedCaught_?(
   sql"select -1".query[PositiveInt]
     .unique.transact(xa).void.unsafeRunSync
  )
}
                        

Применения: slick


  class FooBars(tag: Tag) extends Table[Foobar](tag, "FOOBARS")
  {
    def bar = column[PosInt]("BAR", O.PrimaryKey)
    def foo = column[NonEmptyString]("FOO")

    def * = (bar, foo) <> (FooBar.tupled, FooBar.unapply)
  }
                        

Как работает под капотом?


final class Refined[T, P] private (val value: T)
      extends AnyVal

object Refined {
  def unsafeApply[T, P](value: T): Refined[T, P] =
    new Refined(value)
}
                        
  • compile-time обертка
  • приватный конструктор
  • фантомный тип предиката

trait Validate[T, P] {
  def validate(t: T): Either[String,Refined[T,P]]
}
                        
  • type-class, позволяющий проверить предикат P над значением T
  • (в библиотеке немного сложнее за счет поддержки Refined и @@, а также качественных сообщений об ошибках)

  final case class IsZero()

  implicit def isZeroValidate[T](
    implicit n: Numeric[T]
  ): Validate[T, IsZero] = (t: T) => {
    if (n.zero == t)
      Right(Refined.unsafeApply(t))
    else
      Left("IsZero failed for " + t)
  }
                        
  • простой предикат без параметров
  • обычный код проверки условия

  final case class IsEqual[U](u: U)

  implicit def equalValidate[T, U](
    implicit w: Witness.Aux[U]
  ): Validate[T, IsEqual[U]] = (t: T) => {
    if (w.value == t)
      Right(Refined.unsafeApply(t))
    else
      Left("IsEqual(" + w.value + ") failed for " + t)
  }
                        
  • предикат с параметром
  • тип U - синглетонный тип
  • Witness - способ получить значение синглетона, из которого построен тип

  shapeless.Witness.`0`.T
  def selectDynamic(tpeSelector: String): Any =
    macro SingletonTypeMacros.witnessTypeImpl
                        
  • синглетонные типы поддерживаются на уровне компилятора
  • но не очень поддерживаются на уровне синтаксиса
  • Witness (из shapeless) создает синглетонный тип из строкового представления

Комбинаторы And, Or, ...


  final case class And[A,B]()
  final case class Or[A,B]()
  final case class Not[A]()

  implicit def andValidate[T, A, B](
    implicit va: Validate[T, A], vb: Validate[T, B]
  ): Validate[T, And[A, B]] = (t: T) => {
    (va.validate(t), vb.validate(t)) match {
      case (Right(_), Right(_)) =>
        Right(Refined.unsafeApply(t))
      case (Left(e), _) => Left(e)
      case (_, Left(e)) => Left(e)
    }
  }
                        
  • фантомные типы - функции от нескольких аргументов в пространстве типов
  • каждый аргумент имеет свой валидатор

Вывод (inference, widening)


  case class Inference[P, C](isValid: Boolean)

  type ==>[A, B] = Inference[A, B]

  implicit def greaterInferenceNat[A <: Nat, B <: Nat](
    implicit ta: ToInt[A], tb: ToInt[B]
  ): Greater[A] ==> Greater[B] =
    Inference(ta() > tb(),
      s"greaterInferenceNat(${ta()}, ${tb()})")

  implicit def autoInfer(implicit ir: A ==> B) =
    macro InferMacro.impl[F, T, A, B]

  def impl = macro {
    val inference = eval(ir)
    if (!inference.isValid) {
      abort(invalidInference(...))
    }
    unsafeRewrap(rt)
  }
                        
  • type-class времени компиляции
  • ограниченный вывод - только для известных предикатов

Светлое будущее


def max(x: Int, y: Int): { v: Int => v >= x && v >= y } =
  if (x > y) x else y

type NonNegative = { v: Int => v >= 0}

def sqrt(z: NonNegative): Double =
  scala.math.sqrt(z.toDouble)

val u: Int = ???
sqrt(max(0,u))
					

THE END

- See the presentation at GitHub
- Mail to zhizhelev at Primetalk.