The letter A styled as Alchemists logo. lchemists
Published July 27, 2023 Updated October 1, 2023
Wholable Icon



Wholable is a mixin that allows you to turn your object into a whole value object by ensuring object equality is determined by the values of the object instead of by identity. Whole value objects — or value objects in general — have the following traits as also noted via Wikipedia:

  • Equality is determined by the values that make up an object and not by identity (i.e. memory address) which is the default behavior for all Ruby objects except for Data and Structs.

  • Identity remains unique since two objects can have the same values but different identity. This means BasicObject#equal? is never overwritten — which is strongly discouraged — as per BasicObject documentation.

  • Value objects should be immutable (i.e. frozen) by default. This implementation enforces a strict adherence to immutability in order to ensure value objects remain equal and discourage mutation.


  • Ensures equality (i.e. #== and #eql?) is determined by attribute values and not object identity (i.e. #equal?).

  • Allows you to compare two objects of same or different types and see their differences.

  • Provides pattern matching.

  • Automatically defines public attribute readers (i.e. .attr_reader) based on provided keys.

  • Ensures object inspection (i.e. #inspect) shows all registered attributes.

  • Ensures object is frozen upon initialization.


  1. Ruby.


To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location
gem install wholable --trust-policy HighSecurity

To install without security, run:

gem install wholable

You can also add the gem directly to your project:

bundle add wholable

Once the gem is installed, you only need to require it:

require "wholable"


To use, include Wholable along with a list of attributes that make up your whole value object:

class Person
  include Wholable[:name, :email]

  def initialize name:, email:
    @name = name
    @email = email

jill = name: "Jill Smith", email: ""
jill_two = name: "Jill Smith", email: ""
jack = name: "Jack Smith", email: ""              # "Jill Smith"             # ""

jill.frozen?           # true
jill_two.frozen?       # true
jack.frozen?           # true

jill.inspect           # "#<Person @name=\"Jill Smith\", @email=\"\">"
jill_two.inspect       # "#<Person @name=\"Jill Smith\", @email=\"\">"
jack.inspect           # "#<Person @name=\"Jack Smith\", @email=\"\">"

jill == jill           # true
jill == jill_two       # true
jill == jack           # false

jill.diff(jill)        # {}
jill.diff(jack)        # {
                       #   name: ["Jill Smith", "Jack Smith"],
                       #   email: ["", ""]
                       # }
jill.diff(  # {:name=>["Jill Smith", nil], :email=>["", nil]}

jill.eql? jill         # true
jill.eql? jill_two     # true
jill.eql? jack         # false

jill.equal? jill       # true
jill.equal? jill_two   # false
jill.equal? jack       # false

jill.hash              # 3650965837788801745
jill_two.hash          # 3650965837788801745
jack.hash              # 4460658980509842640

jill.to_a              # ["Jill Smith", ""]
jack.to_a              # ["Jack Smith", ""]

jill.to_h              # {:name=>"Jill Smith", :email=>""}
jack.to_h              # {:name=>"Jack Smith", :email=>""}

jill.with name: "Sue"  # #<Person @name="Sue", @email="">
jill.with bad: "!"     # unknown keyword: :bad (ArgumentError)

As you can see, object equality is determined by the object’s values and not by the object’s identity. When you include Wholable along with a list of keys, the following happens:

  1. The corresponding public attr_reader for each key is created which saves you time and reduces double entry when implementing your whole value object.

  2. The #to_a and #to_h methods are added for convenience in order to play nice with Data and Structs.

  3. The #deconstruct and #deconstruct_keys aliases are created for you so you can leverage pattern matching.

  4. Custom #==, #eql?, #hash, #inspect, #to_a, #to_h, and #with methods are added to provide whole value behavior.

  5. The object is immediately frozen after initialization to ensure your instance is immutable by default.


Whole values can be broken via the following:

  • Duplication: Sending the #dup message will cause your whole value object to be unfrozen. This might be desired in certain situations but make sure to refreeze when able.

  • Post Attributes: Adding additional attributes after what is defined when including Wholable will break your whole value object. To prevent this, let Wholable manage this for you (easiest). Otherwise (harder), you can manually override #==, #eql?, #hash, #inspect, #to_a, and #to_h behavior at which point you don’t need Wholable anymore.

  • Deep Freezing: The automatic freezing of your instances is shallow and will not deeply freeze nested attributes. This behavior mimics the behavior of Data objects.


This implementation is based upon these original designs:

  • Equalizer: One of the first implementations that is over a decade old and no longer maintained.

  • Dry Equalizer: Deprecated and no longer maintained but was based upon the above implementation and has now moved into Dry Core.

  • Dry Core: Includes the Dry::Core::Equalizer module which is officially supported and actively maintained by the Dry RB team. A good alternative to this gem.

  • Equatable: A similar implementation to the above but is based off what you define via your .attr_reader. The project hasn’t been maintained or updated in several years.


To contribute, run:

git clone
cd wholable

You can also use the IRB console for direct access to all objects:



To test, run: