Idiomatic Ruby Type Conversion

Published on

Built-in Ruby types include a method with the same name as the type to idempotently convert some value into that type.

irb(main):001:0> Array('some value')
=> ["some value"]
irb(main):002:0> Array(['some value'])
=> ["some value"]

This can be done with custom types/objects, but there are a couple of gotchas.

  1. The converter method is named after a class, it’s not a class method, so it must be defined outside the class. Unless something like Email.Email(object) is acceptable 😅.
  2. Since the converter method is not directly linked to the class it belongs to, there can be autoloading issues in Rails applications.

These two gotchas allow for a few different ways to manage these converter methods.

One strategy is avoiding copying the built-in converter methods.

Although defining uppercased methods for your custom classes to create instances of it looks like an interesting idea at first glance, it is rather confusing.
Consider defining class.[] instead, which enables a very similar syntax, but uses the real constant it belongs to. An example of such usage is Set

class Email
  def self.[](obj)
    return obj if obj.is_a?(self)

    new(obj)
  end
end

In a situation where constant-based autoloading is not a concern, the converter method could be defined in the same file as the class.

# lib/email.rb

class Email
  def initialize(email)
    @email = email
  end
end

def Email(obj)
  return obj if obj.is_a?(Email)

  Email.new(obj)
end

In a Rails application, another option to allow for Email() that might be controversial but still works: define the converters in an initializer.

# config/initializers/vale_objects.rb
def Email(obj)
  return obj if obj.is_a?(Email)

  Email.new(obj)
end
# app/value_objects/email.rb
class Email
  def initialize(email)
    @email = email
  end
end