Skip to main content
Version: v1.16.0

Custom Inlined Numeric

InlinedNumeric* traits are helpers for creating custom numeric refined types with less boilerplate.

They are good when your domain rule is a numeric bound and you want:

  • inline compile-time validation for literals
  • runtime validation with Either[String, X]
  • reusable, domain-specific numeric types

Import

import refined4s.types.all.*

Choose a Trait

TraitConstraintUse when
InlinedNumericMinMax[A]minValue <= value <= maxValueboth lower and upper bounds are required
InlinedNumericMin[A]minValue <= valueonly lower bound is required
InlinedNumericMax[A]value <= maxValueonly upper bound is required

InlinedNumericMinMax

InlinedNumericMinMax is a helper trait for numeric custom refined types that have both minimum and maximum bounds.

It gives you:

  • inclusive range validation (minValue <= value <= maxValue)
  • built-in predicate and invalidReason
  • inline validation support for literals with apply (compile-time), plus runtime validation with from

Use it when your type is a numeric value constrained to a closed interval.

How to use

  1. Define your custom type as type X = X.Type.
  2. X extends InlinedNumericMinMax[A] from refined4s.types.numeric or refined4s.types.all.
  3. Implement minValue and maxValue as inline def.
  4. Use apply for literals and from for runtime values.

Example

type Percent = Percent.Type
object Percent extends InlinedNumericMinMax[Int] {
override inline def minValue: Int = 0
override inline def maxValue: Int = 100
}
NOTE

the minValue and maxValue have to be defined as inline def so that they can be used at compile-time.

Create with Compile-time Validation

Compile-time Validation (apply)

Percent(0)
// res0: Type = 0
Percent(100)
// res1: Type = 100
Percent(-1)
// Percent(-1)
// ^^^^^^^^^^^
// Invalid value: [-1]. It must be >= 0 && <= 100.

Percent(101)
// Percent(101)
// ^^^^^^^^^^^^
// Invalid value: [101]. It must be >= 0 && <= 100.

Create with Runtime Validation

Runtime Validation (from)

val percentInput1 = 20
// percentInput1: Int = 20
val percentInput2 = 120
// percentInput2: Int = 120

Percent.from(percentInput1)
// res2: Either[String, Type] = Right(value = 20)
Percent.from(percentInput2)
// res3: Either[String, Type] = Left(
// value = "Invalid value: [120]. It must be >= 0 && <= 100."
// )

Functional Runtime Handling

def describePercent(input: Int): String =
Percent.from(input).fold(
error => s"Invalid percent input: $error",
percent => s"Validated percent: ${percent.value}"
)

describePercent(35)
// res4: String = "Validated percent: 35"
describePercent(135)
// res5: String = "Invalid percent input: Invalid value: [135]. It must be >= 0 && <= 100."

Runtime Unsafe Validation (unsafeFrom)

danger

unsafeFrom may throw an exception if the input value is invalid. So it is not recommended to use it.

Percent.unsafeFrom(percentInput2)
// java.lang.IllegalArgumentException: Invalid value: [120]. It must be >= 0 && <= 100.
// at refined4s.RefinedBase.unsafeFrom$$anonfun$1(RefinedBase.scala:27)
// at scala.util.Either.fold(Either.scala:198)
// at refined4s.RefinedBase.unsafeFrom(RefinedBase.scala:27)
// at refined4s.RefinedBase.unsafeFrom$(RefinedBase.scala:7)
// at repl.MdocSession$MdocApp$Percent$.unsafeFrom(custom-inlined-numeric.md:15)
// at repl.MdocSession$MdocApp.$init$$$anonfun$1(custom-inlined-numeric.md:75)

InlinedNumericMin

InlinedNumericMin is a helper trait for numeric custom refined types that have only a minimum bound.

It gives you:

  • lower-bound validation (minValue <= value)
  • built-in predicate and invalidReason
  • inline validation support for literals with apply (compile-time), plus runtime validation with from

Use it when your type is a numeric value constrained to a lower-bound inclusive interval.

How to use

  1. Define your custom type as type X = X.Type.
  2. X extends InlinedNumericMin[A] from refined4s.types.numeric or refined4s.types.all.
  3. Implement minValue as inline def.
  4. Use apply for literals and from for runtime values.

Example

type NonNegativeCount = NonNegativeCount.Type
object NonNegativeCount extends InlinedNumericMin[Int] {
override inline def minValue: Int = 0
}
NOTE

the minValue has to be defined as inline def so that it can be used at compile-time.

Create with Compile-time Validation

Compile-time Validation (apply)

NonNegativeCount(0)
// res6: Type = 0
NonNegativeCount(10)
// res7: Type = 10
NonNegativeCount(-1)
// NonNegativeCount(-1)
// ^^^^^^^^^^^^^^^^^^^^
// Invalid value: [-1]. It must be >= 0.

