+
+ Semantic Type Refinement
+ Types can be great. In the context of Scala, I like using types heavily.
+ They help me think and write better code more quickly. The notion of refined types
+ is a way to leverage types to enforce certain constraints - often at compile time.
+ Excellent libraries such as
+ refined
+ have an obvious introductory example:
+ scala> val i2: Int Refined Positive = -5
+ error: Predicate failed: (-5 > 0).
+ To be clear: this is great! I want to talk about another way to refine types
+ without taking away from the standard case.
+
+
+
+ Another Approach to Refinement
+
+ The basic refinement example essentially makes the type more specific. Another way to look
+ at it is that it removes unwanted values from the range of possible values. Rather than a
+ type that accepts integers, we have a type which only accepts positive integers.
+
+ This is useful, though in many context I prefer to go further. At the end of the day,
+ with this example, I just have a positive integer. What is the purpose of that integer?
+ Adding specific semantics to a type can make it stronger and more meaningful.
+
+ Let's pretend that our positive integer is actually intended to express some configurable
+ maximum concurrency value (please bear with the exception for now):
+
+ opaque type MaximumConcurrency = Int
+
+object MaximumConcurrency:
+
+ def apply(candidate: Int): MaximumConcurrency =
+ if candidate <= 0 then
+ throw new IllegalArgumentException(
+ "Maximum concurrency must be 1 or greater."
+ )
+ else candidate
+
+ given CanEqual[MaximumConcurrency, MaximumConcurrency] = CanEqual.derived
+
+ extension (mc: MaximumConcurrency) def toInt(): Int = mc
+
+end MaximumConcurrency
+
+ While this involves slightly more typing, it has several benefits:
+
+
+ MaximumConcurrency obviously describes the purpose of the type.
+ - The type can be independently documented.
+ - Some value of this type can only be equated to other
MaximumConcurrency values.
+ - Logic and functions can be contextualized to this type.
+ - Values of this type cannot exist unless they meet validation criteria.
+ - Zero dependencies.
+
+
+ Lest we forget the drawbacks:
+
+
+ - Value validation does not occur at compile time.
+ - Implementation is manual.
+
+
+
+
+
+ Addressing Drawbacks
+
+ First off: many values are not known at compile time. Refinement libraries
+ are not unaware of this, and provide tools for refining at runtime.
+
+ Manual implementation simply does not bother me -- it is a small effort, and that
+ effort forces me to think about each type and document each type. I am also forced
+ to justify a purpose for each type. I spend more time thinking, and less
+ time actually writing; a common theme with specific types.
+
+ That being said, this approach isn't for everything. Sometimes there are literals that
+ just need to be non-semantic, because trying to apply that layer has no value. That's okay,
+ and lines need to be drawn.
+
+
+
+
+ What About Exceptions?
+
+ There is no reason a type must rely on exceptions to perform validation:
+
+ opaque type MaximumConcurrency = Int
+
+object MaximumConcurrency:
+
+ def validate(candidate: Int): Either[MyError, MaximumConcurrency] =
+ if candidate <= 0 then Left(MyError.InvalidMaximumConcurrency(candidate))
+ else candidate
+
+ Refinement of the type can be catered to the case at hand. Use whatever mechanism
+ best fits the type.
+
+
+
+
+ Types Without Validation
+
+ This same approach can be used to give type restrictions to unconstrained values:
+
+ opaque type MaximumConcurrency = Int
+
+object MaximumConcurrency:
+
+ def apply(value: Int): MaximumConcurrency = value
+
+ Opaque type aliases are a technique that I use often, as these types still
+ prevent the use of mismatched types and communicate valuable information.
+
+
+
+ Summary
+
+ Restrictions are often power, and clarity is also often power. I enjoy both,
+ and make heavy use of them in my code. In general, I tend to rely on a dependency-free
+ manual approach to refining my types in a way that forces me to justify the existence
+ of every single type - I think the approach is at least worth consideration. I think
+ that libraries which solve a general case well are valuable in their own right and hope
+ that this brief writeup is not taken as a reason to avoid them.
+
+ I particularly like the inclusion of opaque types in Scala 3 and use
+ them heavily in lieu of fundamental types.
+
+