Adventures in Ruby-esque type enforcement
Tags: ruby, types, typesafety
Published: 2023-05-15 14:53:22

---
title: Adventures in Ruby-esque type enforcement
published: true
date: 2023-05-13 04:00:00 UTC
tags: Ruby, Types, Typesafety
canonical_url: https://max.engineer/portrayal-guards-poc
---

In Ruby you can kinda pretend that you have type enforcement at runtime, because Ruby is very flexible. This could be a useful-enough thing to do to organize and formalize the “guarding” of your data. As a disclaimer, I’m not actually a huge fan of this practice, because I think that if you’re going to enforce types at runtime, you may as well achieve the same result via learning how to write good constructors and immutable objects. I believe the focus should be on controlling the flow of data from source to destination, not declaring types to guard against every generic use case. Nevertheless, for many existing codebases out there, runtime-level types might be the right way to improve maintainability, so I decided to experiment with my own approach.

Again, to be clear, the above criticism is only about enforcing types at runtime, not at compile time.

Before I start, there are already libraries out there that let you declare types at runtime. They offer a bunch of fancy-named classes and methods that let you construct your own types. I disagree with their approach, because it introduces a lot of cognitive overhead. They expect me to learn all of that extra vocabulary only to end up running boolean expressions against my values. Why not just let me write those boolean expressions in the first place? This is the whole premise of my experiment: it seems easier to write a plain Ruby value check than to figure out how to build it with fancy type libraries.