Create with Runtime Validation

Runtime Validation (from)

val nonNegativeInput1 = 3
// nonNegativeInput1: Int = 3
val nonNegativeInput2 = -3
// nonNegativeInput2: Int = -3

NonNegativeCount.from(nonNegativeInput1)
// res8: Either[String, Type] = Right(value = 3)
NonNegativeCount.from(nonNegativeInput2)
// res9: Either[String, Type] = Left(
// value = "Invalid value: [-3]. It must be >= 0."
// )

Functional Runtime Handling

def describeNonNegativeCount(input: Int): String =
NonNegativeCount.from(input).fold(
error => s"Invalid non-negative count input: $error",
count => s"Validated count: ${count.value}"
)

describeNonNegativeCount(7)
// res10: String = "Validated count: 7"
describeNonNegativeCount(-2)
// res11: String = "Invalid non-negative count input: Invalid value: [-2]. It must be >= 0."

Runtime Unsafe Validation (unsafeFrom)

danger

unsafeFrom may throw an exception if the input value is invalid. So it is not recommended to use it.

NonNegativeCount.unsafeFrom(nonNegativeInput2)
// java.lang.IllegalArgumentException: Invalid value: [-3]. It must be >= 0.
// at refined4s.RefinedBase.unsafeFrom$$anonfun$1(RefinedBase.scala:27)
// at scala.util.Either.fold(Either.scala:198)
// at refined4s.RefinedBase.unsafeFrom(RefinedBase.scala:27)
// at refined4s.RefinedBase.unsafeFrom$(RefinedBase.scala:7)
// at repl.MdocSession$MdocApp$NonNegativeCount$.unsafeFrom(custom-inlined-numeric.md:86)
// at repl.MdocSession$MdocApp.$init$$$anonfun$2(custom-inlined-numeric.md:145)

InlinedNumericMax

InlinedNumericMax is a helper trait for numeric custom refined types that have only a maximum bound.

It gives you:

  • upper-bound validation (value <= maxValue)
  • built-in predicate and invalidReason
  • inline validation support for literals with apply (compile-time), plus runtime validation with from

Use it when your type is a numeric value constrained to an upper-bound inclusive interval.

How to use

  1. Define your custom type as type X = X.Type.
  2. X extends InlinedNumericMax[A] from refined4s.types.numeric or refined4s.types.all.
  3. Implement maxValue as inline def.
  4. Use apply for literals and from for runtime values.

Example

type ScoreOutOf100 = ScoreOutOf100.Type
object ScoreOutOf100 extends InlinedNumericMax[Int] {
override inline def maxValue: Int = 100
}
NOTE

the maxValue has to be defined as inline def so that it can be used at compile-time.

Create with Compile-time Validation

Compile-time Validation (apply)

ScoreOutOf100(0)
// res12: Type = 0
ScoreOutOf100(100)
// res13: Type = 100
ScoreOutOf100(101)
// ScoreOutOf100(101)
// ^^^^^^^^^^^^^^^^^^
// Invalid value: [101]. It must be <= 100.

Create with Runtime Validation

Runtime Validation (from)

val scoreInput1 = 80
// scoreInput1: Int = 80
val scoreInput2 = 120
// scoreInput2: Int = 120

ScoreOutOf100.from(scoreInput1)
// res14: Either[String, Type] = Right(value = 80)
ScoreOutOf100.from(scoreInput2)
// res15: Either[String, Type] = Left(
// value = "Invalid value: [120]. It must be <= 100."
// )

Functional Runtime Handling

def describeScore(input: Int): String =
ScoreOutOf100.from(input).fold(
error => s"Invalid score input: $error",
score => s"Validated score: ${score.value}"
)

describeScore(95)
// res16: String = "Validated score: 95"
describeScore(120)
// res17: String = "Invalid score input: Invalid value: [120]. It must be <= 100."

Runtime Unsafe Validation (unsafeFrom)

danger

unsafeFrom may throw an exception if the input value is invalid. So it is not recommended to use it.

ScoreOutOf100.unsafeFrom(scoreInput2)
// java.lang.IllegalArgumentException: Invalid value: [120]. It must be <= 100.
// at refined4s.RefinedBase.unsafeFrom$$anonfun$1(RefinedBase.scala:27)
// at scala.util.Either.fold(Either.scala:198)
// at refined4s.RefinedBase.unsafeFrom(RefinedBase.scala:27)
// at refined4s.RefinedBase.unsafeFrom$(RefinedBase.scala:7)
// at repl.MdocSession$MdocApp$ScoreOutOf100$.unsafeFrom(custom-inlined-numeric.md:156)
// at repl.MdocSession$MdocApp.$init$$$anonfun$3(custom-inlined-numeric.md:215)