В процессе написания программ мы часто сталкиваемся с данными, для которых допустим только ограниченный набор значений. Например, возраст, который не может быть отрицательным или email, который может иметь только определенный формат строки. При использовании примитивных типов (Int, String) приходится писать различные валидаторы, поскольку такие типы:

  • могут представлять все что угодно
  • могут содержать все что угодно

Тут еще можно упомянуть антипаттерн Primitive Obsession.

Если у нас есть некоторая функция, которая принимает в аргументы имя, email и возраст, описанные примитивными типами:

def foo(name: String, email: String, age: Int) = {
	// что-то делаем с name, email и age
}

Пользователь или мы сами можем случайно передать ей некорректные значения.

val result = foo("???", "", -20) // не ок

Добавление валидации в самом примитивном виде может выглядеть как то так.

def foo(name: String, email: String, age: Int) = {
	if (!validateName(name) || !validateEmail(email) || !validateAge(age))
	  // Ошибка валидации
 
	// Если валидация прошла успешно, то используем name, email и age
}

Теперь функция foo делает не только то, для чего она действительно была создана, но еще и ответственна за то, чтобы в ней производилась валидация (тем самым нарушая первый принцип SOLID).

Может помогут псевдонимы типов (Type alias)?

Псевдонимы типов не помогут, поскольку они так же продолжают представлять примитивные типы.

type Name = String
type Email = String
type Age = Int
 
def foo(name: Name, email: Email, age: Age) = ???
 
val name: Name = "Mark"
val email: Email = "mark@email.com"
val age: Age = 42
 
foo(name, email, age) // ок
 
foo(email, name, age) // вообще не ок

И компилятор не сможет сообщить, что мы допустили ошибку, случайно поменяв email и name местами.

А value class-ы?

Обернув примитивные типы в кейс-классы (тем самым создав спец. типы), мы не можем просто так взять и подсунуть Name вместо Email.

final case class Name(value: String) extends AnyVal
final case class Email(value: String) extends AnyVal
final case class Age(value: Int) extends AnyVal
 
def foo(name: Name, email: Email, age: Age) = ???
 
foo(Name("Mark"), Email("mark@email.com"), Age(42)) // ок
 
foo(Email("mark@email.com"), Name("Mark"), Age(42)) // ошибка компиляции
// type mismatch;
//  found   : org.github.ainr.experiments.Main.Email
//  required: org.github.ainr.experiments.Main.Name

Но эти типы все еще могут принимать любые значения.

val email = Email("blah blah blah") // не ок

Используем refined (уточненные) типы

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

Для использования refined-типов необходимо подключить библиотеку refined добавив следующую строчку в build.sbt

libraryDependencies += "eu.timepit"  "circe-refined" % "0.14.1"

И сделать импорт там, где будет выполняться json-преобразование.

import io.circe.generic.auto._, io.circe.parser._
import io.circe.refined._
 
val json =
  """
    |{
    |   "name": "Martin",
    |   "email": "martin?email.com", // ошибка
    |   "age": 55
    |}
    |""".stripMargin
 
val decodedFoo: Either[circe.Error, Foo] = decode[Foo](json)
// Left(DecodingFailure(Predicate failed: "martin?email.com".matches("(\w)+@([\w\.]+)")., List(DownField(email))))

Собственно, если json будет содержать некорректные значения, то мы в результате получим Either с ошибкой декодирования (DecodingFailure).

Неполный список доступных расширений для интеграции различных библиотек с refined-типами приведен в документации.

И кстати, преобразование примитивных типов в refined-типы в рантайме происходит примерно следующим способом.

import eu.timepit.refined.api.RefType
import eu.timepit.refined.api.Refined
import eu.timepit.refined.string._
 
type Email = String Refined MatchesRegex["""(\w)+@([\w\.]+)"""]
 
val badEmail = "bad email"
 
val email = RefType.applyRef[Email](badEmail)

Вместо заключения

Целью этого небольшого поста ставил обозначить проблему, для решения которой были созданы refined-типы и продемонстрировать некоторые возможности библиотеки.

Более подробно с примерами и списком предикатов можно ознакомиться в гитхабе проекта.