A while ago, I wrote a little library called [portrayal](https://github.com/maxim/portrayal), which is a simple Struct-like object builder. It lets you declare keywords, which are just `attr_accessor`s and a default `initialize`, plus some extra convenience. Using this lib as the basis, I wrote a proof of concept extension called `Portrayal::Guards`. In this article I show you how it works.

## Leaning into boolean expressions

Let’s say we have a `class Person`, who has `age` and `favorite_beer`.

```ruby
class Person
  extend Portrayal

  keyword :age
  keyword :favorite_beer, default: nil
  public :age=, :favorite_beer=
end
```

Note: Normally setters are protected, but I’m making them public above to illustrate how guards work.

Imagine that our data type requirements are as follows:

- Age must be an integer between 0 and 130
- Favorite beer must be nil or any string
- If favorite beer is not nil, then age must be >=21

Here is one simple way to do this with `Portrayal::Guards`.

```ruby
class Person
  extend Portrayal

  keyword :age
  keyword :favorite_beer, default: nil
  public :age=, :favorite_beer=

  guard('age must be human and beer is only for >=21yo') {
    age.is_a?(Integer) && (0..130).cover?(age) &&
      (favorite_beer.nil? || (favorite_beer.is_a?(String) && age >= 21))
  }
end
```

This guard can be declared _anywhere in the class body_. It has a single boolean expression in it. If it returns anything truthy, the guard passes. If it returns `false` or `nil`, the guard fails. The string argument serves as the error message in case it fails. With this single guard we actually solved the whole problem.

Check out how this guard protects our object:

```irb
> Person.new(age: 200)
ArgumentError: age must be human and beer is only for >=21yo

> person = Person.new(age: 5)
=> #<Person @age=5, @favorite_beer=nil>

> person.favorite_beer = 'corona'
ArgumentError: age must be human and beer is only for >=21yo

> person.update(age: 200, favorite_beer: 9)
=> {:base=>["age must be human and beer is only for >=21yo"]}

> person.update(age: 30, favorite_beer: 'corona')
=> nil

> person
=> #<Person @age=30, @favorite_beer="corona">
```

Three things to notice here:

1. This guard is guarding both `initialize` (`.new`), and writer methods.
2. We have a special method `update`, which lets you update multiple values at the same time. This helps resolve situations when you can’t assign attributes one at a time, because guards cross-check them.
3. Notice that the error we got from `update` is under a key `:base`. Keep it in mind for now, I will explain this later.

This was easy, it’s just a plain boolean expression that now completely guards our attributes. However, the expression is a little bit unwieldy, and the error message is not super useful for telling us what exactly is wrong. That’s okay. We can rewrite the guard into 3 separate guards.

```ruby
guard('age must be an integer in human range') {
  age.is_a?(Integer) && (0..130).cover?(age)
}

guard('favorite_beer must be string or nil') {
  favorite_beer.nil? || favorite_beer.is_a?(String)
}

guard('favorite_beer is only allowed for age >=21') {
  favorite_beer.nil? || age >= 21
}
```

Much neater. Let’s try running the same code:

```irb
> Person.new(age: 200)
ArgumentError: age must be an integer in human range

> person = Person.new(age: 5)
=> #<Person @age=5, @favorite_beer=nil>

> person.favorite_beer = 'corona'
ArgumentError: favorite_beer is only allowed for age >=21

> person.update(age: 200, favorite_beer: 9)
=> {:base=>["age must be an integer in human range", "favorite_beer must be string or nil"]}

> person.update(age: 30, favorite_beer: 'corona')
=> nil

> person
=> #<Person @age=30, @favorite_beer="corona">
```

Nice, error messages are now more specific.

Just to recap, with `guard` and plain Ruby we can accomplish… everything. Okay, thanks, b…

## But what about reuse?

Ah. Reuse is already here by default. We can have a module like this.

```ruby
module ReusableTypes
  def int(name)
    guard("#{name} must be an integer") { send(name).is_a?(Integer) }
  end

  def age(name)
    int(name)
    guard("#{name} must be within 0-130") { (0..130).cover?(send(name)) }
  end

  def nullable_string(name)
    guard("#{name} must be nil or a string") {
      value = send(name)
      value.nil? || value.is_a?(String)
    }
  end
end

class Person
  extend Portrayal
  extend ReusableTypes

  keyword :age
  keyword :favorite_beer, default: nil
  public :age=, :favorite_beer=

  age :age
  nullable_string :favorite_beer

  guard('favorite_beer is only allowed for age >=21') {
    favorite_beer.nil? || age >= 21
  }
end
```

We put guards in module methods and call them. Nothing really changed, but we suddenly have reusable types.

In Ruby it’s a common tradition to return the name of what’s being declared. Portrayal’s `keyword` follows this tradition, returning the name of the keyword. If you’d like, you can put our type methods in front of `keyword`, and it works the same.

```ruby
class Person
  extend Portrayal
  extend ReusableTypes

  age keyword :age
  nullable_string keyword :favorite_beer, default: nil
  public :age=, :favorite_beer=

  guard('favorite_beer is only allowed for age >=21') {
    favorite_beer.nil? || age >= 21
  }
end
```

If you don’t like the above style, you could do something else. For example, you could return `name` from methods in our module, and wrap the keyword names in them. Let’s also capitalize method names while at it:

```ruby
module ReusableTypes
  def Int(name)
    guard("#{name} must be an integer") { send(name).is_a?(Integer) }
    name
  end

  def Age(name)
    Int(name)
    guard("#{name} must be within 0-130") { (0..130).cover?(send(name)) }
    name
  end

  def NullableString(name)
    guard("#{name} must be nil or a string") {
      value = send(name)
      value.nil? || value.is_a?(String)
    }
    name
  end
end
```

Which makes this possible:

```ruby
class Person
  extend Portrayal
  extend ReusableTypes

  keyword Age(:age)
  keyword NullableString(:favorite_beer), default: nil
  public :age=, :favorite_beer=

  guard('favorite_beer is only allowed for age >=21') {
    favorite_beer.nil? || age >= 21
  }
end
```

When I said earlier that guards can be declared _anywhere in the class body_, I really meant it. This still works. I’m sure there are more ways you can come up with for using these guards. These are just a couple off the top of my head.

Looking at the above, you can probably already imagine how you’d be able to easily implement a type of any complexity, and `Portrayal::Guards` will make sure to guard your initializers and writers for you.

Okay, thanks, b…

## But what about composition?

Right, we actually might need some extra features to make composition nice. After toying around with some ideas, I decided to include the following additional features into the proof of concept.

### Guard chaining

One way to compose guards could be to make sure that our reusable methods return the passed-in `name`, like we already did above. If every declaration returns the name that it received, then we could chain guards like this:

```ruby
# Type methods
def Odd(name)
  guard("#{name} must be odd") { value = send(name); value.respond_to?(:odd?) && value.odd? }
  name
end

def Int(name)
  guard("#{name} must be an integer") { send(name).is_a?(Integer) }
  name
end

# Chaining example:
Odd Int keyword :odd_number
```

This could be especially nice for something like `Nullable`, where we don’t want to create `NullableString`, `NullableInt`, etc for every possible type. So maybe if we had

```ruby
def Nullable(name)
  guard("#{name} can be nil") { send(name).nil? }
  name
end
```

Then maybe we could write `Nullable Int keyword :number`?

Unfortunately, we cannot. It won’t work, because `Nullable` will fail anything that isn’t a nil, and `Int` will fail anything that isn’t an integer. They don’t mesh, because we don’t have full `&&`/`||` capabilities across guards. The good news is that perhaps we don’t actually need them.

I’ve thought about a few ways to enable this sort of composition, and came up with what I find to be a simple solution: a `pass!` guard.

### Special `pass!` guard

A `pass!` is just like a regular `guard`, you can have as many as you want (but you probably never need more than one), and they always run first. If a `pass!` returns anything truthy, then we’re done, the object is valid, no further guards are called. With this new capability we can make `Nullable` like this:

```ruby
def Nullable(name)
  pass!("#{name} can be nil") { send(name).nil? }
  name
end
```

And this kind of composition works now:

```ruby
Nullable Int keyword :number, default: nil
Nullable String keyword :text, default: nil
```

Yay!

Because a `pass!` always runs first, the order doesn’t matter. If a `pass!` sees `nil`, other guards won’t run. If it sees non-nil, then we proceed into int/string guards.

Unfortunately, there’s still a problem here. All the guards are mixed together, so the `Nullable` check for `number` will stop all guards from executing, even the `String` guard for `text`. That’s because we add guards into the class, but we aren’t grouping them with each other.

To solve this, I added guard grouping. But don’t worry, it’s basically nothing.

### Guard grouping

Remember that `:base` key in the error hash you saw earlier? Here’s a reminder:

```ruby
{:base=>["age must be human and beer is only for >=21yo"]}
```

The `:base` is actually a default topic for guards. And it’s super simple to group guards into other topics. Just add one more argument to the guard:

```ruby
guard(:topic_name, 'error message') { boolean expression }
```

The new first argument `:topic_name` (it could be anything really) is the topic. So all guards are actually per topic. A fail or `pass!` in one topic won’t stop guards in another topic. This is just a more generic way to let you make guards “per attribute”. And of course it’s just what the doctor ordered for `ReusableTypes` module. We can now do this:

```ruby
module ReusableTypes
  def int(name)
    guard(name, "#{name} must be an integer") { send(name).is_a?(Integer) }
  end

  def age(name)
    int(name)
    guard(name, "#{name} must be between 0 and 130") { (0..130).cover?(send(name)) }
  end

  def string(name)
    guard(name, "#{name} must be string") { send(name).is_a?(String) }
  end

  def nullable(name)
    pass!(name, "#{name} can be nil") { send(name).nil? }
  end
end
```

By the way, notice how we’re no longer returning `name` from each method. That’s because each guard already returns its topic, so we don’t have to do that anymore. Another small win.

With these in place we can now declare our `Person` this way:

```ruby
class Person
  extend Portrayal
  extend ReusableTypes

  age keyword :age
  nullable string keyword :favorite_beer, default: nil

  guard('favorite_beer is only allowed for age >=21') {
    favorite_beer.nil? || age >= 21
  }  
end
```

Or this way if you made methods capitalized:

```ruby
Age keyword :age
Nullable String keyword :favorite_beer, default: nil
```

Or this way, if you like to keep keyword on the left:

```ruby
keyword Age(:age)
keyword Nullable(String :favorite_beer), default: nil
```

Or this way, if you don’t want to intefere with keywords:

```ruby
Age :age
Nullable String :favorite_beer

keyword :age
keyword :favorite_beer, default: nil
```

Or go back to plain guard declarations. Whatever you fancy.

Keep in mind, we only learned 2 methods so far: `guard` and `pass!` (well, maybe also `update` if you’re pedantic). The rest is just plain Ruby.

### Listing guards

Just for fun, I wanted to be able to list guards declared on a class. It’s possible with `Person.portrayal.list_guards`, which returns the following:

```ruby
> Person.portrayal.list_guards
=> {:age=>["age must be an integer", "age must be between 0 and 130"],
 :favorite_beer=>["favorite_beer can be nil", "favorite_beer must be string"],
 :base=>["favorite_beer is only allowed for age >=21"]}
```

## Where is this lib?

At the time of this writing the implementation is [just a gist](https://gist.github.com/maxim/12e086f23f7ae9d230a2895bbb519483). I’m curious what people think about this before I make it into a proper gem. Let me know your thoughts. Too crazy? Or not crazy enough? :)