The “constraint” type in Literal
Following on my introduction to Literal yesterday, I want to explore a few specific types in detail. To kick things off, let’s look at the _Constraint
type.
The _Constraint
type constructor takes a list of positional arguments (types) and a list of keyword arguments (properties) and returns a new Ruby type1 based on the provided parameters. Its method signature is:
def _Constraint(*types, **properties)
Type constraints
The types provided as positional arguments are combined as an intersection. For the constraint to match, all of the constraints types must match.
This type, for example, will only match objects that are an integer and covered by the range. The range on its own would allow floats.
Age = _Constraint(Integer, 18..)
This type composes the two parameters together, calling ===
on each type and making sure they all return truthy. You can think of this as being something like:
@types.all? { it === object }
When given only positional arguments, _Constraint
is equivalent to _Intersection
. But it has another trick up its sleeve.
Property constraints
You can also provide property constraints as keyword arguments to the _Constraint
type constructor. Here’s an example:
PopulatedString = _Constraint(String, length: 1..)
After checking the type constraints — in this case just that the object is a string — this type will call the method length
on the object and send the return value to ===
on the range. If this is also truthy, the object is described by the constraint and it will return true
.
You can think of this part as being implemented like this:
@properties.all? do |name, type|
@object.public_send(name) === type
end
Because _Constraint
uses the ===
interface at each layer, it’s incredibly composable and flexible. You can define all kinds of precise types.
Here’s a type that matches all odd numbers.
OddNumbers = _Constraint(Integer, odd?: true)
Be careful because some predicate methods in Ruby don’t return true
or false
exactly. I know, I know. At least this one does. If you wanted to be extra safe, you could use another Literal type _Truthy
.
OddNumbers = _Constraint(Integer, odd?: _Truthy)
The _Truthy
type will match any object that is not nil
or false
. And yes, there’s a _Falsy
type that matches only nil
or false
. And that is called a union type, which we’ll save for another post.
Getting back to constraint types, it’s worth noting that many of Literal’s built-in types just compose this type with a preset parameter.
Literal has the type _String
for example. Its implementation is:
def _String(...)
_Constraint(String, ...)
end
It’s just a shortcut that lets you write a type like _String(length: 1..)
. How sick is that?