1. Introduction
Type refinement is all about being more precise about our types, this leads to a more robust program with less opportunities to introduce bugs. In Scala, Refined stands out as one of the best libraries for refining Scala types.
It works by making use of a predicate which should hold for elements of the type we are refining. The Refined type takes two type parameters, the type to be refined and a predicate in the following form:
Refined[Int, Positive]
In the example above, it takes an Int type as the first parameter, followed by the Positive predicate which checks numeric values greater than zero. Refined comes with multiple predefined predicates used to refine Scala types.
In this tutorial, we’ll cover different predicate examples for selected types such as Int, Char, String, and collections and how to use them in our project. Lastly, we’ll look at how to create custom Refined types.
2. Set Up
In order to work with Refined, we need to add the following to our build.sbt file:
libraryDependencies += "eu.timepit" %% "refined" % "0.11.1"
Since Refined is a standalone library, no other dependencies are needed. However, for this tutorial, we’ll be using Scala version 2.13.
3. Working With Refined Types
In this section, we’ll look at how to refine Scala types and conclude with the creation of custom refined types, first, we’ll need to have the following imports two imports in scope:
scala>import eu.timepit.refined.auto._
import eu.timepit.refined.auto._
scala> import eu.timepit.refined.api.Refined
import eu.timepit.refined.api.Refined
The first provides automatic refinements and automatic conversions between refined types, while the second is the Refined type that we’ll learn to use throughout this article.
3.1. The Int Type
In this section we’ll be working with the Int type and how it can be refined in our project:
scala> import eu.timepit.refined.numeric._
import eu.timepit.refined.numeric._
scala> val oddNumber: Refined[Int, Odd] = 8
^
error: Predicate (8 % 2 == 0) did not fail.
Above, we define oddNumber with Odd as the predicate, which checks if a value is evenly divisible by 2. When we supply an even value, 8, we receive an informative error message telling us (8 % 2 == 0) did not fail yet this should fail for all odd numbers.
The Refined type can also be written in infix notation as follows:
scala> val oddNumber: Int Refined Odd = 8
^
error: Predicate (8 % 2 == 0) did not fail.
Throughout the article, we’ll be using it in this form, some predicates provided by Refined also take parameters:
scala> val age: Int Refined Less[35] = 30
val age: eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Less[35]] = 30
scala> val ageInterval: Int Refined Interval.Closed[30, 35] = 35
val ageInterval: eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Interval.Closed[30,35]] = 35
scala> val age2: Int Refined GreaterEqual[35] = 35
val age2: eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.GreaterEqual[35]] = 35
Here we showcase three types of predicates that take parameters, first, Less[35] which checks if age is less than 35. Next, the Interval.Closed[30, 35] predicate checks if ageInerval is between 30 and 35, including those values. Lastly, GreaterEqual[35] checks if age2 is greater or equal to 35.
There’s also a refineV() function which checks values at runtime:
scala> import eu.timepit.refined._
import eu.timepit.refined._
scala> val ageInput: Int = 36
val ageInput: Int = 36
scala> val ageCheck = refineV[GreaterEqual[35]](ageInput)
val ageCheck: Either[String,eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.GreaterEqual[35]]] = Right(36)
The refineV() function takes a predicate as a type parameter followed by a value to check. We assume ageInput is provided by the user at runtime, and when supplied to refineV() along with the GreaterEqual[35] predicate, it returns a Right(36). However, if this fails, we’d receive a Left with the error message as a String.
3.2. The Char Type
In this section, we’ll look at some predicates defined for the Char type:
scala> import eu.timepit.refined.char._
import eu.timepit.refined.char._
scala> val myDigit: Char Refined Digit = '8'
val myDigit: eu.timepit.refined.api.Refined[Char,eu.timepit.refined.char.Digit] = 8
scala> val myLetter: Char Refined Letter = 'H'
val myLetter: eu.timepit.refined.api.Refined[Char,eu.timepit.refined.char.Letter] = H
In the first example, we check if a Char is Digit and in the next, if it’s a Letter, however, Refined provides other predicates for the Char type such as LowerCase, UpperCase, and Whitespace to check for lowercase, uppercase, and whitespace characters respectively.
3.3. The String Type
Validating strings remains a common task for many programmers. Fortunately, the most number of predicates provided by Refined are for the String type. Here are a couple of scenarios:
scala> import eu.timepit.refined.string._
import eu.timepit.refined.string._
scala> val myName: String Refined StartsWith["S"] = "Sandra"
val myName: eu.timepit.refined.api.Refined[String,eu.timepit.refined.string.StartsWith["S"]] = Sandra
scala> val myName2: String Refined EndsWith["t"] = "Herbert"
val myName2: eu.timepit.refined.api.Refined[String,eu.timepit.refined.string.EndsWith["t"]] = Herbert
We use the StartsWith and EndsWith predicates to check if a String starts with or ends with a certain string, however, these predicates are also case-sensitive:
scala> val myName3: String Refined StartsWith["s"] = "Sandra"
^
error: Predicate failed: "Sandra".startsWith("s").
There also predicates to check for types such as UUID and IPV6 addresses:
scala> val myUuid: String Refined Uuid = "3f80d046-58ff-4bb9-b555-d0972fec3685"
val myUuid: eu.timepit.refined.api.Refined[String,eu.timepit.refined.string.Uuid] = 3f80d046-58ff-4bb9-b555-d0972fec3685
scala> val myAddr: String Refined IPv6 = "127.0.0.1"
^
error: Predicate failed: 127.0.0.1 is a valid IPv6.
In the first line, we validate a UUID string and, later, try to validate an IPv6 address but supply an IPv4 address string. There are other available predicates provided to specific types, such as IPv4, Uri, and XML.
Validation using regular expressions is also covered for strings with the help of MatchesRegex:
scala> type myRegex = MatchesRegex["""[A-Za-z0-9]+"""]
type myRegex
scala> val accessCode: String Refined myRegex = "DC13h"
val accessCode: eu.timepit.refined.api.Refined[String,myRegex] = DC13h
In this example, we assign a type, myRegex to a regular expression with the help of MatchesRegex. [A-Za-z0-9]+ matches uppercase, lowercase, and numbers in a string; this is used to check accessCode.
The convention of assigning the Refined predicate to a type is common especially when the predicate expression is long, this is especially common when working with booleans to mix up two or more predicates:
scala> type myIntRegex = myRegex And ValidInt
type myIntRegex
Here we create a new type, myIntRegex which combines two predicates, myRegex and ValidInt. This now checks the String against the REGEX, if it holds, it then proceeds to check if the String is a passable Int. We use the And boolean predicate to combine these two checks:
scala> val accessCode2: String Refined myIntRegex = "97426"
val accessCode2: eu.timepit.refined.api.Refined[String,myIntRegex] = 97426
scala> val accessCode3: String Refined myIntRegex = "9742B"
^
error: Right predicate of ("9742B".matches("[A-Za-z0-9]+") && isValidValidInt("9742B")) failed: ValidInt predicate failed: For input string: "9742B"
When we check the string 97426 it passes both predicates. However, 9742B fails. Refined even gives information where the error occurred which in our case was ValidInt. Other common boolean predicates include Not, Or, True, and False; these work for all predicates not just String.
3.4. Collection Predicates
The predicates covered in this section greatly reduce the amount of boilerplate code when validating collections:
scala> import eu.timepit.refined.collection._
import eu.timepit.refined.collection._
scala> val fruits = List("Banana", "Orange", "Lemon", "Guava")
val fruits: List[String] = List(Banana, Orange, Lemon, Guava)
scala> val contains = refineV[Contains["Berry"]](fruits)
val contains: Either[String,eu.timepit.refined.api.Refined[List[String],eu.timepit.refined.collection.Contains["Berry"]]] = Left(Predicate (!(Banana == Berry) && !(Orange == Berry) && !(Lemon == Berry) && !(Guava == Berry)) did not fail.)
We start by checking whether fruits contains Berry using the Contains predicate. This results in a Left. Just like before, we also have predicates that take other predicates as parameters, and since we are dealing with a List[String], all the String predicates will be applicable:
scala> val forall = refineV[Forall[Trimmed]](fruits)
val forall: Either[String,eu.timepit.refined.api.Refined[List[String],eu.timepit.refined.collection.Forall[eu.timepit.refined.string.Trimmed]]] = Right(List(Banana, Orange, Lemon, Guava))
scala> val last = refineV[Last[Uuid]](fruits)
val last: Either[String,eu.timepit.refined.api.Refined[List[String],eu.timepit.refined.collection.Last[eu.timepit.refined.string.Uuid]]] = Left(Predicate taking last(List(Banana, Orange, Lemon, Guava)) = Guava failed: Uuid predicate failed: Invalid UUID string: Guava)
scala> val size = refineV[Size[Less[5]]](fruits)
val size: Either[String,eu.timepit.refined.api.Refined[List[String],eu.timepit.refined.collection.Size[eu.timepit.refined.numeric.Less[5]]]] = Right(List(Banana, Orange, Lemon, Guava))
We showcase 3 predicates above, Forall which checks if the predicate holds for all elements, Last[Uuid] which checks if the last element is a UUID, and Size[Less[5]], which checks if the size of fruits is less than 5.
3.5. Custom Refined Types
In this section we’ll create a custom validator to check whether a person is tall, of average height, or short:
scala> case class Person(name: String, height: Double)
class Person
We start by defining Person that takes a name and height of type String and Double, respectively. Next, we define the different height categories:
scala> case class Tall()
class Tall
scala> case class Average()
class Average
scala> case class Short()
class Short
The final step is to define instances of Validate, a special Refined type class for Person:
scala> import eu.timepit.refined.api.Validate
import eu.timepit.refined.api.Validate
scala> implicit val tallValidate: Validate.Plain[Person, Tall] = Validate.fromPredicate(p => p.height >= 6.0, p => s"(${p.name} is tall)", Tall())
val tallValidate: eu.timepit.refined.api.Validate.Plain[Person,Tall] = eu.timepit.refined.api.Validate$$anon$3@490ec42a
scala> implicit val averageValidate: Validate.Plain[Person, Average] = Validate.fromPredicate(p => p.height >= 5.0 && p.height < 6.0, p => s"(${p.name} is average)", Average())
val averageValidate: eu.timepit.refined.api.Validate.Plain[Person,Average] = eu.timepit.refined.api.Validate$$anon$3@3c2c175
scala> implicit val shortValidate: Validate.Plain[Person, Short] = Validate.fromPredicate(p => p.height < 5.0, p => s"(${p.name} is short)", Short())
val shortValidate: eu.timepit.refined.api.Validate.Plain[Person,Short] = eu.timepit.refined.api.Validate$$anon$3@7bd177ce
We use the Validate.fromPredicate() which takes as the first parameter a function to check the height for a tall, average, and short person, this returns a boolean. Next, we have a function that returns the error message and finally, we pass the predicate class. Here’s how we’d use this:
scala> val tall = refineV[Tall](Person("Herbert", 5.5))
val tall: Either[String,eu.timepit.refined.api.Refined[Person,Tall]] = Left(Predicate failed: (Herbert is tall).)
scala> val average = refineV[Average](Person("Herbert", 5.5))
val average: Either[String,eu.timepit.refined.api.Refined[Person,Average]] = Right(Person(Herbert,5.5))
When we try to check if a Person is tall but has an average height, we get a Left showing failure, however, when we check Person against the Average class, we receive a Right(Person(Herbert,5.5)).
4. Conclusion
In this article, we’ve learned how to refine various Scala types such as Int, String, Char, and collections.
Refined is a very good library for data validation and has many predefined predicates for this purpose. It also comes with over 30 internal and external modules for working with different libraries such as refined-cats, refined-pureconfig, refined-scalacheck, circe-refined, doobie-refined, and play-refined to mention but a few.
As always, the code for this tutorial can be found over on GitHub.