<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://denisdefreyne.com/">
  <id>https://denisdefreyne.com/</id>
  <title>Denis Defreyne</title>
  <updated>2023-01-05T23:00:00Z</updated>
  <link rel="alternate" href="https://denisdefreyne.com/" type="text/html"/>
  <link rel="self" href="https://denisdefreyne.com/feed.xml" type="application/atom+xml"/>
  <author>
    <name>Denis Defreyne</name>
    <uri>https://denisdefreyne.com</uri>
  </author>
  <entry>
    <id>tag:denisdefreyne.com,2023-01-05:/articles/2023-state-pattern/</id>
    <title type="html">Avoiding bugs in Ruby code using the state pattern</title>
    <published>2023-01-05T23:00:00Z</published>
    <updated>2023-01-05T23:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2023-state-pattern/" type="text/html"/>
    <content type="html">&lt;p&gt;“Be more careful” is often not useful advice. Software bugs are inevitable, but as I become proficient in software development, my code tends to have fewer initial bugs.&lt;/p&gt;&lt;p&gt;In some part, this is because experience has taught me to spot bugs more quickly. But more importantly, I’ve learned techniques to structure code in ways that make bugs much less likely to arise in the first place.&lt;/p&gt;&lt;p&gt;One such technique is the &lt;em&gt;state pattern&lt;/em&gt;. In this article, I’ll take you through an example, at first with a brittle implementation, and then reworked using the state pattern into something more stable.&lt;/p&gt;&lt;h2 id="the-example-a-messaging-platform" data-skip-toc="false" data-numbering="1"&gt;&lt;span&gt;&lt;span&gt;The example: A messaging platform&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The example I’ll use throughout this article is a simplified messaging platform. On this platform, any person can register:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;account = Account.register(account_details)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The account details, passed to the &lt;code&gt;.register&lt;/code&gt; method, would include at least an email address, but also properties such as first name and last name.&lt;/p&gt;&lt;p&gt;Before the registered account is usable, the person needs to confirm the account, using a code they received via email:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;account.confirm("ABC123")&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, an administrator is expected to approve the account before it can be used:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;account.approve&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With the account confirmed (by the person registering) and approved (by an administrator), the person is now free to use their account to send messages to other people:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;account.message(
  tim,
  "I’m selling these fine leather jackets.",
)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It is important that people &lt;em&gt;cannot&lt;/em&gt; send messages unless their account is both confirmed and approved.&lt;sup id="fnref:1" role="doc-noteref"&gt;&lt;a href="#fn:1" class="footnote" rel="footnote"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;p&gt;Let’s take a look at two different ways of implementing this: a naïve implementation, and a safer implementation that uses the state pattern.&lt;/p&gt;&lt;h2 id="a-na-ve-implementation" data-skip-toc="false" data-numbering="2"&gt;&lt;span&gt;&lt;span&gt;A naïve implementation&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The simplest implementation (that I can think of) starts with an initializer:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Account
  def initialize
    @code = "ABC123"
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The initializer sets the confirmation code. In a real-world scenario, the confirmation code would be randomly generated. Here, it is hardcoded for the sake of simplicity.&lt;/p&gt;&lt;p&gt;We’ll also need an implementation for the &lt;code&gt;Account.register&lt;/code&gt; method, which we used earlier:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def self.register(account_details)
    new
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For this demo implementation, all it does is create a new instance of &lt;code&gt;Account&lt;/code&gt;. In a real-world scenario, this might be the place to create a new database record and send out an email with the confirmation code. For simplicity, all of that is left out.&lt;/p&gt;&lt;p&gt;We’ll need &lt;code&gt;#confirm&lt;/code&gt;, which checks whether the given code is correct, and advances the state to &lt;code&gt;:confirmed&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def confirm(code)
    if code != @code
      raise InvalidConfirmationCode
    end

    @state = :confirmed
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next up is &lt;code&gt;#approve&lt;/code&gt;, which advances the state to &lt;code&gt;:confirmed_and_approved&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def approve
    @state = :confirmed_and_approved
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, we have &lt;code&gt;#message&lt;/code&gt;, which is the method used for sending messages to other accounts. It requires that the account is in the &lt;code&gt;:confirmed_and_approved&lt;/code&gt; state:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def message(who, text)
    unless @state == :confirmed_and_approved
      raise AccountNotApproved
    end

    puts "Sending message to #{who}"
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this example implementation of &lt;code&gt;#message&lt;/code&gt;, we just log the message.&lt;/p&gt;&lt;p&gt;We’ll also need these two exception classes:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;AccountNotApproved = Class.new(StandardError)
InvalidConfirmationCode = Class.new(StandardError)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here is an example that ties it all together:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tims_account_id = 12358

account = Account.new
account.confirm("ABC123")
account.approve
account.message(tims_account_id, "What’s up, Tim?")&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, we confirm the account, then an administrator approves the account, and lastly we use the account to send a message to Tim, whose account ID is 12358. The terminal output now shows this:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;Sending message to 12358.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So far, so good.&lt;/p&gt;&lt;p&gt;But what if we confirm the account again, after it has already been approved?&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tims_account_id = 12358

account = Account.new
account.confirm("ABC123")
account.approve
&lt;mark&gt;account.confirm("ABC123")&lt;/mark&gt;
account.message(tims_account_id, "What’s up, Tim?")&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Running this example results in an error during the call to the &lt;code&gt;#message&lt;/code&gt; method:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;account.rb:9:in `message':
  AccountNotApproved (AccountNotApproved)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;AccountNotApproved&lt;/code&gt;?! The account definitely &lt;em&gt;was&lt;/em&gt; approved. The issue is that our implementation of &lt;code&gt;#confirm&lt;/code&gt; set the state to &lt;code&gt;:confirmed&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def confirm(code)
    if code != @code
      raise InvalidConfirmationCode
    end

    &lt;mark&gt;@state = :confirmed&lt;/mark&gt;
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, we have a bug: the state shouldn’t be set to &lt;code&gt;:confirmed&lt;/code&gt; if the account is already approved. One quick fix for this would be to zero out the &lt;code&gt;@code&lt;/code&gt; variable, so that attempting to confirm again would fail:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def confirm(code)
    if code != @code
      raise InvalidConfirmationCode
    end

    @state = :confirmed
    &lt;mark&gt;@code = ""&lt;/mark&gt;
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is a bit of a hack, though. A slightly better way to solve this is to only allow confirmation when the &lt;code&gt;@state&lt;/code&gt; is the initial state:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def confirm(code)
    &lt;mark&gt;return if @state != :initial&lt;/mark&gt;

    if code != @code
      raise InvalidConfirmationCode
    end

    @state = :confirmed
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We’d also need to set the initial &lt;code&gt;@state&lt;/code&gt; in the initializer:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def initialize
    @code = "ABC123"
    &lt;mark&gt;@state = :initial&lt;/mark&gt;
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that bug solved, let us take a look at another issue: it is possible for an account to be approved without having gone through confirmation at all:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tims_account_id = 12358

account = Account.new
account.approve
account.message(tims_account_id, "What’s up, Tim?")&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The terminal output now shows this:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;Sending message to 12358.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This might be a bug, or it might not be. Perhaps it is intentional that administrators can approve accounts, skipping confirmation. Or perhaps this is an oversight in our implementation.&lt;/p&gt;&lt;p&gt;If we assume it is an oversight, and thus a bug, one way of fixing it is to verify that the state is what we expect it to be:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def approve
    &lt;mark&gt;raise AccountNotConfirmed if @state != :confirmed&lt;/mark&gt;

    @state = :confirmed_and_approved
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We’ll also need to define a new exception:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;mark&gt;AccountNotConfirmed = Class.new(StandardError)&lt;/mark&gt;
AccountNotApproved = Class.new(StandardError)
InvalidConfirmationCode = Class.new(StandardError)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code is now — as far as I can tell — bug-free, though I’m not comfortable with the end result. This code feels brittle to me: any change in the future has a high chance of inadvertently modifying the expected behavior.&lt;/p&gt;&lt;p&gt;This could would need an extensive test suite. Such a test suite would verify that going through all the different paths, in all different orders, yield correct results. The presence of a test suite would make me feel more comfortable, but there is more that we can do.&lt;/p&gt;&lt;p&gt;Let’s now take a look at a new implementation, which uses &lt;code&gt;@state&lt;/code&gt; rather differently.&lt;/p&gt;&lt;h2 id="a-safer-implementation" data-skip-toc="false" data-numbering="3"&gt;&lt;span&gt;&lt;span&gt;A safer implementation&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In this implementation, the &lt;code&gt;Account&lt;/code&gt; class no longer contains the functionality for confirming, approving, and messaging. Rather, all of that is delegated to &lt;code&gt;@state&lt;/code&gt;, which is now its own object:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Account
  attr_accessor :state

  def initialize
    @state = InitialAccountState.new(self)
  end

  def confirm(code)
    @state.confirm(code)
  end

  def approve
    @state.approve
  end

  def message(who, text)
    @state.message(who, text)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You could go fancy and use Ruby’s built-in &lt;code&gt;Forwardable&lt;/code&gt; module for that if you want. With that module, the implementation of &lt;code&gt;Account&lt;/code&gt; could look like this — doing exactly the same:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Account
  &lt;mark&gt;extend Forwardable&lt;/mark&gt;
  &lt;mark&gt;def_delegators :@state, :confirm, :approve, :message&lt;/mark&gt;

  attr_accessor :state

  def initialize
    @state = InitialAccountState.new(self)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here is what &lt;code&gt;InitialAccountState&lt;/code&gt; looks like:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class InitialAccountState
  def initialize(account)
    @code = "ABC123"
    @account = account
  end

  def confirm(code)
    if code != @code
      raise InvalidConfirmationCode
    end

    @account.state = ConfirmedAccountState.new(@account)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;InitialAccountState#confirm&lt;/code&gt; method is quite similar to the original &lt;code&gt;#confirm&lt;/code&gt;: it checks the confirmation codes, and raises an exception if they don’t match.&lt;/p&gt;&lt;p&gt;While the original &lt;code&gt;#confirm&lt;/code&gt; changed the state using &lt;code&gt;@state = :confirmed&lt;/code&gt;, this new &lt;code&gt;#confirm&lt;/code&gt; method changes the account state to a new state object. (We’ll get to the implementation of &lt;code&gt;ConfirmedAccountState&lt;/code&gt; in a bit.)&lt;/p&gt;&lt;p&gt;Also worth noting is that the confirmation code lives in the &lt;code&gt;InitialAccountState&lt;/code&gt; instance. That is the only place where it is useful. It could also live in &lt;code&gt;Account&lt;/code&gt;, but it wouldn’t have a purpose there.&lt;/p&gt;&lt;p&gt;Without having the other state objects implemented, we can already see that one of the buggy behaviors from earlier no longer silently succeeds:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tims_account_id = 12358

account = Account.new
account.approve
account.message(tims_account_id, "What’s up, Tim?")&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This piece of code raises a &lt;code&gt;NoMethodError&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;account_with_state.rb:13:
  in `approve': undefined method `approve' for
  #&amp;lt;InitialAccountState:0x0000000100907fa8 …&amp;gt;
  (NoMethodError)

  @state.approve(code)
        ^^^^^^^^
      from account_with_state.rb:66:in `&amp;lt;main&amp;gt;'&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This &lt;code&gt;NoMethodError&lt;/code&gt; is intentional! It signals that there hasn’t been any thought put into the situation where someone would try to approve an account that hasn’t been confirmed yet.&lt;/p&gt;&lt;p&gt;This &lt;code&gt;NoMethodError&lt;/code&gt; is a replacement for undefined behavior. I believe that it is preferable to get a &lt;code&gt;NoMethodError&lt;/code&gt; than to execute incorrect behavior.&lt;/p&gt;&lt;p&gt;We are still able to implement &lt;code&gt;#approve&lt;/code&gt; here, if we wish. Perhaps it &lt;em&gt;is&lt;/em&gt; desirable to approve accounts from their initial state, bypassing confirmation. If so, we could implement &lt;code&gt;#approve&lt;/code&gt; as follows:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class InitialAccountState&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;

  def approve
    @account.state = ConfirmedAndApprovedAccountState.new(@account)
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let’s move on to &lt;code&gt;ConfirmedAccountState&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class ConfirmedAccountState
  def initialize(account)
    @account = account
  end

  def approve
    @account.state = ConfirmedAndApprovedAccountState.new(@account)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;#approve&lt;/code&gt; method exists here, and moves the state forward to the “account approved” state.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;ConfirmedAccountState&lt;/code&gt; state has no &lt;code&gt;#confirm&lt;/code&gt; method here. If we were to try to confirm an already approved account, we’d get a &lt;code&gt;NoMethodError&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;account_with_state.rb:28:
  in `confirm': undefined method `confirm' for
  #&amp;lt;ConfirmedAccountState:0x0000000100907fa8 …&amp;gt;
  (NoMethodError)

  @state.confirm(code)
        ^^^^^^^^
      from account_with_state.rb:66:in `&amp;lt;main&amp;gt;'&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, the &lt;code&gt;NoMethodError&lt;/code&gt; is less desirable. I would probably implement &lt;code&gt;#confirm&lt;/code&gt; anyway, and have it do nothing:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class ConfirmedAccountState&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;

  def confirm
    # Already confirmed; do nothing
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This makes the behavior explicit: confirming an account that is already confirmed does nothing. The &lt;code&gt;#confirm&lt;/code&gt; method in our naïve implementation also did nothing in this case, but it wasn’t nearly as explicit.&lt;/p&gt;&lt;p&gt;Lastly, we have &lt;code&gt;ConfirmedAndApprovedAccountState&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class ConfirmedAndApprovedAccountState
  def initialize(account)
    @account = account
  end

  def message(who, text)
    puts "Sending message to #{who}"
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This state is the least interesting: it just has the &lt;code&gt;#message&lt;/code&gt; for sending messages.&lt;/p&gt;&lt;p&gt;For this state, it makes sense to implement the &lt;code&gt;#confirm&lt;/code&gt; and &lt;code&gt;#approve&lt;/code&gt; methods, and have them do nothing:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class ConfirmedAndApprovedAccountState&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;

  def confirm
    # Already confirmed; do nothing
  end

  def approve
    # Already approved; do nothing
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With all that, we have an implementation where the state transitions are explicit, and it is also more clear what actions can be taken in which states.&lt;/p&gt;&lt;h2 id="closing-thoughts" data-skip-toc="false" data-numbering="4"&gt;&lt;span&gt;&lt;span&gt;Closing thoughts&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The implementation which uses the state pattern makes me the most comfortable. It avoids undefined behavior, and even though &lt;code&gt;NoMethodError&lt;/code&gt;s are a little nasty, they’re better to have than undefined behavior.&lt;/p&gt;&lt;p&gt;There is nothing preventing us from implementing &lt;em&gt;all&lt;/em&gt; methods, and avoiding &lt;code&gt;NoMethodError&lt;/code&gt; altogether. For each combination of state and method, we’d have to think about what the behavior should be. Perhaps it is doing nothing, perhaps it is raising a specific exception, or perhaps it is doing something else entirely.&lt;/p&gt;&lt;p&gt;Defining the behavior for each combination of state and method is not always easy. This can make the state pattern look cumbersome and difficult. However, if we want high-quality software, we can’t get away from defining behavior anyway. It seemed to be optional in the original, naïve implementation, but that led to bugs as a result.&lt;/p&gt;&lt;p&gt;I prefer explicit, readable code over compact code every time. Explicit code makes it easier to find and prevent bugs. Explicit, readable code is less likely to break over time, even in a codebase with a large amount of churn.&lt;/p&gt;&lt;p&gt;Back in university, a professor said to me that &lt;code&gt;if&lt;/code&gt; statements are a code smell. While I think that is an over-generalisation, you’ll find that the original, naïve implementation has quite some &lt;code&gt;if&lt;/code&gt;s — especially in the bug fixes — while the state pattern implementation has few.&lt;/p&gt;&lt;p&gt;If in the future I find myself in a situation where behavior depends on state, you can be sure I’ll whip out the state pattern.&lt;/p&gt;&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol class="stack-3"&gt;
&lt;li id="fn:1" role="doc-endnote"&gt;&lt;p&gt;That should cut down on the cryptocurrency/NFT spam. That is unfortunately still a thing, right? &lt;a href="#fnref:1" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <summary type="html">The state pattern is a technique that is useful for structuring code in a way that makes bugs less likely to arise. This article walks through a simple example to show how the state pattern works.</summary>
  </entry>
  <entry>
    <id>tag:denisdefreyne.com,2022-05-25:/articles/2022-ruby-equality/</id>
    <title type="html">The complete guide to implementing equality in Ruby</title>
    <published>2022-05-25T22:00:00Z</published>
    <updated>2022-05-25T22:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2022-ruby-equality/" type="text/html"/>
    <content type="html">&lt;p&gt;Ruby is one of the few programming languages that gets equality &lt;em&gt;right&lt;/em&gt;. I often play around with other languages, but keep coming back to Ruby. In large part, this is because Ruby’s implementation of equality is so nice.&lt;sup id="fnref:1" role="doc-noteref"&gt;&lt;a href="#fn:1" class="footnote" rel="footnote"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;p&gt;Nonetheless, equality in Ruby is not straightforward. There is &lt;code&gt;#==&lt;/code&gt;, &lt;code&gt;#eql?&lt;/code&gt;, &lt;code&gt;#equal?&lt;/code&gt;, &lt;code&gt;#===&lt;/code&gt;, and more. Even if you’re familiar with using them, &lt;em&gt;implementing&lt;/em&gt; them can be a whole other story.&lt;/p&gt;&lt;p&gt;Let’s walk through all forms of equality in Ruby and how to implement them.&lt;/p&gt;&lt;ol class="toc toc__list toc__root-list" role="doc-toc"&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#why-implementing-equality-matters"&gt;&lt;span&gt;&lt;span&gt;Why implementing equality matters&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#entities"&gt;&lt;span&gt;&lt;span&gt;Entities&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#value-objects"&gt;&lt;span&gt;&lt;span&gt;Value objects&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#basic-equality-double-equals"&gt;&lt;span&gt;&lt;span&gt;Basic equality (double equals)&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#basic-equality-for-value-objects"&gt;&lt;span&gt;&lt;span&gt;Basic equality for value objects&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#basic-equality-for-entities"&gt;&lt;span&gt;&lt;span&gt;Basic equality for entities&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#basic-equality-with-type-coercion"&gt;&lt;span&gt;&lt;span&gt;Basic equality with type coercion&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#strict-equality"&gt;&lt;span&gt;&lt;span&gt;Strict equality&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#hash-equality"&gt;&lt;span&gt;&lt;span&gt;Hash equality&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#the-eql-method"&gt;&lt;span&gt;&lt;span&gt;The #eql? method&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#the-hash-method"&gt;&lt;span&gt;&lt;span&gt;The #hash method&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#putting-it-together"&gt;&lt;span&gt;&lt;span&gt;Putting it together&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#case-equality-triple-equals"&gt;&lt;span&gt;&lt;span&gt;Case equality (triple equals)&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#ordered-comparison"&gt;&lt;span&gt;&lt;span&gt;Ordered comparison&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#wrapping-up"&gt;&lt;span&gt;&lt;span&gt;Wrapping up&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#further-reading"&gt;&lt;span&gt;&lt;span&gt;Further reading&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h2 id="why-implementing-equality-matters" data-skip-toc="false" data-numbering="1"&gt;&lt;span&gt;&lt;span&gt;Why implementing equality matters&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We check whether objects are equal &lt;em&gt;all the time&lt;/em&gt;. Sometimes we do this explicitly, sometimes implicitly. Here are some examples:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Do these two &lt;code&gt;Employee&lt;/code&gt;s work in the same &lt;code&gt;Team&lt;/code&gt;? Or, in code: &lt;code&gt;denis.team == someone.team&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is the given &lt;code&gt;DiscountCode&lt;/code&gt; valid for this particular &lt;code&gt;Product&lt;/code&gt;? Or, in code: &lt;code&gt;product.discount_codes.include?(given_discount_code)&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who are the (distinct) managers for this given group of employees? Or, in code: &lt;code&gt;employees.map(&amp;amp;:manager).uniq&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;A good implementation of equality is predictable; it aligns with our understanding of equality.&lt;/p&gt;&lt;p&gt;An incorrect implementation of equality, on the other hand, conflicts with what we commonly assume to be true. Here is an example of what happens with such an incorrect implementation:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;# Find the book “Gödel, Escher, Bach”
repo = BookRepository.new
geb      = repo.find(isbn: "978-0-14-028920-6")
geb_also = repo.find(isbn: "978-0-14-028920-6")

geb == geb_also # =&amp;gt; false?!&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;geb&lt;/code&gt; and &lt;code&gt;geb_also&lt;/code&gt; objects should definitely be equal. The fact that the code says they’re not, is bound to cause bugs down the line. Luckily, we can implement equality ourselves and avoid this class of bugs.&lt;/p&gt;&lt;p&gt;No one-size-fits-all solution exists for an equality implementation. However, there are two kinds of objects where we &lt;em&gt;do&lt;/em&gt; have a general pattern for implementing equality: &lt;i&gt;entities&lt;/i&gt; and &lt;i&gt;value objects&lt;/i&gt;. These two terms come from &lt;i&gt;domain-driven design&lt;/i&gt; (DDD for short), but they’re relevant even if you’re not using DDD. Let’s take a closer look.&lt;/p&gt;&lt;h3 id="entities" data-skip-toc="false" data-numbering="1.1"&gt;&lt;span&gt;&lt;span&gt;Entities&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Entities are objects that have an explicit identity attribute. Often, entities are stored in some database and have a unique &lt;code&gt;id&lt;/code&gt; attribute, corresponding to a unique &lt;code&gt;id&lt;/code&gt; table column. The following &lt;code&gt;Employee&lt;/code&gt; example class is such an entity:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Employee
  attr_reader :id
  attr_reader :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two entities are equal when their IDs are equal. All other attributes are ignored. After all, an employee’s name might change, but that does not change their identity. Imagine getting married, changing your name and not getting paid anymore because HR has no clue who you are anymore!&lt;/p&gt;&lt;p&gt;ActiveRecord, the ORM that is part of Ruby on Rails, calls entities &lt;i&gt;models&lt;/i&gt; instead, but they’re the same concept. These model objects automatically have an ID. In fact, ActiveRecord models already implement equality correctly out of the box!&lt;/p&gt;&lt;h3 id="value-objects" data-skip-toc="false" data-numbering="1.2"&gt;&lt;span&gt;&lt;span&gt;Value objects&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Value objects are objects without an explicit identity. Instead, their value as a whole constitutes identity. Consider this &lt;code&gt;Point&lt;/code&gt; class:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Point
  attr_reader :x
  attr_reader :y

  def initialize(x, y)
    @x = x
    @y = y
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two &lt;code&gt;Point&lt;/code&gt;s will be equal if their &lt;i&gt;x&lt;/i&gt; and &lt;i&gt;y&lt;/i&gt; values are equal. The &lt;i&gt;x&lt;/i&gt; and &lt;i&gt;y&lt;/i&gt; values constitute the identity of the point.&lt;/p&gt;&lt;p&gt;In Ruby, the basic value object types are numbers (both integers and floating-point numbers), characters, booleans, and &lt;code&gt;nil&lt;/code&gt;. For these basic types, equality works out of the box:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;17 == 17      # =&amp;gt; true
false != 12   # =&amp;gt; true
1.2 == 3.4    # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Arrays of value objects are in themselves also value objects. Equality for arrays of value objects works out of the box — for example, &lt;code&gt;[17, true] == [17, true]&lt;/code&gt;. This might seem obvious, but this is not true in all programming languages.&lt;/p&gt;&lt;p&gt;Other examples of value objects are timestamps, date ranges, time intervals, colors, 3D coordinates, and money objects. These are built from other value objects: for example, a money object consists of a fixed-decimal number and a currency code string.&lt;/p&gt;&lt;h2 id="basic-equality-double-equals" data-skip-toc="false" data-numbering="2"&gt;&lt;span&gt;&lt;span&gt;Basic equality (double equals)&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Ruby has the &lt;code&gt;==&lt;/code&gt; and &lt;code&gt;!=&lt;/code&gt; operators for checking whether two objects are equal or not:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;"orange" == "red"   # =&amp;gt; false
1 + 3 == 4          # =&amp;gt; true
12 - 1 != 10        # =&amp;gt; true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ruby’s built-in types all have a sensible implementation of &lt;code&gt;==&lt;/code&gt;. Some frameworks and libraries provide custom types, which will have a sensible implementation of &lt;code&gt;==&lt;/code&gt;, too. Here is an example with ActiveRecord:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;geb = Book.find_by(title: "Gödel, Escher, Bach")
also_geb = Book.find_by(isbn: "978-0-14-028920-6")

geb == also_geb   # =&amp;gt; true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For custom classes, the &lt;code&gt;==&lt;/code&gt; operator returns true if and only if the two objects are the exact same instance. Ruby does this by checking whether the internal object IDs are equal. These internal object IDs are accessible using &lt;code&gt;#__id__&lt;/code&gt;. Effectively, &lt;code&gt;gizmo == thing&lt;/code&gt; is the same as &lt;code&gt;gizmo.__id__ == thing.__id__&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;This behavior is often not a good default, however. To illustrate this, consider the &lt;code&gt;Point&lt;/code&gt; class from earlier:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Point
  attr_reader :x
  attr_reader :y

  def initialize(x, y)
    @x = x
    @y = y
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;==&lt;/code&gt; operator will return &lt;code&gt;true&lt;/code&gt; only when calling it on itself:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;a = Point.new(4, 6)
b = Point.new(3, 7)
a_also = Point.new(4, 6)

a == a        # =&amp;gt; true
a == b        # =&amp;gt; false
a == "soup"   # =&amp;gt; false
a == a_also   # =&amp;gt; false?!&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This default behavior is often undesirable in custom classes. After all, two points are equal if (and only if) their &lt;i&gt;x&lt;/i&gt; and &lt;i&gt;y&lt;/i&gt; values are equal. This behavior is undesirable for value objects (such as &lt;code&gt;Point&lt;/code&gt;), and entities (such as the &lt;code&gt;Employee&lt;/code&gt; class mentioned earlier).&lt;/p&gt;&lt;p&gt;The desired behavior for value objects and entities is as follows:&lt;/p&gt;&lt;div class="figureset figureset--wide"&gt;
&lt;div class="figureset__figure"&gt;
&lt;div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no" ?&gt;

&lt;svg width="170px" height="170px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1.5;"&gt;
    &lt;rect id="Equality-for-value-objects" serif:id="Equality for value objects" x="0" y="0" width="170" height="170" style="fill:none;"&gt;&lt;/rect&gt;
    &lt;g id="Equality-for-value-objects1" serif:id="Equality for value objects"&gt;
        &lt;path d="M20,48L20,112" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;rect x="8" y="48" width="64" height="64" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/rect&gt;
        &lt;path d="M110,48L110,112" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;rect x="98" y="48" width="64" height="64" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/rect&gt;
        &lt;text x="24px" y="64.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;__id__&lt;/text&gt;
        &lt;text x="114px" y="64.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;__id__&lt;/text&gt;
        &lt;text x="24px" y="84.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@x&lt;/text&gt;
        &lt;text x="114px" y="84.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@x&lt;/text&gt;
        &lt;text x="24px" y="104.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@y&lt;/text&gt;
        &lt;text x="114px" y="104.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@y&lt;/text&gt;
        &lt;circle cx="65" cy="80" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;circle cx="105" cy="80" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;path d="M66.8,80L103.2,80" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;circle cx="65" cy="100" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;circle cx="105" cy="100" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;path d="M66.8,100L103.2,100" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;strong&gt;For value objects&lt;/strong&gt;, we’d like to check whether &lt;em&gt;all&lt;/em&gt; attributes are equal.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="figureset__figure"&gt;
&lt;div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no" ?&gt;

&lt;svg width="170px" height="170px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1.5;"&gt;
    &lt;rect id="Equality-for-entities" serif:id="Equality for entities" x="0" y="0" width="170" height="170" style="fill:none;"&gt;&lt;/rect&gt;
    &lt;g id="Equality-for-entities1" serif:id="Equality for entities"&gt;
        &lt;path d="M110,48L110,112" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;rect x="98" y="48" width="64" height="64" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/rect&gt;
        &lt;path d="M20,48L20,112" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;rect x="8" y="48" width="63" height="65" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/rect&gt;
        &lt;text x="24px" y="64.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;__id__&lt;/text&gt;
        &lt;text x="114px" y="64.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;__id__&lt;/text&gt;
        &lt;text x="24px" y="84.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@id&lt;/text&gt;
        &lt;text x="114px" y="84.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@id&lt;/text&gt;
        &lt;text x="24px" y="104.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@name&lt;/text&gt;
        &lt;text x="114px" y="104.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@name&lt;/text&gt;
        &lt;circle cx="65" cy="80" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;circle cx="105" cy="80" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;path d="M66.8,80L103.2,80" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;strong&gt;For entities&lt;/strong&gt;, we’d like to check whether the explicit ID attributes are equal.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="figureset__figure"&gt;
&lt;div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no" ?&gt;

&lt;svg width="170px" height="170px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1.5;"&gt;
    &lt;rect id="Equality-by-default" serif:id="Equality by default" x="0" y="0" width="170" height="170" style="fill:none;"&gt;&lt;/rect&gt;
    &lt;g id="Equality-by-default1" serif:id="Equality by default"&gt;
        &lt;path d="M20,48L20,112" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;rect x="8" y="48" width="64" height="64" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/rect&gt;
        &lt;path d="M110,48L110,112" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;rect x="98" y="48" width="64" height="64" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/rect&gt;
        &lt;text x="24px" y="64.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;__id__&lt;/text&gt;
        &lt;text x="114px" y="64.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;__id__&lt;/text&gt;
        &lt;text x="24px" y="84.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@title&lt;/text&gt;
        &lt;text x="114px" y="84.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@title&lt;/text&gt;
        &lt;text x="24px" y="104.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@role&lt;/text&gt;
        &lt;text x="114px" y="104.41px" style="font-family:var(--font-code);font-size:12px;fill:var(--color-figure--text);"&gt;@role&lt;/text&gt;
        &lt;circle cx="65" cy="60" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;circle cx="105" cy="60" r="3" style="fill:var(--color-figure--intense);"&gt;&lt;/circle&gt;
        &lt;path d="M66.8,60L103.2,60" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;strong&gt;By default&lt;/strong&gt;, Ruby checks whether the internal object IDs are equal.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Instances of &lt;code&gt;Point&lt;/code&gt; are value objects. With the above in mind, a good implementation of &lt;code&gt;==&lt;/code&gt; for &lt;code&gt;Point&lt;/code&gt; would look as follows:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    self.class == other.class &amp;amp;&amp;amp;
      @x == other.x &amp;amp;&amp;amp;
      @y == other.y
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This implementation checks all attributes and the &lt;code&gt;class&lt;/code&gt; of both objects. By checking the class, checking equality of a &lt;code&gt;Point&lt;/code&gt; instance and something of a different class return &lt;code&gt;false&lt;/code&gt; rather than raise an exception.&lt;/p&gt;&lt;p&gt;Checking equality on &lt;code&gt;Point&lt;/code&gt; objects now works as intended:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;a = Point.new(4, 6)
b = Point.new(3, 7)
a_also = Point.new(4, 6)

a == a        # =&amp;gt; true
a == b        # =&amp;gt; false
a == "soup"   # =&amp;gt; false
a == a_also   # =&amp;gt; true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;!=&lt;/code&gt; operator works too:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;a != b          # =&amp;gt; true
a != "rabbit"   # =&amp;gt; true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A correct implementation of equality has three properties: &lt;i&gt;reflexivity&lt;/i&gt;, &lt;i&gt;symmetry&lt;/i&gt;, and &lt;i&gt;transitivity&lt;/i&gt;.&lt;/p&gt;&lt;div class="figureset figureset--wide"&gt;
&lt;div class="figureset__figure"&gt;
&lt;div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no" ?&gt;

&lt;svg width="170px" height="170px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"&gt;
    &lt;rect id="Reflexivity" x="0" y="0" width="170" height="170" style="fill:none;"&gt;&lt;/rect&gt;
    &lt;g id="Reflexivity1" serif:id="Reflexivity"&gt;
        &lt;g id="Reflexivity2" serif:id="Reflexivity"&gt;
            &lt;circle cx="69.125" cy="85" r="25" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/circle&gt;
            &lt;path d="M92.036,115.458L89.618,107.325L97.752,104.907" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
            &lt;path d="M88.375,63.349C92.054,61.219 96.323,60 100.875,60C114.673,60 125.875,71.202 125.875,85C125.875,98.798 114.673,110 100.875,110C96.827,110 93.003,109.036 89.618,107.325" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
            &lt;text x="65.472px" y="89.984px" style="font-family:var(--font-code);font-size:16px;fill:var(--color-figure--text);"&gt;a&lt;/text&gt;
        &lt;/g&gt;
    &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;strong&gt;Reflexivity:&lt;/strong&gt; An object is equal to itself: &lt;code&gt;a == a&lt;/code&gt;.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="figureset__figure"&gt;
&lt;div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no" ?&gt;

&lt;svg width="170px" height="170px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"&gt;
    &lt;rect id="Symmetry" x="0" y="0" width="170" height="170" style="fill:none;"&gt;&lt;/rect&gt;
    &lt;g id="Symmetry1" serif:id="Symmetry"&gt;
        &lt;circle cx="40" cy="85.243" r="25" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/circle&gt;
        &lt;circle cx="130" cy="85.243" r="25" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/circle&gt;
        &lt;path d="M113.718,50.735L113.502,59.218L105.02,59.002" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M55.524,60.243C62.837,52.272 73.339,47.271 85,47.271C96.153,47.271 106.245,51.845 113.502,59.218" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M56.282,119.265L56.498,110.782L64.98,110.998" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M114.476,109.757C107.163,117.728 96.661,122.729 85,122.729C73.847,122.729 63.755,118.155 56.498,110.782" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;text x="36.472px" y="90.227px" style="font-family:var(--font-code);font-size:16px;fill:var(--color-figure--text);"&gt;a&lt;/text&gt;
        &lt;text x="126.129px" y="90.227px" style="font-family:var(--font-code);font-size:16px;fill:var(--color-figure--text);"&gt;b&lt;/text&gt;
    &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;strong&gt;Symmetry:&lt;/strong&gt; If &lt;code&gt;a == b&lt;/code&gt;, then &lt;code&gt;b == a&lt;/code&gt;.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="figureset__figure"&gt;
&lt;div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no" ?&gt;

&lt;svg width="170px" height="170px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"&gt;
    &lt;rect id="Transitivity" x="0" y="0" width="170" height="170" style="fill:none;"&gt;&lt;/rect&gt;
    &lt;g id="Transitivity1" serif:id="Transitivity"&gt;
        &lt;circle cx="130" cy="117.028" r="25" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/circle&gt;
        &lt;circle cx="40" cy="117.028" r="25" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/circle&gt;
        &lt;path d="M140.235,82.188L144.289,89.642L151.743,85.588" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M144.289,89.642C147.045,79.672 145.96,68.644 140.384,58.986C134.554,48.887 124.972,42.292 114.412,39.944" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M47.131,35.847L54.369,40.275L49.94,47.514" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M26.267,90.999C23.021,80.68 23.941,69.084 29.772,58.986C35.348,49.327 44.356,42.874 54.369,40.275" style="fill:none;stroke:var(--color-figure--muted);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;circle cx="85" cy="40" r="25" style="fill:none;stroke:var(--color-figure--base);stroke-width:2px;"&gt;&lt;/circle&gt;
        &lt;path d="M105.02,143.269L113.502,143.053L113.718,151.536" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;path d="M55.524,142.028C62.837,149.999 73.339,155 85,155C96.153,155 106.245,150.426 113.502,143.053" style="fill:none;stroke:var(--color-figure--intense);stroke-width:1px;"&gt;&lt;/path&gt;
        &lt;text x="36.472px" y="122.012px" style="font-family:var(--font-code);font-size:16px;fill:var(--color-figure--text);"&gt;a&lt;/text&gt;
        &lt;text x="126.808px" y="122.012px" style="font-family:var(--font-code);font-size:16px;fill:var(--color-figure--text);"&gt;c&lt;/text&gt;
        &lt;text x="81.129px" y="44.984px" style="font-family:var(--font-code);font-size:16px;fill:var(--color-figure--text);"&gt;b&lt;/text&gt;
    &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;strong&gt;Transitivity:&lt;/strong&gt; If &lt;code&gt;a == b&lt;/code&gt; and &lt;code&gt;b == c&lt;/code&gt;, then &lt;code&gt;a == c&lt;/code&gt;.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;These properties embody a common understanding of what equality means. Ruby won’t check these properties for you, so you’ll have to be vigilant to ensure you don’t break these properties when implementing equality yourself.&lt;/p&gt;&lt;aside class="aside stack-3 plain p-3"&gt;&lt;h3 class="aside__h" data-skip-toc="true"&gt;IEEE 754 and violations of reflexivity&lt;/h3&gt;
&lt;p&gt;It seems natural that something would be equal to itself, but there is an exception. IEEE 754 defines NaN (&lt;i&gt;Not a Number&lt;/i&gt;) as a value resulting from an undefined floating-point operation, such as dividing 0 by 0. NaN by definition is not equal to itself. You can see this for yourself:&lt;/p&gt;
&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;Float::NAN == Float::NAN   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means that &lt;code&gt;==&lt;/code&gt; in Ruby is not universally reflexive. Luckily, exceptions to reflexivity are exceedingly rare; this is the only exception that I am aware of.&lt;/p&gt;&lt;/aside&gt;&lt;h3 id="basic-equality-for-value-objects" data-skip-toc="false" data-numbering="2.1"&gt;&lt;span&gt;&lt;span&gt;Basic equality for value objects&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;Point&lt;/code&gt; class is an example of a &lt;i&gt;value object&lt;/i&gt;. The identity of a value object, and thereby equality, is based on all its attributes. That is exactly what the earlier example does:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    self.class == other.class &amp;amp;&amp;amp;
      @x == other.x &amp;amp;&amp;amp;
      @y == other.y
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="basic-equality-for-entities" data-skip-toc="false" data-numbering="2.2"&gt;&lt;span&gt;&lt;span&gt;Basic equality for entities&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;i&gt;Entities&lt;/i&gt; are objects with an explicit identity attribute, commonly &lt;code&gt;@id&lt;/code&gt;. Unlike value objects, an entity is equal to another entity if and only if their explicit identities are equal.&lt;/p&gt;&lt;p&gt;Entities are uniquely identifiable objects. Typically, any database record with an &lt;code&gt;id&lt;/code&gt; column corresponds to an entity.&lt;sup id="fnref:2" role="doc-noteref"&gt;&lt;a href="#fn:2" class="footnote" rel="footnote"&gt;2&lt;/a&gt;&lt;/sup&gt; Consider the following &lt;code&gt;Employee&lt;/code&gt; entity class:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Employee
  attr_reader :id
  attr_reader :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For entities, the &lt;code&gt;==&lt;/code&gt; operator is more involved to implement than for value objects:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Employee&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    super || (
      self.class == other.class &amp;amp;&amp;amp;
      !@id.nil? &amp;amp;&amp;amp;
      @id == other.id
    )
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This code does the following:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;super&lt;/code&gt; call invokes the default implementation of equality: &lt;code&gt;Object#==&lt;/code&gt;. On &lt;code&gt;Object&lt;/code&gt;, the &lt;code&gt;#==&lt;/code&gt; method returns &lt;code&gt;true&lt;/code&gt; if and only if the two objects are the same instance. This &lt;code&gt;super&lt;/code&gt; call, therefore, ensures that the &lt;i&gt;reflexivity&lt;/i&gt; property always holds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As with &lt;code&gt;Point&lt;/code&gt;, the implementation &lt;code&gt;Employee#==&lt;/code&gt; checks &lt;code&gt;class&lt;/code&gt;. This way, an &lt;code&gt;Employee&lt;/code&gt; instance can be checked for equality against objects of other classes, and this will always return &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If &lt;code&gt;@id&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt;, the entity is considered &lt;em&gt;not&lt;/em&gt; equal to any other entity. This is useful for newly-created entities which have not been persisted yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lastly, this implementation checks whether the ID is the same as the ID of the other entity. If so, the two entities are equal.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Checking equality on entities now works as intended:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;denis       = Employee.new(570, "Denis Defreyne")
also_denis  = Employee.new(570, "Denis")
denis_v     = Employee.new(992, "Denis Villeneuve")
new_denis_1 = Employee.new(nil, "Denis 1")
new_denis_2 = Employee.new(nil, "Denis 2")

denis == denis               # =&amp;gt; true
denis == also_denis          # =&amp;gt; true
denis == "cucumber"          # =&amp;gt; false
denis == denis_v             # =&amp;gt; false
denis == new_denis_1         # =&amp;gt; false
new_denis_1 == new_denis_1   # =&amp;gt; true
new_denis_1 == new_denis_2   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class="aside stack-3 plain p-3"&gt;&lt;h3 class="aside__h" data-skip-toc="true"&gt;Blog post of Theseus&lt;/h3&gt;
&lt;p&gt;Implementing equality on entity objects isn’t always straightforward. An object might have an &lt;code&gt;id&lt;/code&gt; attribute that doesn’t quite align with the object’s conceptual identity.&lt;/p&gt;
&lt;p&gt;Take a &lt;code&gt;BlogPost&lt;/code&gt; class, for example, with &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;body&lt;/code&gt; attributes. Imagine creating a &lt;code&gt;BlogPost&lt;/code&gt;, then halfway through writing the body for it, scratching everything and start over with a new title and a new body. The &lt;code&gt;id&lt;/code&gt; of that &lt;code&gt;BlogPost&lt;/code&gt; will still be the same, but is it still the same blog post?&lt;/p&gt;
&lt;p&gt;If I follow a Twitter account which later gets hacked and turned into a cryptocurrency spambot, is it still the same Twitter account?&lt;/p&gt;
&lt;p&gt;These questions don’t have a proper answer. That’s not surprising, as this is essentially the &lt;a href="https://en.wikipedia.org/wiki/Ship_of_Theseus"&gt;Ship of Theseus&lt;/a&gt; thought experiment. Luckily, in the world of computers, the generally accepted answer seems to be &lt;i&gt;yes&lt;/i&gt;: if two entities have the same &lt;code&gt;id&lt;/code&gt;, then the entities are equal as well.&lt;/p&gt;&lt;/aside&gt;&lt;h2 id="basic-equality-with-type-coercion" data-skip-toc="false" data-numbering="3"&gt;&lt;span&gt;&lt;span&gt;Basic equality with type coercion&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Typically, an object is not equal to an object of a different class. However, this is not always the case. Consider integers and floating-point numbers:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;float_two = 2.0
integer_two = 2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, &lt;code&gt;float_two&lt;/code&gt; is an instance of &lt;code&gt;Float&lt;/code&gt;, and &lt;code&gt;integer_two&lt;/code&gt; is an instance of &lt;code&gt;Integer&lt;/code&gt;. They are equal: &lt;code&gt;float_two == integer_two&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, despite different classes. Instances of &lt;code&gt;Integer&lt;/code&gt; and &lt;code&gt;Float&lt;/code&gt; are interchangeable when it comes to equality.&lt;/p&gt;&lt;p&gt;As a second example, consider this &lt;code&gt;Path&lt;/code&gt; class:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Path
  attr_reader :components

  def initialize(*components)
    @components = components
  end

  def string
    "/" + @components.join("/")
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This &lt;code&gt;Path&lt;/code&gt; class provides an API for creating paths:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;path = Path.new("usr", "bin", "ruby")
path.string   # =&amp;gt; "/usr/bin/ruby"&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;Path&lt;/code&gt; class is a value object, and implementing &lt;code&gt;#==&lt;/code&gt; could be done just as with other value objects:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Path&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    self.class == other.class &amp;amp;&amp;amp;
      @components == other.components
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, the &lt;code&gt;Path&lt;/code&gt; class is special because it represents a value that could be considered a string. The &lt;code&gt;==&lt;/code&gt; operator will return &lt;code&gt;false&lt;/code&gt; when checking equality with anything that isn’t a &lt;code&gt;Path&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;path = Path.new("usr", "bin", "ruby")
path.string == "/usr/bin/ruby"   # =&amp;gt; true
path == "/usr/bin/ruby"          # =&amp;gt; false :(&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It can be beneficial for &lt;code&gt;path == "/usr/bin/ruby"&lt;/code&gt; to be &lt;code&gt;true&lt;/code&gt; rather than &lt;code&gt;false&lt;/code&gt;. To make this happen, the &lt;code&gt;==&lt;/code&gt; operator needs to be implemented differently:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Path&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    other.respond_to?(:to_str) &amp;amp;&amp;amp;
      to_str == other.to_str
  end

  def to_str
    string
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This implementation of &lt;code&gt;==&lt;/code&gt; coerces both objects to &lt;code&gt;String&lt;/code&gt;s, and then checks whether they are equal. Checking equality of a &lt;code&gt;Path&lt;/code&gt; now works:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;path = Path.new("usr", "bin", "ruby")

path == path                # =&amp;gt; true
path == "/usr/bin/ruby"     # =&amp;gt; true
path == "/usr/bin/python"   # =&amp;gt; false

path == 12.3   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This class implements &lt;code&gt;#to_str&lt;/code&gt;, rather than &lt;code&gt;#to_s&lt;/code&gt;. These methods both return strings, but by convention, the &lt;code&gt;to_str&lt;/code&gt; method is only implemented on types that are interchangeable with strings.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;Path&lt;/code&gt; class is such a type. By implementing &lt;code&gt;Path#to_str&lt;/code&gt;, the implementation states that this class behaves like a &lt;code&gt;String&lt;/code&gt;. For example, it’s now possible to pass a &lt;code&gt;Path&lt;/code&gt; (rather than a &lt;code&gt;String&lt;/code&gt;) to &lt;code&gt;IO.open&lt;/code&gt;, and it will just work. This is because &lt;code&gt;IO.open&lt;/code&gt; accepts anything that responds to &lt;code&gt;#to_str&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;code&gt;String#==&lt;/code&gt; also uses the &lt;code&gt;to_str&lt;/code&gt; method. Because of this, the &lt;code&gt;==&lt;/code&gt; operator is reflexive:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;"/usr/bin/ruby" == path     # =&amp;gt; true
"/usr/bin/python" == path   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="strict-equality" data-skip-toc="false" data-numbering="4"&gt;&lt;span&gt;&lt;span&gt;Strict equality&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Ruby provides &lt;code&gt;#equal?&lt;/code&gt; to check whether two objects are the same instance:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;"snow" + "plow" == "snowplow"
# =&amp;gt; true

("snow" + "plow").equal?("snowplow")
# =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, we end up with two &lt;code&gt;String&lt;/code&gt; instances with the same content. Because they are distinct instances, &lt;code&gt;#equal?&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, and because their content is the same, &lt;code&gt;#==&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Do not implement &lt;code&gt;#equal?&lt;/code&gt; in your own classes. It is not meant to be overridden. It’ll all end in tears.&lt;/p&gt;&lt;p&gt;Earlier in this article, I mentioned that &lt;code&gt;#==&lt;/code&gt; has the property of &lt;em&gt;reflexivity&lt;/em&gt;: an object is always equal to itself. Here is a related property for &lt;code&gt;#equal?&lt;/code&gt;:&lt;/p&gt;&lt;div class="p-2 lh-abs-md callout"&gt;
&lt;strong&gt;Property:&lt;/strong&gt; Given objects &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. If &lt;code&gt;a.equal?(b)&lt;/code&gt;, then &lt;code&gt;a == b&lt;/code&gt;.&lt;/div&gt;&lt;p&gt;Ruby won’t automatically validate this property for your code. It’s up to you to ensure that this property holds when you implement the equality methods.&lt;/p&gt;&lt;p&gt;For example, recall the implementation of &lt;code&gt;Employee#==&lt;/code&gt; from earlier in this article:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Employee&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    super || (
      self.class == other.class &amp;amp;&amp;amp;
      !@id.nil? &amp;amp;&amp;amp;
      @id == other.id
    )
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The call to &lt;code&gt;super&lt;/code&gt; on the first line makes this implementation of &lt;code&gt;#==&lt;/code&gt; reflexive. This &lt;code&gt;super&lt;/code&gt; invokes the default implementation of &lt;code&gt;#==&lt;/code&gt;, which delegates to &lt;code&gt;#equal?&lt;/code&gt;. Therefore, I could have used &lt;code&gt;#equal?&lt;/code&gt;, rather than &lt;code&gt;super&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Employee&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    self.equal?(other) || (
      &lt;span class="text-muted"&gt;…&lt;/span&gt;
    )
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I prefer using &lt;code&gt;super&lt;/code&gt;, though this is likely a matter of taste.&lt;/p&gt;&lt;h2 id="hash-equality" data-skip-toc="false" data-numbering="5"&gt;&lt;span&gt;&lt;span&gt;Hash equality&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In Ruby, any object can be used as a key in a &lt;code&gt;Hash&lt;/code&gt;. Strings, symbols, and numbers are commonly used as &lt;code&gt;Hash&lt;/code&gt; keys, but instances of your own classes can function as &lt;code&gt;Hash&lt;/code&gt; keys too — provided that you implement both &lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#hash&lt;/code&gt;.&lt;/p&gt;&lt;h3 id="the-eql-method" data-skip-toc="false" data-numbering="5.1"&gt;&lt;span&gt;&lt;span&gt;The #eql? method&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;#eql?&lt;/code&gt; method behaves similarly to &lt;code&gt;#==&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;"foo" == "foo"      # =&amp;gt; true
"foo".eql?("foo")   # =&amp;gt; true
"foo".eql?("bar")   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, &lt;code&gt;#eql?&lt;/code&gt;, unlike &lt;code&gt;#==&lt;/code&gt;, does not perform type coercion:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;1 == 1.0        # =&amp;gt; true
1.eql?(1.0)     # =&amp;gt; false
1.0.eql?(1.0)   # =&amp;gt; true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;#==&lt;/code&gt; doesn’t perform type coercion, the implementations of &lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#==&lt;/code&gt; will be identical. Rather than copy-pasting, however, we’ll put the implementation in &lt;code&gt;#eql?&lt;/code&gt;, and let &lt;code&gt;#==&lt;/code&gt; delegate to &lt;code&gt;#eql?&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    self.eql?(other)
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Employee&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    self.eql?(other)
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I made the deliberate decision to put the implementation in &lt;code&gt;#eql?&lt;/code&gt; and let &lt;code&gt;#==&lt;/code&gt; delegate to it, rather than the other way around. If we were to let &lt;code&gt;#eql?&lt;/code&gt; delegate to &lt;code&gt;#==&lt;/code&gt;, there’s an increased risk that someone will update &lt;code&gt;#==&lt;/code&gt; and inadvertently break the properties of &lt;code&gt;#eql?&lt;/code&gt; (which I’ll mention below) in the process.&lt;/p&gt;&lt;p&gt;For the &lt;code&gt;Path&lt;/code&gt; value object, whose &lt;code&gt;#==&lt;/code&gt; method &lt;em&gt;does&lt;/em&gt; perform type coercion, the implementation of &lt;code&gt;#eql?&lt;/code&gt; will differ from the implementation of &lt;code&gt;#==&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Path&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def ==(other)
    other.respond_to?(:to_str) &amp;amp;&amp;amp;
      to_str == other.to_str
  end

  def eql?(other)
    self.class == other.class &amp;amp;&amp;amp;
      @components == other.components
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, &lt;code&gt;#==&lt;/code&gt; does not delegate to &lt;code&gt;#eql?&lt;/code&gt;, nor the other way around.&lt;/p&gt;&lt;p&gt;A correct implementation of &lt;code&gt;#eql?&lt;/code&gt; has the following two properties:&lt;/p&gt;&lt;div class="p-2 lh-abs-md callout"&gt;
&lt;strong&gt;Property:&lt;/strong&gt; Given objects &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. If &lt;code&gt;a.eql?(b)&lt;/code&gt;, then &lt;code&gt;a == b&lt;/code&gt;.&lt;/div&gt;&lt;div class="p-2 lh-abs-md callout"&gt;
&lt;strong&gt;Property:&lt;/strong&gt; Given objects &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. If &lt;code&gt;a.equal?(b)&lt;/code&gt;, then &lt;code&gt;a.eql?(b)&lt;/code&gt;.&lt;/div&gt;&lt;p&gt;These two properties are not explicitly called out in the Ruby documentation. However, to the best of my knowledge, all implementations of &lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#==&lt;/code&gt; respect this property.&lt;/p&gt;&lt;p&gt;As before, Ruby will not automatically validate that these properties hold in your code. It’s up to you to ensure that these properties aren’t violated.&lt;/p&gt;&lt;h3 id="the-hash-method" data-skip-toc="false" data-numbering="5.2"&gt;&lt;span&gt;&lt;span&gt;The #hash method&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;For an object to be usable as a key in a &lt;code&gt;Hash&lt;/code&gt;, it needs to implement not only &lt;code&gt;#eql?&lt;/code&gt;, but also &lt;code&gt;#hash&lt;/code&gt;. This &lt;code&gt;#hash&lt;/code&gt; method will return an integer, the &lt;i&gt;hash code&lt;/i&gt;, that respects the following property:&lt;/p&gt;&lt;div class="p-2 lh-abs-md callout"&gt;
&lt;strong&gt;Property:&lt;/strong&gt; Given objects &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. If &lt;code&gt;a.eql?(b)&lt;/code&gt;, then &lt;code&gt;a.hash == b.hash&lt;/code&gt;.&lt;/div&gt;&lt;p&gt;Typically, the implementation of &lt;code&gt;#hash&lt;/code&gt; creates an array of all attributes that constitute identity, and returns the hash of that array. For example, here is &lt;code&gt;Point#hash&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def hash
    [self.class, @x, @y].hash
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For &lt;code&gt;Path&lt;/code&gt;, the implementation of &lt;code&gt;#hash&lt;/code&gt; will look similar:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Path&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def hash
    [self.class, @components].hash
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the &lt;code&gt;Employee&lt;/code&gt; class, which is an entity rather than a value object, the implementation of &lt;code&gt;#hash&lt;/code&gt; will use the class and the &lt;code&gt;@id&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Employee&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def hash
    [self.class, @id].hash
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If two objects are not equal, the hash code should ideally be different, too. This is not mandatory, however: it is okay for two non-equal objects to have the same hash code. Ruby will use &lt;code&gt;#eql?&lt;/code&gt; to tell objects with identical hash codes apart.&lt;/p&gt;&lt;aside class="aside stack-3 plain p-3"&gt;&lt;h3 class="aside__h" data-skip-toc="true"&gt;Avoid XOR for calculating hash codes&lt;/h3&gt;
&lt;p&gt;A popular but problematic approach for implementing &lt;code&gt;#hash&lt;/code&gt; uses XOR (the &lt;code&gt;^&lt;/code&gt; operator). Such an implementation would calculate the hash codes of each individual attribute, and combine these hash codes with XOR. For example:&lt;/p&gt;
&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def hash
    &lt;span class="text-muted"&gt;# Do not do this!&lt;/span&gt;
    self.class.hash ^ @x.hash ^ @y.hash
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With such an implementation, the chance of a &lt;em&gt;hash code collision&lt;/em&gt;, which means that multiple objects have the same hash code, is higher than with an implementation that delegates to &lt;code&gt;Array#hash&lt;/code&gt;. Hash code collisions will degrade performance, and could potentially pose a denial-of-service security risk.&lt;/p&gt;
&lt;p&gt;A better way, though still flawed, is to multiply the components of the hash code by unique prime numbers before combining them:&lt;/p&gt;
&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def hash
    &lt;span class="text-muted"&gt;# Do not do this!&lt;/span&gt;
    self.class.hash ^ (@x.hash * 3) ^ (@y.hash * 5)
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Such an implementation has additional performance overhead due to the new multiplication. It also requires mental effort to ensure the implementation is and remains correct.&lt;/p&gt;
&lt;p&gt;An even better way of implementing &lt;code&gt;#hash&lt;/code&gt; is the one I’ve laid out before — making use of &lt;code&gt;Array#hash&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Point&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def hash
    [self.class, @x, @y].hash
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;An implementation that uses &lt;code&gt;Array#hash&lt;/code&gt; is simple, performs quite well, and produces hash codes with the lowest chance of collisions. It’s the best approach to implementing &lt;code&gt;#hash&lt;/code&gt;.&lt;/p&gt;&lt;/aside&gt;&lt;h3 id="putting-it-together" data-skip-toc="false" data-numbering="5.3"&gt;&lt;span&gt;&lt;span&gt;Putting it together&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;With both &lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#hash&lt;/code&gt; in place, the &lt;code&gt;Point&lt;/code&gt;, &lt;code&gt;Path&lt;/code&gt;, and &lt;code&gt;Employee&lt;/code&gt; objects can be used as hash keys:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;points = {}
points[Point.new(11, 24)] = true

points[Point.new(11, 24)]   # =&amp;gt; true
points[Point.new(10, 22)]   # =&amp;gt; nil&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, we use a &lt;code&gt;Hash&lt;/code&gt; instance to keep track of a collection of &lt;code&gt;Point&lt;/code&gt;s. We can also use a &lt;code&gt;Set&lt;/code&gt; for this, which uses a &lt;code&gt;Hash&lt;/code&gt; under the hood, but provides a nicer API:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;require "set"

points = Set.new
points &amp;lt;&amp;lt; Point.new(11, 24)

points.include?(Point.new(11, 24))   # =&amp;gt; true
points.include?(Point.new(10, 22))   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Objects that are used in &lt;code&gt;Set&lt;/code&gt;s need to have an implementation of both &lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#hash&lt;/code&gt;, just like objects that are used as hash keys.&lt;/p&gt;&lt;p&gt;Objects that perform type coercion, such as &lt;code&gt;Path&lt;/code&gt;, can also be used as hash keys, and thus also in sets:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;require "set"

home = Path.new("home", "denis")
also_home = Path.new("home", "denis")
elsewhere = Path.new("usr", "bin")

paths = Set.new
paths &amp;lt;&amp;lt; home

paths.include?(home)        # =&amp;gt; true
paths.include?(also_home)   # =&amp;gt; true
paths.include?(elsewhere)   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We now have an implementation of equality that works for all kinds of objects.&lt;/p&gt;&lt;aside class="aside stack-3 plain p-3"&gt;&lt;h3 class="aside__h" data-skip-toc="true"&gt;Mutability, nemesis of Equality&lt;/h3&gt;
&lt;p&gt;So far, the examples for value objects have assumed that these value objects are &lt;i&gt;immutable&lt;/i&gt;. This is with good reason, because mutable value objects are far harder to deal with.&lt;/p&gt;
&lt;p&gt;To illustrate this, consider a &lt;code&gt;Point&lt;/code&gt; instance used as a hash key:&lt;/p&gt;
&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;point = Point.new(5, 6)

collection = {}
collection[point] = 42

p point.hash               # =&amp;gt; 421522
p collection.key?(point)   # =&amp;gt; true&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The problem arises when changing attributes of this point:&lt;/p&gt;
&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;point.x = 10
p point.hash               # =&amp;gt; 910895
p collection.key?(point)   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because the hash code is based on the attributes, and an attribute has changed, the hash code is no longer the same. As a result, &lt;code&gt;collection&lt;/code&gt; no longer seems to contain the point. Uh oh!&lt;/p&gt;
&lt;p&gt;There are no good ways to solve this problem, except for making value objects immutable.&lt;/p&gt;
&lt;p&gt;This is not a problem with entities. This is because the &lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#hash&lt;/code&gt; methods of an entity are solely based on its explicit identity — not its attributes.&lt;/p&gt;&lt;/aside&gt;&lt;p&gt;So far, we’ve covered &lt;code&gt;#==&lt;/code&gt;, &lt;code&gt;#eql?&lt;/code&gt;, and &lt;code&gt;#hash&lt;/code&gt;. These three methods are sufficient for a correct implementation of equality. However, we can go further to improve that sweet Ruby developer experience, and implement &lt;code&gt;#===&lt;/code&gt;.&lt;/p&gt;&lt;h2 id="case-equality-triple-equals" data-skip-toc="false" data-numbering="6"&gt;&lt;span&gt;&lt;span&gt;Case equality (triple equals)&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;#===&lt;/code&gt; operator, also called the &lt;i&gt;case equality operator&lt;/i&gt;, is not really an equality operator at all. Rather, it’s better to think of it as a membership testing operator. Consider the following:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;10..15 === 14   # =&amp;gt; true
80..99 === 14   # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, &lt;code&gt;Range#===&lt;/code&gt; checks whether a range covers a certain element. It is also common to use &lt;code&gt;case&lt;/code&gt; expressions to achieve the same:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;case 14
when 10..15
  puts "Kinda small!"
when 80..99
  puts "Kinda large!"
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is also where case equality gets its name. Triple-equals is called &lt;i&gt;case&lt;/i&gt; equality, because &lt;code&gt;case&lt;/code&gt; expressions use it.&lt;/p&gt;&lt;p&gt;You never &lt;em&gt;need&lt;/em&gt; to use &lt;code&gt;case&lt;/code&gt;. It’s possible to rewrite a &lt;code&gt;case&lt;/code&gt; expression using &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;===&lt;/code&gt;. In general, &lt;code&gt;case&lt;/code&gt; expressions tend to look cleaner. Compare:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;if 10..15 === 14
  puts "Kinda small!"
elsif 80..99 === 14
  puts "Kinda large!"
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The examples above all use &lt;code&gt;Range#===&lt;/code&gt;, to check whether the range covers a certain number. Another commonly used implementation is &lt;code&gt;Class#===&lt;/code&gt;, which checks whether an object is an instance of a class:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;Integer === 15              # =&amp;gt; true
Integer === 15.5            # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I’m rather fond of the &lt;code&gt;#grep&lt;/code&gt; method, which uses &lt;code&gt;#===&lt;/code&gt; to select matching elements from an array. It can be shorter and sweeter than using &lt;code&gt;#select&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;[4, 2.0, 7, 6.1].grep(Integer)   # =&amp;gt; [4, 7]
[4, 2.0, 7, 6.1].grep(2..6)      # =&amp;gt; [4, 2.0]

# Same, but more verbose:
[4, 2.0, 7, 6.1].select { |num| Integer === num }
[4, 2.0, 7, 6.1].select { |num| 2..6 === num }&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Regular expressions also implement &lt;code&gt;#===&lt;/code&gt;. You can use it to check whether a string matches a regular expression:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;phone = "+491573abcde"
case phone
when /00000/
  puts "Too many zeroes!"
when /[a-z]/
  puts "Your phone number has letters in it?!"
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It helps to think of a regular expression as the (infinite) collection of all strings that can be produced by it. The set of all strings produced by &lt;code&gt;/[a-z]/&lt;/code&gt; includes the example string &lt;code&gt;"+491573abcde"&lt;/code&gt;. Similarly, you can think of a &lt;code&gt;Class&lt;/code&gt; as the (infinite) collection of all its instances, and a &lt;code&gt;Range&lt;/code&gt; as the collection of all elements in that range. This way of thinking clarifies that &lt;code&gt;#===&lt;/code&gt; really is a membership testing operator.&lt;/p&gt;&lt;p&gt;An example of a class that could implement &lt;code&gt;#===&lt;/code&gt; is a &lt;code&gt;PathPattern&lt;/code&gt; class:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class PathPattern
  def initialize(string)
    @string = string
  end

  def ===(other)
    File.fnmatch(@string, other)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An example instance is &lt;code&gt;PathPattern.new("/bin/*")&lt;/code&gt;, which matches anything directly under the &lt;span class="path"&gt;/bin&lt;wbr&gt;&lt;/wbr&gt;&lt;/span&gt; directory, such as &lt;span class="path"&gt;/bin&lt;wbr&gt;&lt;/wbr&gt;/ruby&lt;wbr&gt;&lt;/wbr&gt;&lt;/span&gt;, but not &lt;span class="path"&gt;/var&lt;wbr&gt;&lt;/wbr&gt;/log&lt;wbr&gt;&lt;/wbr&gt;&lt;/span&gt;.&lt;/p&gt;&lt;p&gt;The implementation of &lt;code&gt;PathPattern#===&lt;/code&gt; uses Ruby’s built-in &lt;code&gt;File.fnmatch&lt;/code&gt; to check whether the pattern string matches. Here is an example of it in use:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;pattern = PathPattern.new("/bin/*")

pattern === "/bin/ruby"   # =&amp;gt; true
pattern === "/var/log"    # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Worth noting is that &lt;code&gt;File.fnmatch&lt;/code&gt; calls &lt;code&gt;#to_str&lt;/code&gt; on its arguments. This way, &lt;code&gt;#===&lt;/code&gt; automatically works on other string-like objects as well, such as &lt;code&gt;Path&lt;/code&gt; instances:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;bin_ruby = Path.new("bin", "ruby")
var_log = Path.new("var", "log")

pattern = PathPattern.new("/bin/*")

pattern === bin_ruby       # =&amp;gt; true
pattern === var_log        # =&amp;gt; false&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;PathPattern&lt;/code&gt; class implements &lt;code&gt;#===&lt;/code&gt;, and therefore &lt;code&gt;PathPattern&lt;/code&gt; instances work with &lt;code&gt;case&lt;/code&gt;/&lt;code&gt;when&lt;/code&gt;, too:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;case "/home/denis"
when PathPattern.new("/home/*")
  puts "Home sweet home"
else
  puts "Somewhere, I guess"
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="ordered-comparison" data-skip-toc="false" data-numbering="7"&gt;&lt;span&gt;&lt;span&gt;Ordered comparison&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For some objects, it’s useful not only to check whether two objects are the same, but how they are ordered. Are they larger? Smaller? Consider this &lt;code&gt;Score&lt;/code&gt; class, which models the scoring system of my university in Ghent, Belgium.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class Score
  attr_reader :value

  def initialize(value)
    @value = value
  end

  def grade
    if @value &amp;lt; 10
      "failing"
    elsif @value &amp;lt; 14
      "passing"
    elsif @value &amp;lt; 16
      "distinction"
    elsif @value &amp;lt; 18
      "high distinction"
    else
      "highest distinction"
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(I was a terrible student. I’m not sure if this was really how the scoring even worked — but as an example, it will do just fine.)&lt;/p&gt;&lt;p&gt;In any case, we benefit from having such a &lt;code&gt;Score&lt;/code&gt; class. We can encode relevant logic on there, such as determining the grade, and checking whether or not a score is passing. For example, it might be useful to get the lowest and highest score out of a list:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;scores = [
  Score.new(6),
  Score.new(17),
  Score.new(14),
  Score.new(13),
  Score.new(11),
]

p scores.min
p scores.max&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, as it stands right now, the expressions &lt;code&gt;scores.min&lt;/code&gt; and &lt;code&gt;scores.max&lt;/code&gt; will result in an error: &lt;code&gt;comparison of Score with Score failed (ArgumentError)&lt;/code&gt;. We haven’t told Ruby how to compare two &lt;code&gt;Score&lt;/code&gt; objects. We can do so by implementing &lt;code&gt;Score#&amp;lt;=&amp;gt;&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Score&lt;/span&gt;
  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  def &amp;lt;=&amp;gt;(other)
    return nil unless other.class == self.class

    @value &amp;lt;=&amp;gt; other.value
  end
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An implementation of &lt;code&gt;#&amp;lt;=&amp;gt;&lt;/code&gt; returns four possible values:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;It returns &lt;code&gt;0&lt;/code&gt; when the two objects are equal.&lt;/li&gt;
&lt;li&gt;It returns &lt;code&gt;-1&lt;/code&gt; when &lt;code&gt;self&lt;/code&gt; is less than &lt;code&gt;other&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It returns &lt;code&gt;1&lt;/code&gt; when &lt;code&gt;self&lt;/code&gt; is greater than &lt;code&gt;other&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It returns &lt;code&gt;nil&lt;/code&gt; when the two objects cannot be compared.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;The &lt;code&gt;#&amp;lt;=&amp;gt;&lt;/code&gt; and &lt;code&gt;#==&lt;/code&gt; operators are connected:&lt;/p&gt;&lt;div class="p-2 lh-abs-md callout"&gt;
&lt;strong&gt;Property:&lt;/strong&gt; Given objects &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. If &lt;code&gt;(a &amp;lt;=&amp;gt; b) == 0&lt;/code&gt;, then &lt;code&gt;a == b&lt;/code&gt;.&lt;/div&gt;&lt;div class="p-2 lh-abs-md callout"&gt;
&lt;strong&gt;Property:&lt;/strong&gt; Given objects &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. If &lt;code&gt;(a &amp;lt;=&amp;gt; b) != 0&lt;/code&gt;, then &lt;code&gt;a != b&lt;/code&gt;.&lt;/div&gt;&lt;p&gt;As before, it’s up to you to ensure that these properties hold when implementing &lt;code&gt;#==&lt;/code&gt; and &lt;code&gt;#&amp;lt;=&amp;gt;&lt;/code&gt;. Ruby won’t check this for you.&lt;/p&gt;&lt;p&gt;For simplicity, I’ve left out the implementation &lt;code&gt;Score#==&lt;/code&gt; in the &lt;code&gt;Score&lt;/code&gt; example above. It’d certainly be good to have that, though.&lt;/p&gt;&lt;p&gt;In the case of &lt;code&gt;Score#&amp;lt;=&amp;gt;&lt;/code&gt;, we bail out if &lt;code&gt;other&lt;/code&gt; is not a score, and otherwise, we call &lt;code&gt;#&amp;lt;=&amp;gt;&lt;/code&gt; on the two values. We can check that this works: the expression &lt;code&gt;Score.new(6) &amp;lt;=&amp;gt; Score.new(12)&lt;/code&gt; evaluates to &lt;code&gt;-1&lt;/code&gt;, which is correct because a score of 6 is lower than a score of 12.&lt;sup id="fnref:3" role="doc-noteref"&gt;&lt;a href="#fn:3" class="footnote" rel="footnote"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;p&gt;With &lt;code&gt;Score#&amp;lt;=&amp;gt;&lt;/code&gt; in place, &lt;code&gt;scores.max&lt;/code&gt; now returns the maximum score. Other methods such as &lt;code&gt;#min&lt;/code&gt;, &lt;code&gt;#minmax&lt;/code&gt;, and &lt;code&gt;#sort&lt;/code&gt; work as well.&lt;/p&gt;&lt;p&gt;However, we can’t yet use operators like &lt;code&gt;&amp;lt;&lt;/code&gt;. The expression &lt;code&gt;scores[0] &amp;lt; scores[1]&lt;/code&gt;, for example, will raise an undefined method error: &lt;code&gt;undefined method `&amp;lt;' for #&amp;lt;Score:0x00112233 @value=6&amp;gt;&lt;/code&gt;. We can solve that by including the &lt;code&gt;Comparable&lt;/code&gt; mixin:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="text-muted"&gt;class Score&lt;/span&gt;
  include Comparable

  &lt;span class="text-muted"&gt;…&lt;/span&gt;
  &lt;span class="text-muted"&gt;def &amp;lt;=&amp;gt;(other)&lt;/span&gt;
    &lt;span class="text-muted"&gt;…&lt;/span&gt;
  &lt;span class="text-muted"&gt;end&lt;/span&gt;
&lt;span class="text-muted"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By including &lt;code&gt;Comparable&lt;/code&gt;, the &lt;code&gt;Score&lt;/code&gt; class automatically gains the &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;gt;=&lt;/code&gt; operators, which all call &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; internally. The expression &lt;code&gt;scores[0] &amp;lt; scores[1]&lt;/code&gt; now evaluates to a boolean, as expected.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;Comparable&lt;/code&gt; mixin also provides other useful methods such as &lt;code&gt;#between?&lt;/code&gt; and &lt;code&gt;#clamp&lt;/code&gt;.&lt;/p&gt;&lt;h2 id="wrapping-up" data-skip-toc="false" data-numbering="8"&gt;&lt;span&gt;&lt;span&gt;Wrapping up&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We talked about the following topics:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the &lt;code&gt;#==&lt;/code&gt; operator, used for basic equality, with optional type coercion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;#equal?&lt;/code&gt;, which checks whether two objects are the same instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;#eql?&lt;/code&gt; and &lt;code&gt;#hash&lt;/code&gt;, which are used for testing whether an object is a key in a hash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;#===&lt;/code&gt;, which isn’t quite an equality operator, but rather a “is kind of” or “is member of” operator.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;#&amp;lt;=&amp;gt;&lt;/code&gt; for ordered comparison, along with the &lt;code&gt;Comparable&lt;/code&gt; module, which provides operators such as &lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;=&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;You now know all you need to know about implementing equality in Ruby.&lt;/p&gt;&lt;h2 id="further-reading" data-skip-toc="false" data-numbering="9"&gt;&lt;span&gt;&lt;span&gt;Further reading&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The Ruby documentation is a good place to find out more about equality:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.0/Object.html#method-i-eql-3F"&gt;Documentation on &lt;code&gt;#==&lt;/code&gt; (double equals), &lt;code&gt;#eql?&lt;/code&gt;, and &lt;code&gt;#equal?&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.0/Object.html#method-i-hash"&gt;Documentation on &lt;code&gt;#hash&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.0/Object.html#method-i-3D-3D-3D"&gt;Documentation on &lt;code&gt;#===&lt;/code&gt; (triple equals)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.0/Object.html#method-i-3C-3D-3E"&gt;Documentation on &lt;code&gt;#&amp;lt;=&amp;gt;&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.0/Comparable.html"&gt;Documentation on &lt;code&gt;Comparable&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;I also found the following resources useful:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kddnewton.com/2021/09/09/ruby-type-conversion.html"&gt;Kevin Newton’s article on Ruby type conversion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/baweaver/understanding-ruby-triple-equals-2p9c"&gt;Brandon Weaver’s article on triple equals&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Special thanks to &lt;a href="https://kddnewton.com"&gt;Kevin Newton&lt;/a&gt; and Chris Seaton for their invaluable input.&lt;/p&gt;&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol class="stack-3"&gt;
&lt;li id="fn:1" role="doc-endnote"&gt;&lt;p&gt;You could say that Ruby’s implementation of equality… &lt;i&gt;has no equal&lt;/i&gt;. Ahem. &lt;a href="#fnref:1" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn:2" role="doc-endnote"&gt;&lt;p&gt;Other forms of ID are possible too. For example, books have an ISBN, and recordings have an ISRC. But if you have a library with multiple copies of the same book, then ISBN won’t uniquely identify your books anymore. &lt;a href="#fnref:2" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn:3" role="doc-endnote"&gt;&lt;p&gt;Did you know that the Belgian high school system used to have a scoring system where 1 was the highest and 10 was the lowest? Imagine the confusion! &lt;a href="#fnref:3" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <summary type="html">Ruby is one of the few programming languages that gets equality right. Nonetheless, equality in Ruby is not straightforward: there is &lt;code&gt;#==&lt;/code&gt;, &lt;code&gt;#eql?&lt;/code&gt;, &lt;code&gt;#equal?&lt;/code&gt;, &lt;code&gt;#===&lt;/code&gt;, and more. In this article, I will take you through all forms of equality in Ruby and how to implement them.</summary>
  </entry>
  <entry>
    <id>tag:denisdefreyne.com,2022-03-19:/articles/2022-modal-lexer/</id>
    <title type="html">Using a modal lexer for parsing sublanguages</title>
    <published>2022-03-19T23:00:00Z</published>
    <updated>2022-03-19T23:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2022-modal-lexer/" type="text/html"/>
    <content type="html">&lt;p&gt;How do you write a lexer for &lt;code&gt;print "one plus two is ${1+2}";&lt;/code&gt;? The string interpolation part makes this tricky, as the string forms a sublanguage that is different from the main language. To make things worse, interpolation can be nested. Textbook lexers won’t cut it, and we need something stronger.&lt;/p&gt;&lt;p&gt;Enter &lt;em&gt;modal lexers&lt;/em&gt;. These lexers can switch between different modes for lexing different languages. In this article, I’ll run you through an example of how to build such a modal lexer.&lt;/p&gt;&lt;p&gt;For this article, I’m using Ruby as the programming language. The same technique will work in other programming languages too. No external dependencies (gems) are used.&lt;/p&gt;&lt;ol class="toc toc__list toc__root-list" role="doc-toc"&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#ruby-s-string-scanner"&gt;&lt;span&gt;&lt;span&gt;Ruby’s string scanner&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#a-basic-lexer"&gt;&lt;span&gt;&lt;span&gt;A basic lexer&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#testing-it-out"&gt;&lt;span&gt;&lt;span&gt;Testing it out&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#a-modal-lexer"&gt;&lt;span&gt;&lt;span&gt;A modal lexer&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#the-main-lexer-mode"&gt;&lt;span&gt;&lt;span&gt;The main lexer mode&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#the-string-lexer-mode"&gt;&lt;span&gt;&lt;span&gt;The string lexer mode&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#the-string-interpolation-lexer-mode"&gt;&lt;span&gt;&lt;span&gt;The string interpolation lexer mode&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#testing-it-out-"&gt;&lt;span&gt;&lt;span&gt;Testing it out&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#hints-for-the-parser"&gt;&lt;span&gt;&lt;span&gt;Hints for the parser&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#wrapping-up"&gt;&lt;span&gt;&lt;span&gt;Wrapping up&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#references"&gt;&lt;span&gt;&lt;span&gt;References&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h2 id="ruby-s-string-scanner" data-skip-toc="false" data-numbering="1"&gt;&lt;span&gt;&lt;span&gt;Ruby’s string scanner&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Ruby provides a &lt;code&gt;StringScanner&lt;/code&gt; class, which is is an excellent building block for lexers. It offers a clean API to split up a string into individual parts.&lt;/p&gt;&lt;p&gt;I’ll run you through a simple example, so that you can get a feel of how it works. First, we require &lt;code&gt;strscan&lt;/code&gt;, and then we instantiate the &lt;code&gt;StringScanner&lt;/code&gt; with the string to scan:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;require "strscan"

str = "let amount = 5;"
scanner = StringScanner.new(str)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;#scan&lt;/code&gt; method, when given a string part, will attempt to match this given string part:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;scanner.scan("let") # =&amp;gt; "let"&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The scanner starts at the beginning of the string. When a match is successful, the scanner will move forward. Because we just matched &lt;code&gt;"let"&lt;/code&gt;, the scanner has moved past this &lt;code&gt;"let"&lt;/code&gt;, and attempting to match it again will fail:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;scanner.scan("let") # =&amp;gt; nil&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;#scan&lt;/code&gt; method returns the matched string part, or &lt;code&gt;nil&lt;/code&gt; if there is no match. In the latter case, the scanner stays at its current position.&lt;/p&gt;&lt;p&gt;This method can also take a regular expression:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;scanner.scan(/\s+/) # =&amp;gt; " "&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The regular expression &lt;code&gt;\s+&lt;/code&gt; matches one or more whitespace characters, such as spaces, tabs, and newlines.&lt;/p&gt;&lt;p&gt;Now that we’ve matched &lt;code&gt;"let"&lt;/code&gt; and a space character, we can match the next part: an identifier, consisting of alphanumeric characters. For this, the regular expression &lt;code&gt;\w+&lt;/code&gt; comes in handy, which matches one or more alphanumeric characters:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;scanner.scan(/\w+/) # =&amp;gt; "amount"&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another useful method is &lt;code&gt;#matched&lt;/code&gt;, which returns the matched string part:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;scanner.matched # =&amp;gt; "amount"&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;StringScanner&lt;/code&gt; provides more functionality, but &lt;code&gt;#scan&lt;/code&gt; and &lt;code&gt;#matched&lt;/code&gt; is all we need to write a lexer.&lt;/p&gt;&lt;h2 id="a-basic-lexer" data-skip-toc="false" data-numbering="2"&gt;&lt;span&gt;&lt;span&gt;A basic lexer&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Before writing a &lt;em&gt;modal&lt;/em&gt; lexer, we’ll start with a basic lexer:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class BasicLexer
  def initialize(str)
    @scanner = StringScanner.new(str)
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This &lt;code&gt;BasicLexer&lt;/code&gt; is constructed with a string. The string itself isn’t important; we’ll only use the &lt;code&gt;StringScanner&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def next_token&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only public method for this lexer is &lt;code&gt;#next_token&lt;/code&gt;, which will return a single token, or &lt;code&gt;nil&lt;/code&gt; when there are no more tokens.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    if @scanner.eos?
      nil&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the scanner is at the end of the string (&lt;code&gt;eos&lt;/code&gt; stands for end-of-string), we’ll return &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan(/\s+/)
      [:WHITESPACE, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we match one or more whitespace characters (&lt;code&gt;\s+&lt;/code&gt;), we return a &lt;code&gt;WHITESPACE&lt;/code&gt; token.&lt;/p&gt;&lt;p&gt;Tokens are arrays with two elements: first, the token type as an uppercase symbol, and second, an optional value. The value for this &lt;code&gt;WHITESPACE&lt;/code&gt; token is &lt;code&gt;nil&lt;/code&gt;; we won’t use the value for anything.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan("+")
      [:PLUS, nil]
    elsif @scanner.scan("*")
      [:TIMES, nil]
    elsif @scanner.scan("-")
      [:MINUS, nil]
    elsif @scanner.scan("/")
      [:DIVIDE, nil]
    elsif @scanner.scan(";")
      [:SEMICOLON, nil]
    elsif @scanner.scan("=")
      [:EQUAL, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, we match against some single-character syntax elements.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan(/\d+/)
      [:NUMBER, @scanner.matched]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We match against the regular expression &lt;code&gt;\d+&lt;/code&gt;, which matches one or more digits. For this token, we record the value: &lt;code&gt;@scanner.matched&lt;/code&gt; is the string containing the digits, e.g. &lt;code&gt;"42"&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The next bit deals with keywords and identifiers:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan(/\w+/)
      case @scanner.matched
      when "let"
        [:KW_LET, nil]
      when "print"
        [:KW_PRINT, nil]
      else
        [:IDENTIFIER, @scanner.matched]
      end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This code reads a sequence of alphanumeric characters — anything that could be a keyword or an identifier. Then, we check whether the matched string part is one of the predefined keywords.&lt;/p&gt;&lt;p&gt;Note that this correctly handles identifiers that start with a keyword. For example, &lt;code&gt;"letter"&lt;/code&gt; is an identifier, &lt;em&gt;not&lt;/em&gt; the keyword &lt;code&gt;"let"&lt;/code&gt; followed by the identifier &lt;code&gt;"ter"&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    else
      char = @scanner.getch
      raise "Unknown character: #{char}"
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Lastly, in case the scanner did not match anything, we raise an exception. It’s a crude way of reporting errors, but it’ll do for this basic lexer.&lt;/p&gt;&lt;h3 id="testing-it-out" data-skip-toc="false" data-numbering="2.1"&gt;&lt;span&gt;&lt;span&gt;Testing it out&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;To test out this basic lexer, construct it with a string, and keep calling &lt;code&gt;#next_token&lt;/code&gt; until that method returns &lt;code&gt;nil&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;str = "let amount = 42;"
lexer = BasicLexer.new(str)

loop do
  token = lexer.next_token
  break unless token

  p token
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This code spits out the following, as expected:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;[:KW_LET, nil]
[:WHITESPACE, nil]
[:IDENTIFIER, "amount"]
[:WHITESPACE, nil]
[:EQUAL, nil]
[:WHITESPACE, nil]
[:NUMBER, "42"]
[:SEMICOLON, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="a-modal-lexer" data-skip-toc="false" data-numbering="3"&gt;&lt;span&gt;&lt;span&gt;A modal lexer&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We want to be able to parse code like this:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;print "one plus two is ${1+2}";&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this hypothetical programming language, the expression between &lt;code&gt;${&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt; is evaluated and embedded in the string. This code would thus effectively be the same as &lt;code&gt;print "one plus two is 3";&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The contents of the string are in a different language. Outside of a string, &lt;code&gt;${&lt;/code&gt; would be a lexer error.  Also, &lt;code&gt;one&lt;/code&gt; and &lt;code&gt;plus&lt;/code&gt; are string literal parts, not identifiers. To make our lexer support this code, we’ll allow our lexer two switch between two modes.&lt;sup id="fnref:1" role="doc-noteref"&gt;&lt;a href="#fn:1" class="footnote" rel="footnote"&gt;1&lt;/a&gt;&lt;/sup&gt; First, we’ll have a &lt;em&gt;main&lt;/em&gt; lexer mode, which will do pretty much the same as earlier &lt;code&gt;BasicLexer&lt;/code&gt;. Secondly, we’ll have a &lt;em&gt;string&lt;/em&gt; lexer mode, which will be used for handling the contents of strings.&lt;/p&gt;&lt;p&gt;However, toggling between these two modes is not enough. It is possible to have string interpolation nested within string interpolation:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;print "why ${"${2+6}" * 5}";&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The easiest way to support this in our &lt;code&gt;ModalLexer&lt;/code&gt; class is using a stack of lexer modes:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class ModalLexer
  def initialize(str)
    scanner = StringScanner.new(str)
    mode = MainLexerMode.new(scanner)
    @mode_stack = [mode]
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;ModalLexer&lt;/code&gt; class will, like the previous &lt;code&gt;BasicLexer&lt;/code&gt; class, be constructed with a string. It creates a stack of lexer modes, which will contain just a &lt;code&gt;MainLexerMode&lt;/code&gt; to begin with. This string scanner will be shared across lexer modes.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def next_token
    mode = @mode_stack.last
    mode.next_token(@mode_stack)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Just like with &lt;code&gt;BasicLexer&lt;/code&gt;, the only public method for this lexer is &lt;code&gt;#next_token&lt;/code&gt;. However, the implementation here delegates to the current lexer mode, which sits at the top of the stack.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;#next_token&lt;/code&gt; method for lexer modes takes the mode stack as an argument. This will allow the lexer modes to modify this stack.&lt;/p&gt;&lt;h3 id="the-main-lexer-mode" data-skip-toc="false" data-numbering="3.1"&gt;&lt;span&gt;&lt;span&gt;The main lexer mode&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The implementation of &lt;code&gt;MainLexerMode&lt;/code&gt; is similar to that of &lt;code&gt;BasicLexer&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class MainLexerMode
  def initialize(scanner)
    @scanner = scanner
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A &lt;code&gt;MainLexerMode&lt;/code&gt; is constructed with a &lt;em&gt;string scanner&lt;/em&gt;, rather than a string. The string scanner needs to be shared across lexer modes. This way, when switching lexer modes, the new lexer mode can continue where the previous one left off.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def next_token(mode_stack)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;#next_token&lt;/code&gt; method takes the stack of lexer modes as a parameter.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    if @scanner.eos?
      nil&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As with &lt;code&gt;BasicLexer&lt;/code&gt;, if the scanner is at the end of the string, we return &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan('"')
      mode = StringLexerMode.new(@scanner)
      mode_stack.push(mode)
      [:STRING_START, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we find a &lt;code&gt;"&lt;/code&gt; character, we push a new &lt;code&gt;StringLexerMode&lt;/code&gt; onto the lexer mode stack, and return a &lt;code&gt;STRING_START&lt;/code&gt; token. This way, the next call to &lt;code&gt;#next_token&lt;/code&gt; will be delegated to this new &lt;code&gt;StringLexerMode&lt;/code&gt; instance, at the top of the stack.&lt;/p&gt;&lt;p&gt;The rest of the &lt;code&gt;MainLexerMode&lt;/code&gt; implementation is identical to &lt;code&gt;BasicLexer&lt;/code&gt;.&lt;/p&gt;&lt;h3 id="the-string-lexer-mode" data-skip-toc="false" data-numbering="3.2"&gt;&lt;span&gt;&lt;span&gt;The string lexer mode&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;StringLexerMode&lt;/code&gt; class is the lexer mode that is used inside string literals.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class StringLexerMode
  def initialize(scanner)
    @scanner = scanner
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As with &lt;code&gt;MainLexerMode&lt;/code&gt;, a &lt;code&gt;StringLexerMode&lt;/code&gt; is constructed with a string scanner.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def next_token(mode_stack)
    if @scanner.eos?
      nil&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;#next_token&lt;/code&gt; method starts identical to &lt;code&gt;MainLexerMode&lt;/code&gt;, returning &lt;code&gt;nil&lt;/code&gt; when the scanner is at the end of the string.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan('"')
      mode_stack.pop
      [:STRING_END, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;"&lt;/code&gt; character indicates the end of the string. We pop the current lexer mode off the stack, and return a &lt;code&gt;STRING_END&lt;/code&gt; token.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan("${")
      mode = InterpolationLexerMode.new(@scanner)
      mode_stack.push(mode)
      [:STRING_INTERP_START, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we find &lt;code&gt;${&lt;/code&gt;, we push a new lexer mode: &lt;code&gt;InterpolationLexerMode&lt;/code&gt;. This is a to-be-implemented lexer mode which will be similar to &lt;code&gt;MainLexerMode&lt;/code&gt;. We return a &lt;code&gt;STRING_INTERP_START&lt;/code&gt; token.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan("$")
      [:STRING_PART_LIT, @scanner.matched]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we find a &lt;code&gt;$&lt;/code&gt; character, we return a &lt;code&gt;STRING_PART_LIT&lt;/code&gt; token with that &lt;code&gt;$&lt;/code&gt; character as the content of the token. A &lt;code&gt;STRING_PART_LIT&lt;/code&gt; token is used for literal string content. We know that this &lt;code&gt;$&lt;/code&gt; won’t be followed by a &lt;code&gt;{&lt;/code&gt;, because otherwise it’d have matched the earlier &lt;code&gt;${&lt;/code&gt; case.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    elsif @scanner.scan(/[^"$]+/)
      [:STRING_PART_LIT, @scanner.matched]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we find a character sequence that includes neither &lt;code&gt;"&lt;/code&gt; nor &lt;code&gt;$&lt;/code&gt;, we return a &lt;code&gt;STRING_PART_LIT&lt;/code&gt; token.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    else
      char = @scanner.getch
      raise "Unknown character: #{char}"
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Lastly, if the scanner did not match anything, we raise an exception.&lt;/p&gt;&lt;h3 id="the-string-interpolation-lexer-mode" data-skip-toc="false" data-numbering="3.3"&gt;&lt;span&gt;&lt;span&gt;The string interpolation lexer mode&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The string interpolation lexer mode is used between &lt;code&gt;${&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt; inside a string. This lexer mode is identical to &lt;code&gt;MainLexerMode&lt;/code&gt;, with the difference that it needs to pop the lexer mode stack when it finds &lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;class InterpolationLexerMode
  def initialize(scanner)
    @scanner = scanner
    @delegate = MainLexerMode.new(@scanner)
  end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;InterpolationLexerMode&lt;/code&gt; is constructed with a scanner, similar to the other lexer modes. It also creates a &lt;code&gt;MainLexerMode&lt;/code&gt;, to which it will delegate.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;  def next_token(mode_stack)
    if @scanner.scan("}")
      mode_stack.pop
      [:STRING_INTERP_END, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we find a &lt;code&gt;}&lt;/code&gt; character, we pop the lexer mode stack. Afterwards, the lexer mode at the top of the stack will be a &lt;code&gt;StringLexerMode&lt;/code&gt;. We return a &lt;code&gt;STRING_INTERP_END&lt;/code&gt; token.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;    else
      @delegate.next_token(mode_stack)
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Otherwise, we delegate the &lt;code&gt;#next_token&lt;/code&gt; call to the &lt;code&gt;MainLexerMode&lt;/code&gt; instance.&lt;/p&gt;&lt;h3 id="testing-it-out-" data-skip-toc="false" data-numbering="3.4"&gt;&lt;span&gt;&lt;span&gt;Testing it out&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;As before, we can test out this modal lexer, by construct it with a string, and keep calling &lt;code&gt;#next_token&lt;/code&gt; until that method returns &lt;code&gt;nil&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;string = 'print "one plus two is ${1+2}.";'
lexer = ModalLexer.new(string)

loop do
  token = lexer.next_token
  break unless token

  p token
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This code spits out the following:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;[:KW_PRINT, nil]
[:WHITESPACE, nil]
[:STRING_START, nil]
[:STRING_PART_LIT, "one plus two is "]
[:STRING_INTERP_START, nil]
[:NUMBER, "1"]
[:PLUS, nil]
[:NUMBER, "2"]
[:STRING_INTERP_END, nil]
[:STRING_PART_LIT, "."]
[:STRING_END, nil]
[:SEMICOLON, nil]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note how the string starts with &lt;code&gt;STRING_START&lt;/code&gt; and ends with &lt;code&gt;STRING_END&lt;/code&gt;. Inside, we have literal parts (&lt;code&gt;STRING_PART_LIT&lt;/code&gt;), as well as interpolated parts, which start with &lt;code&gt;STRING_INTERP_START&lt;/code&gt; and end with &lt;code&gt;STRING_INTERP_END&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Now everything is in place for writing a parser.&lt;/p&gt;&lt;h2 id="hints-for-the-parser" data-skip-toc="false" data-numbering="4"&gt;&lt;span&gt;&lt;span&gt;Hints for the parser&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Writing a parser isn’t quite in scope for this article, but it wouldn’t be complete without touching on the subject at least briefly.&lt;/p&gt;&lt;p&gt;In a parser, you’d define a string as the collection of parts between a &lt;code&gt;STRING_START&lt;/code&gt; and &lt;code&gt;STRING_END&lt;/code&gt; token:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;string      -&amp;gt; STRING_START string_part* STRING_END&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A string part can be a literal part, or an interpolated part.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;string_part -&amp;gt; STRING_PART_LIT

string_part -&amp;gt; STRING_INTERP_START
               expression
               STRING_INTERP_END&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For string parts that are &lt;code&gt;STRING_PART_LIT&lt;/code&gt;, you’d extract the token value. For interpolated string parts, you’d evaluate the expression and convert it to a string. This way, parsing simple strings works:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;print "ab5yz";&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;(print
  (string
    (lit "ab5yz")))&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Parsing strings with interpolation works as well:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;print "ab${2+3}yz";&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;(print
  (string
    (lit "ab")
    (expr
      (+ (num 2) (num 3)))
    (lit "yz")))&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To simplify this AST, you might consider de-sugaring this syntax. Here’s an example where I’ve replaced the &lt;code&gt;string&lt;/code&gt; and &lt;code&gt;expr&lt;/code&gt; AST nodes with calls to my &lt;code&gt;concat()&lt;/code&gt; and &lt;code&gt;toString()&lt;/code&gt; functions:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;(print
  (funcall
    "concat"
    (lit "ab")
    (funcall
      "toString"
      (+ (num 2) (num 3)))
    (lit "yz")))&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="wrapping-up" data-skip-toc="false" data-numbering="5"&gt;&lt;span&gt;&lt;span&gt;Wrapping up&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This wraps up the modal lexing example, though plenty can can be done to improve this lexer. These are the bits I left out, for the sake of simplicity:&lt;/p&gt;&lt;ul class="stack-3"&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;StringLexerMode&lt;/code&gt; does not handle escape sequences, like &lt;code&gt;\"&lt;/code&gt;, &lt;code&gt;\\&lt;/code&gt;, or &lt;code&gt;\$&lt;/code&gt;. Escape sequences are indispensable in a real-world string lexer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The lexer does not track source location (line number, column number, or filename). This too is indispensable for reporting errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Speaking of errors: The lexer’s error handling leaves much to be desired.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The lexer emits &lt;code&gt;WHITESPACE&lt;/code&gt; tokens, which might not be desirable. These could be handled by the parser, or stripped entirely beforehand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I’ve not paid attention to performance characteristics of this lexer. My assumption is that &lt;code&gt;StringScanner&lt;/code&gt; is fast enough, especially with pre-compiled regular expressions. However, it might be worth looking into other approaches and tools for lexing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;InterpolationLexerMode&lt;/code&gt; will pop the lexer mode stack as soon as it finds a &lt;code&gt;}&lt;/code&gt;. This will cause problems if the &lt;code&gt;}&lt;/code&gt; token is used for other purposes as well, such as defining functions (e.g. &lt;code&gt;fn a() { … }&lt;/code&gt;) or creating maps (e.g. &lt;code&gt;let thing = { value: 123 };&lt;/code&gt;). The trick here is to push a &lt;code&gt;MainLexerMode&lt;/code&gt; when finding a &lt;code&gt;{&lt;/code&gt;, and letting &lt;code&gt;MainLexerMode&lt;/code&gt; pop the stack when finding a &lt;code&gt;}&lt;/code&gt;. Now &lt;code&gt;InterpolationLexerMode&lt;/code&gt; is redundant and can be substituted with &lt;code&gt;MainLexerMode&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This modal lexing technique is not just usable for handling string interpolation. It can handle nested comment as well (e.g. &lt;code&gt;/* left /* inner comment */ right */&lt;/code&gt;), and it even allows embedding &lt;em&gt;entire different languages&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;The possibilities are limited only by your imagination.&lt;/p&gt;&lt;h2 id="references" data-skip-toc="false" data-numbering="6"&gt;&lt;span&gt;&lt;span&gt;References&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This article was inspired by &lt;a href="https://www.oilshell.org/blog/2016/10/19.html"&gt;How OSH Uses Lexer Modes&lt;/a&gt; (2016), which introduces the concept of modal lexing. You might also be interested in these:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/stdlib-3.1.0/libdoc/strscan/rdoc/StringScanner.html"&gt;Ruby documentation on &lt;code&gt;StringScanner&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.1/Regexp.html#label-Character+Classes"&gt;Ruby documentation on &lt;code&gt;Regexp&lt;/code&gt; character classes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol class="stack-3"&gt;
&lt;li id="fn:1" role="doc-endnote"&gt;&lt;p&gt;This change turns the lexer from a finite automaton into a pushdown automaton. This means that the lexer no longer recognizes a &lt;em&gt;regular language&lt;/em&gt; but a &lt;em&gt;context-free language&lt;/em&gt;. The textbook theory says that lexers should be regular. But modal lexers are such an elegant solution that I’m quite happy to drop the theory and go with what &lt;em&gt;actually works&lt;/em&gt;. &lt;a href="#fnref:1" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <summary type="html">How do you write a lexer for &lt;code&gt;print "one plus two is ${1+2}";&lt;/code&gt;? The interpolation makes this tricky, as the string forms a sublanguage that is different from the main language. To make things worse, interpolation can be nested. Textbook lexers won’t cut it, and we need something stronger. Enter &lt;em&gt;modal lexers&lt;/em&gt;.
</summary>
  </entry>
  <entry>
    <id>tag:denisdefreyne.com,2021-07-16:/articles/2021-proposal-documents/</id>
    <title type="html">Drive change by writing structured proposal documents</title>
    <published>2021-07-16T22:00:00Z</published>
    <updated>2021-07-16T22:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2021-proposal-documents/" type="text/html"/>
    <content type="html">&lt;p&gt;In any collaborative environment, opportunities for change often arise. When such an opportunity presents itself, write up a structured proposal document describing how to tackle this opportunity. With this document, a decision on whether or not to implement the proposal can be made quickly and confidently.&lt;/p&gt;&lt;p&gt;I use this approach extensively, both at my employers as well as for side projects that are used by others. These documents are often called &lt;i&gt;RFC&lt;/i&gt;s (short for &lt;i&gt;requests for comment&lt;/i&gt;).&lt;/p&gt;&lt;h2 id="motivation" data-skip-toc="false" data-numbering="1"&gt;&lt;span&gt;&lt;span&gt;Motivation&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Whether when working alone or collaborating with others, opportunities for change will come up. While many such changes are trivial, some are not:&lt;/p&gt;&lt;ul class="stack-3"&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Changes that impact others,&lt;/strong&gt; such as restructuring the filesystem of a project, or reformatting all source code to the latest standards.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Changes that are not easily reversible,&lt;/strong&gt; such as the choice of programming language or framework to use for a new project, or the switch to a new paid subscription of a third-party tool.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;To make decisions on how to move forward with such opportunities for change, we need to have as much information as possible about the change and its implications.&lt;/p&gt;&lt;p&gt;We want to collect all relevant information regarding a potential decision from all stakeholders. At the same time, we want to make sure that all stakeholders understand the context and motivation around this change, and the impact it will have.&lt;/p&gt;&lt;h2 id="proposed-solution" data-skip-toc="false" data-numbering="2"&gt;&lt;span&gt;&lt;span&gt;Proposed solution&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;When an opportunity for change arises, write up a document that describes how to take advantage of this opportunity. This proposal document has the following format:&lt;/p&gt;&lt;ol class="stack-3"&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context (optional)&lt;/strong&gt;: What background information (if any) is needed to understand the rest of this document? Consider adding a glossary in this section to clarify any jargon that readers might not be familiar with.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Motivation&lt;/strong&gt;: What pain point does this proposal address?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Solution constraints (optional)&lt;/strong&gt;: What is the set of requirements for every solution for the aforementioned pain point? Consider organising these in “must have”, “should have”, “nice to have”, and “optional”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Proposed solution&lt;/strong&gt;: How will the aforementioned pain points be addressed? What are the implications of this proposed solution? What are the advantages and disadvantages of this solution? Consider adding examples of how this solution would work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alternative solutions (optional)&lt;/strong&gt;: What other solutions would also alleviate this pain point? What advantages and disadvantages do they have compared to the proposed solution?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open questions and concerns (optional)&lt;/strong&gt;: Are there any questions that have not (yet) been answered? Are there concerns that have not (yet) been taken into account? In this section, surface all questions and concerns, even if they might not need to be tackled in this document.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Write this document on a platform that supports collaboration, and has fine-grained commenting functionality, such as Google Docs or Notion. While other approaches work (e.g. sending Word documents and updates to them over email), they have long and cumbersome feedback loops. A collaborative platform tightens the feedback loop, and thereby increases participation. Increased participation means more information is shared, and thus increases the quality of the decisions.&lt;/p&gt;&lt;p&gt;Perform research for decisions asynchronously. Avoid making decisions synchronously, e.g. in a real-time meeting.&lt;/p&gt;&lt;h3 id="ownership" data-skip-toc="false" data-numbering="2.1"&gt;&lt;span&gt;&lt;span&gt;Ownership&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Ensure that a single person is assigned as the owner of this document. The owner is responsible for writing and maintaining this document. All suggested changes to the document should go through the document’s owner.&lt;sup id="fnref:1" role="doc-noteref"&gt;&lt;a href="#fn:1" class="footnote" rel="footnote"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;h3 id="lifecycle" data-skip-toc="false" data-numbering="2.2"&gt;&lt;span&gt;&lt;span&gt;Lifecycle&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Until the proposed solution in this document is accepted, keep the proposal document updated with any new information that comes in. Ensure that all people involved are aware of updates to the document.&lt;/p&gt;&lt;p&gt;Ensure that there is a clear point where the proposal is either accepted or rejected. Setting a deadline up front can be useful.&lt;/p&gt;&lt;h3 id="example" data-skip-toc="false" data-numbering="2.3"&gt;&lt;span&gt;&lt;span&gt;Example&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This very document you’re reading is structured using this format.&lt;/p&gt;&lt;h3 id="advantages" data-skip-toc="false" data-numbering="2.4"&gt;&lt;span&gt;&lt;span&gt;Advantages&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;By documenting the decisions around proposals, a historical record can be kept, and can be referred to later on if needed. In particular, it can shed clarity on the constraints that were (and perhaps no longer are) present when a past decision was made.&lt;/p&gt;&lt;p&gt;This approach ensures all potential stakeholders are involved in the process, and that their input is taken into account.&lt;/p&gt;&lt;p&gt;With this approach, not everyone is &lt;em&gt;required&lt;/em&gt; to participate in the process. In this way, this approach can be used to &lt;a href="/notes/get-people-on-board-by-radiating-intent/"&gt;get people on board with breaking changes by radiating intent&lt;/a&gt;.&lt;/p&gt;&lt;h3 id="disadvantages" data-skip-toc="false" data-numbering="2.5"&gt;&lt;span&gt;&lt;span&gt;Disadvantages&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This approach is not well-suited for trivial decisions, nor for urgent decisions.&lt;/p&gt;&lt;h2 id="open-questions-and-concerns" data-skip-toc="false" data-numbering="3"&gt;&lt;span&gt;&lt;span&gt;Open questions and concerns&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;ul class="stack-3"&gt;
&lt;li&gt;&lt;p&gt;It might be worth adding a &lt;a href="https://en.wikipedia.org/wiki/Responsibility_assignment_matrix"&gt;RACI matrix&lt;/a&gt; to the document, to state who is responsible, accountable, consulted, and informed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It might be worth documenting the thought process that went into the proposed solution. References (bibliography) would then probably go into a subsection of the &lt;i&gt;Proposed solution&lt;/i&gt; section.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol class="stack-3"&gt;
&lt;li id="fn:1" role="doc-endnote"&gt;&lt;p&gt;If “owner” sounds too rigid, consider “steward” instead. &lt;a href="#fnref:1" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <summary type="html">In any collaborative environment, opportunities for change often arise. When such an opportunity presents itself, write up a structured proposal document describing how to tackle this opportunity. With this document, a decision on whether or not to implement the proposal can be made quickly and confidently.</summary>
  </entry>
  <entry>
    <id>tag:denisdefreyne.com,2021-06-12:/articles/2021-budget-replacement/</id>
    <title type="html">Budget for replacing past purchases</title>
    <published>2021-06-12T22:00:00Z</published>
    <updated>2021-06-12T22:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2021-budget-replacement/" type="text/html"/>
    <content type="html">&lt;p&gt;Unexpected expenses can cause financial anxiety. This is true for me, even though I have a good salary and a solid emergency buffer. One way in which I reduce my financial anxiety is by predicting expenses based on the expected lifetime of a purchase.&lt;/p&gt;&lt;h2 id="limited-lifetime-items-with-non-trivial-cost" data-skip-toc="false" data-numbering="1"&gt;&lt;span&gt;&lt;span&gt;Limited-lifetime items with non-trivial cost&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Some items have a limited lifetime (e.g. 4–5 years) and have a non-trivial cost (e.g. €1500). A non-trivial cost is a cost that, if it were to suddenly show up, would stir up my budget enough to cause stress. For me, these items fit primarily into three categories:&lt;sup id="fnref:1" role="doc-noteref"&gt;&lt;a href="#fn:1" class="footnote" rel="footnote"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Electronic devices: laptop, phone, speakers, external display, …&lt;/li&gt;
&lt;li&gt;Larger appliances: dishwasher, washing machine, …&lt;/li&gt;
&lt;li&gt;Furniture with limited lifetime: office chair, mattress, …&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This explicitly excludes items with non-trivial cost that have a long (expected) lifetime. For example, this does this not include my expensive (but gorgeous) kitchen table, which hopefully will last forever.&lt;/p&gt;&lt;h2 id="how-to-budget-for-replacement" data-skip-toc="false" data-numbering="2"&gt;&lt;span&gt;&lt;span&gt;How to budget for replacement&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Starting out with this technique requires some initial setup. Once that’s done, the technique is easy to apply on a monthly basis.&lt;/p&gt;&lt;h3 id="starting-out" data-skip-toc="false" data-numbering="2.1"&gt;&lt;span&gt;&lt;span&gt;Starting out&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Make a list of all items with limited lifetime and non-trivial cost. You can use the categorization above as a guideline.&lt;/p&gt;&lt;p&gt;For each item, roughly estimate its remaining expected lifetime, and the cost to replace it. For example: I bought my MacBook Pro in late 2017 and expect its lifetime to be 5 years. At the time of writing (early 2021), the expected remaining lifetime is roughly 20 months, and replacing it would cost roughly €1500.&lt;/p&gt;&lt;h3 id="monthly-budgeting" data-skip-toc="false" data-numbering="2.2"&gt;&lt;span&gt;&lt;span&gt;Monthly budgeting&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Each month, budget the amount of money needed to cover the replacement cost in the expected lifetime. For example, the €1500 for replacing my MacBook Pro over the remaining 20 months of its lifetime comes down to €75/month.&lt;/p&gt;&lt;p&gt;For each future purchase with limited lifetime and non-trivial cost, do this budgeting calculation &lt;em&gt;right when you make the purchase&lt;/em&gt;. For example, if I were to replace my MacBook Pro right now (assuming the replacement has the same cost of €1500 and the same expected lifetime of 5 years), I’d immediately start budgeting €1500 spread over 5 years, which comes down to €25/month.&lt;sup id="fnref:2" role="doc-noteref"&gt;&lt;a href="#fn:2" class="footnote" rel="footnote"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;h2 id="lifetime-vs-warranty" data-skip-toc="false" data-numbering="3"&gt;&lt;span&gt;&lt;span&gt;Lifetime vs. warranty&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;It is safe to use the warranty period as the lifetime, but this can be overly pessimistic. For example, my 2017 MacBook Pro is out of warranty, but I expect to keep on using it for at least another year.&lt;/p&gt;&lt;p&gt;In the end, it comes down to how much risk (uncertainty) you’re willing to take on.&lt;/p&gt;&lt;h2 id="advantages" data-skip-toc="false" data-numbering="4"&gt;&lt;span&gt;&lt;span&gt;Advantages&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I am able to deal with unanticipated expenses&lt;/strong&gt; in a straightforward, no-anxiety way. Ideally, I only replace the item once I have all the money for replacement saved up, but even if I need to replace it before its expected lifetime ends, I will at least have a helpful part of the money saved up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I see which items have extended past their expected lifetime.&lt;/strong&gt; Because these items have been fully-funded, I can replace them with no financial impact — if I need or want to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It makes me prefer purchasing long-lasting items&lt;/strong&gt; over short-lasting ones, as short-lasting items will have a potentially higher monthly cost (for budgeting) associated with them.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;h2 id="disadvantages" data-skip-toc="false" data-numbering="5"&gt;&lt;span&gt;&lt;span&gt;Disadvantages&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;It looks rather gloomy to immediately start budgeting for replacing an expensive item, right after purchasing it.&lt;/p&gt;&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol class="stack-3"&gt;
&lt;li id="fn:1" role="doc-endnote"&gt;&lt;p&gt;This categorization isn’t all-encompassing: some items with non-trivial cost and limited lifetime don’t fit in properly, e.g. my glasses. &lt;a href="#fnref:1" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn:2" role="doc-endnote"&gt;&lt;p&gt;Budgeting software exists to make this easier! I use &lt;a href="https://www.youneedabudget.com"&gt;You Need A Budget&lt;/a&gt;, which works nicely with this approach. I’m also keeping my eye on &lt;a href="https://actualbudget.com"&gt;Actual&lt;/a&gt;. &lt;a href="#fnref:2" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <summary type="html">One way in which budgeting reduces financial anxiety is by predicting expenses based on the expected lifetime of a purchase.</summary>
  </entry>
  <entry>
    <id>tag:denisdefreyne.com,2021-06-08:/articles/2021-horizontal-layout/</id>
    <title type="html">Horizontally scrolling multi-column layout: a retrospective</title>
    <published>2021-06-08T22:00:00Z</published>
    <updated>2021-06-08T22:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2021-horizontal-layout/" type="text/html"/>
    <content type="html">&lt;p&gt;On wide screens, and especially ultra-wide screens, a single column of text often requires a considerable amount of whitespace to the left and the right. This whitespace is needed to prevent the text lines in the column from growing overly long, and thus hard to read.&lt;/p&gt;

&lt;p&gt;All this whitespace is wasted screen real estate. In this experiment, I aimed at using screen real estate more efficiently using CSS’ multi-column support.&lt;/p&gt;

&lt;figure class="col-wide"&gt;
  &lt;img style="max-width: 100%; object-fit: contain" src="/articles/2021-horizontal-layout/screenshot.png"&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href="/articles/2021-horizontal-layout/sample/"&gt;See it in action&lt;/a&gt;.&lt;/p&gt;

&lt;h2 data-numbering="1" id="what-worked"&gt;What worked&lt;/h2&gt;

&lt;p&gt;So much less wasted screen real estate!&lt;/p&gt;

&lt;p&gt;Reading an article involved a lot less scrolling up and down to refer to previous bits of in the article. This makes sense, as there’s considerably more text visible on the screen.&lt;/p&gt;

&lt;p&gt;I’m fond of the aesthetic: it reminds me of a book or a magazine.&lt;/p&gt;

&lt;h2 data-numbering="2" id="what-didnt-work"&gt;What didn’t work&lt;/h2&gt;

&lt;p&gt;As excited as I was, this experiment revealed some issues.&lt;/p&gt;

&lt;h3 id="controlling-column-widths"&gt;Controlling column widths&lt;/h3&gt;

&lt;p&gt;CSS’ multi-column support really wants to divide the available space in an integer number of columns. This means that the actual width of the columns depends on the width of the container (in this experiment, the container is the window). This happens even when setting an explicit column width using the &lt;code&gt;column-width&lt;/code&gt; property, because this property defines the &lt;em&gt;ideal&lt;/em&gt; column width, not the actual column width. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/column-width"&gt;Quoting MDN&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The &lt;code&gt;column-width&lt;/code&gt; CSS property sets the ideal column width in a multi-column layout. The container will have as many columns as can fit without any of them having a width less than the &lt;code&gt;column-width&lt;/code&gt; value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a consequence, it becomes non-obvious that you can scroll right: there is no indication that there are additional columns past the last-visible column. To resolve this problem in this experiment, I cheated and made the next column half visible, which I think gives enough of a hint that there is more content beyond what is visible.&lt;/p&gt;

&lt;p&gt;Furthermore, as a result of column widths being calculated based on the container width, resizing the window will resize the columns as well. In this experiment, this behavior is jarring, as text reflows, especially when the viewport is already scrolled considerably to the right.&lt;/p&gt;

&lt;h3 id="getting-scrolling-to-work"&gt;Getting scrolling to work&lt;/h3&gt;

&lt;p&gt;Horizontal scrolling is easy using a trackpad, but I had to add some custom JavaScript to make the mouse wheel scroll horizontally as well. I intercepted the page-up and page-down keys as well, and injected some custom JavaScript to make smooth scrolling work.&lt;/p&gt;

&lt;p&gt;Unfortunately, the custom JavaScript is not doing its job quite well. For example, holding space or page-down will not make the scrolling go faster, but instead it will confusingly slow it down. This is a bug in the JavaScript code, but I’d rather rely on native scrolling than on JavaScript.&lt;/p&gt;

&lt;p&gt;I saw mentions of a JavaScript-less solution that involves multiple nested CSS transformations (rotating the container 90° and then rotating the container’s children the opposite way), but I could not get this approach to work.&lt;/p&gt;

&lt;h3 id="padding"&gt;Padding&lt;/h3&gt;

&lt;p&gt;For reasons I don’t fully understand, ensuring that the last column has some right padding is particularly difficult. Without this right padding, the last column sits awkwardly against the window border. I got this to work by adding a fat right border to every element inside the multi-column container, which is a terrible hack and I feel bad about it. Someone needs to come up with a better idea.&lt;/p&gt;

&lt;h3 id="controlling-column-breaks"&gt;Controlling column breaks&lt;/h3&gt;

&lt;p&gt;In multi-column media, like in multi-page media, it becomes important to control how elements are broken across columns. The &lt;code&gt;break-inside: avoid&lt;/code&gt; CSS property works well, though it seems to be the only well-supported property. CSS’ widows and orphans support is spotty across browsers. So is the &lt;code&gt;break-after: avoid&lt;/code&gt; property, which I’d love to have used on headers — keeping headers together with the first-following paragraph makes a difference.&lt;/p&gt;

&lt;h2 data-numbering="3" id="open-questions-and-future-work"&gt;Open questions and future work&lt;/h2&gt;

&lt;p&gt;Is horizontal scrolling a good idea? It’s unconventional, but does that mean it should be avoided?&lt;/p&gt;

&lt;p&gt;Overflowing columns in the block direction would be sweet. &lt;a href="https://www.smashingmagazine.com/2019/01/css-multiple-column-layout-multicol/"&gt;Quoting Rachel Andrew&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;For Level 2 of the specification, we are considering how to enable a method by which overflow columns, those which currently end up causing the horizontal scrollbar, could instead be created in the block direction. This would mean that &lt;strong&gt;you could have a multicol container with a height, and once the content had made columns which filled that container, a new set of columns would be created below&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Emphasis mine.) This approach would eliminate issues with scrolling (no JavaScript needed, and scrolling would be vertical and thus more conventional).&lt;/p&gt;

&lt;h2 data-numbering="4" id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;

&lt;p&gt;CSS’ multi-column support is not usable for long-form content yet. While there is certainly progress made, and the opportunities are exciting, the usability problems related to non-conventional scrolling make it difficult to use, and browser support is too spotty to provide a reliable, comfortable experience.&lt;/p&gt;
</content>
    <summary type="html">On wide screens, a column of text often needs a considerable amount of whitespace to the left and to the right. I experimented with CSS’ multi-column layout to use screen real estate more efficiently. While I ended up quite liking the aesthetic, usability problems and spotty browser support make this unfortunately an impractical approach.</summary>
  </entry>
  <entry>
    <id>tag:denisdefreyne.com,2020-11-23:/articles/2020-lenses/</id>
    <title type="html">Using Lenses to build complex React forms in a type-safe way</title>
    <published>2020-11-23T23:00:00Z</published>
    <updated>2020-11-23T23:00:00Z</updated>
    <link rel="alternate" href="https://denisdefreyne.com/articles/2020-lenses/" type="text/html"/>
    <content type="html">&lt;p&gt;Forms in React are not straightforward to build. For trivial forms, plain-old React is good, but once the forms get more complex, you’ll likely find yourself searching for a good form-building library for React.&lt;/p&gt;&lt;p&gt;In this article, I propose an alternative approach to building forms, one based on &lt;em&gt;lenses&lt;/em&gt;, tightly integrated with Type­Script&lt;sup id="fnref:1" role="doc-noteref"&gt;&lt;a href="#fn:1" class="footnote" rel="footnote"&gt;1&lt;/a&gt;&lt;/sup&gt; to tighten the feedback loop, reducing the change of mistakes and speeding up development.&lt;/p&gt;&lt;ol class="toc toc__list toc__root-list" role="doc-toc"&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#the-trouble-with-forms"&gt;&lt;span&gt;&lt;span&gt;The trouble with forms&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#tightening-the-feedback-loop-using-type-safety"&gt;&lt;span&gt;&lt;span&gt;Tightening the feedback loop using type safety&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#demo-app"&gt;&lt;span&gt;&lt;span&gt;Demo app&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#what-is-a-lens"&gt;&lt;span&gt;&lt;span&gt;What is a lens?&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#lenses-more-formally"&gt;&lt;span&gt;&lt;span&gt;Lenses, more formally&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#conveniently-creating-lenses-with-forprop"&gt;&lt;span&gt;&lt;span&gt;Conveniently creating lenses with forProp()&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#lenses-for-forms"&gt;&lt;span&gt;&lt;span&gt;Lenses for forms&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#minimal-form-example"&gt;&lt;span&gt;&lt;span&gt;Minimal form example&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#prettier-form-example"&gt;&lt;span&gt;&lt;span&gt;Prettier form example&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#handling-nested-data-by-composing-lenses"&gt;&lt;span&gt;&lt;span&gt;Handling nested data by composing lenses&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#handling-lists"&gt;&lt;span&gt;&lt;span&gt;Handling lists&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#adding-and-removing-list-elements"&gt;&lt;span&gt;&lt;span&gt;Adding and removing list elements&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#improving-on-single-select-dropdowns"&gt;&lt;span&gt;&lt;span&gt;Improving on single-select dropdowns&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;ol class="toc__list toc__child-list"&gt;&lt;li class="toc__item toc__child-item"&gt;&lt;a href="#dropdowns-versus-radio-buttons"&gt;&lt;span&gt;&lt;span&gt;Dropdowns versus radio buttons&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#future-work"&gt;&lt;span&gt;&lt;span&gt;Future work&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li class="toc__item toc__root-item"&gt;&lt;a href="#closing-thoughts"&gt;&lt;span&gt;&lt;span&gt;Closing thoughts&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h2 id="the-trouble-with-forms" data-skip-toc="false" data-numbering="1"&gt;&lt;span&gt;&lt;span&gt;The trouble with forms&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;During my time at BCG Digital Ventures, I worked on an internal tool where one particular page has a remarkably complex form: multiple dozens of text fields, radio buttons, checkboxes, and dropdowns, and in addition to all that, it had editable lists inside editable lists, with even more form fields inside.&lt;/p&gt;&lt;p&gt;The implementation of this form had gone through several revisions to increase its robustness and make it more maintainable. The penultimate version was built with &lt;a href="https://formik.org/"&gt;Formik&lt;/a&gt;, which removed a ton of complexity and provided a good out-of-the-box experience.&lt;/p&gt;&lt;p&gt;However, even the version built with Formik had issues. Like other mainstream React form-handling libraries, Formik has trouble dealing with highly-complex nested data structures. In particular, I found that spelling mistakes in field names&lt;sup id="fnref:2" role="doc-noteref"&gt;&lt;a href="#fn:2" class="footnote" rel="footnote"&gt;2&lt;/a&gt;&lt;/sup&gt; create subtle bugs that are hard to detect, and require extensive automated or manual testing to uncover.&lt;/p&gt;&lt;p&gt;We can improve on this by making good use of the type system!&lt;/p&gt;&lt;h3 id="tightening-the-feedback-loop-using-type-safety" data-skip-toc="false" data-numbering="1.1"&gt;&lt;span&gt;&lt;span&gt;Tightening the feedback loop using type safety&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Many mainstream React form libraries are written in plain-old JavaScript. Some of them are written in Type­Script, but are lax with types, and provide little type safety. Type safety is useful because tightens the feedback loop: misspelled field names will be highlighted as errors in the IDE in real time, and the IDE will provide reliable and useful autocompletion with integrated documentation.&lt;/p&gt;&lt;p&gt;The functional programming community came up with the concept of &lt;em&gt;lenses&lt;/em&gt;. Lenses, as you’ll see in more detail further down, are in essence bi-directional accessors for immutable objects. The immutability fits in nicely with React, and the type safety they provide tightens the development feedback loop.&lt;/p&gt;&lt;p&gt;I reimplemented the remarkably complex form using lenses, and was delighted with the outcome: lenses are capable of handling highly-complex data in a type-safe way. Lenses helped eliminated bugs and prevent regressions, and significantly sped up development. Lenses allow building complex forms quickly and with confidence.&lt;/p&gt;&lt;h3 id="demo-app" data-skip-toc="false" data-numbering="1.2"&gt;&lt;span&gt;&lt;span&gt;Demo app&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Below, you will find a live demo of something you can build with lenses. You can find the full implementation in the matching &lt;a href="https://github.com/denisdefreyne/lenses-demo"&gt;demo Git repository&lt;/a&gt;.&lt;/p&gt;&lt;div id="lenses-demo-root" style="isolation: isolate" class="col-wide p-3 demo-wrapper"&gt;&lt;/div&gt;&lt;script src="sample/index.js"&gt;&lt;/script&gt;&lt;h2 id="what-is-a-lens" data-skip-toc="false" data-numbering="2"&gt;&lt;span&gt;&lt;span&gt;What is a lens?&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;To describe what a lens is, we’ll use an example to create a lens from scratch.&lt;/p&gt;&lt;p&gt;Imagine a Person type, and an instance of Person that represents Sherlock Holmes:&lt;/p&gt;&lt;pre&gt;type Person = {
  firstName: string;
  lastName: string;
};

let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes"
};&lt;/pre&gt;&lt;p&gt;We can get Sherlock’s first name:&lt;/p&gt;&lt;pre&gt;sherlock.firstName
  // -&amp;gt; "Sherlock"&lt;/pre&gt;&lt;p&gt;The game is afoot!&lt;/p&gt;&lt;p&gt;We could also write some code to update the first name, which we’ll do in a purely functional way, meaning that we won’t modify the object itself, but rather return a new object with the updated data:&lt;/p&gt;&lt;pre&gt;let locky = { ...sherlock, firstName: "Locky" }
  // -&amp;gt; {
  //      firstName: "Locky",
  //      lastName: "Holmes"
  //    }&lt;/pre&gt;&lt;p&gt;Perhaps Sherlock wouldn’t like to be called Locky. We’ll never know.&lt;/p&gt;&lt;p&gt;We can create functions for getting and updating a person’s first name:&lt;/p&gt;&lt;pre&gt;let getFirstName =
  (person: Person) =&amp;gt; person.firstName

let setFirstName =
  (person: Person, firstName: string) =&amp;gt;
    ({ ...person, firstName: firstName })&lt;/pre&gt;&lt;p&gt;Let’s fix Sherlock’s name:&lt;/p&gt;&lt;pre&gt;getFirstName(locky)
  // -&amp;gt; "Locky"

setFirstName(locky, "Sherlock")
  // -&amp;gt; {
  //      firstName: "Sherlock",
  //      lastName: "Holmes"
  //    }&lt;/pre&gt;&lt;p&gt;We can combine the getter and the setter into a single object:&lt;/p&gt;&lt;pre&gt;let firstNameLens = {
  get: (person: Person) =&amp;gt;
    person.firstName

  set: (person: Person, firstName: string) =&amp;gt;
    ({ ...person, firstName: firstName })
}

firstNameLens.get(locky)
  // -&amp;gt; "Locky"

firstNameLens.set(locky, "Sherlock")
  // -&amp;gt; {
  //      firstName: "Sherlock",
  //      lastName: "Holmes"
  //    }&lt;/pre&gt;&lt;p&gt;Congratulations: &lt;code&gt;firstNameLens&lt;/code&gt; is your first lens!&lt;/p&gt;&lt;h3 id="lenses-more-formally" data-skip-toc="false" data-numbering="2.1"&gt;&lt;span&gt;&lt;span&gt;Lenses, more formally&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The lens that we constructed above has the following type:&lt;/p&gt;&lt;pre&gt;type PersonFirstNameLens = {
  // Given a person,
  // get a string (the first name)
  get: (person: Person) =&amp;gt; string;

  // Given a person and a string
  // (the first name),
  // return a new person
  set: (person: Person, firstName: string) =&amp;gt; Person;
};&lt;/pre&gt;&lt;p&gt;This lens is for two specific types:&lt;/p&gt;&lt;ul class="stack-3"&gt;
&lt;li&gt;The &lt;code&gt;Person&lt;/code&gt; type is the top type: the type that contains the data that you want to extract (using get), or the data that you want to update (using set).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;string&lt;/code&gt; type is the focused type: the type of the extracted data.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;With these two types in mind, we can construct a generic Lens type, with two type parameters (&lt;code&gt;T&lt;/code&gt; for the top type, and &lt;code&gt;F&lt;/code&gt; for the focused type):&lt;/p&gt;&lt;pre&gt;type Lens&amp;lt;T, F&amp;gt; = {
  get: (t: T) =&amp;gt; F;
  set: (t: T, f: F) =&amp;gt; T;
};&lt;/pre&gt;&lt;p&gt;The type definition makes it clear: &lt;mark&gt;a lens is the combination of a getter and a setter, for an immutable data structure&lt;/mark&gt;.&lt;/p&gt;&lt;h3 id="conveniently-creating-lenses-with-forprop" data-skip-toc="false" data-numbering="2.2"&gt;&lt;span&gt;&lt;span&gt;Conveniently creating lenses with forProp()&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;It is convenient to have a function that can automatically create a lens for a property. This is where &lt;code&gt;forProp()&lt;/code&gt; comes in:&lt;sup id="fnref:3" role="doc-noteref"&gt;&lt;a href="#fn:3" class="footnote" rel="footnote"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;pre&gt;let firstNameLens =
  forProp&amp;lt;Person&amp;gt;()("firstName");&lt;/pre&gt;&lt;p&gt;A lens returned by &lt;code&gt;forProp()&lt;/code&gt; behaves exactly the same as a manually-constructed one:&lt;/p&gt;&lt;pre&gt;let john: Person = {
  firstName: "John",
  lastName: "Watson"
};

firstNameLens.get(john);
  // -&amp;gt; "John"
firstNameLens.set(john, "Joe");
  // -&amp;gt; { firstName: "Joe", lastName: "Watson" }&lt;/pre&gt;&lt;p&gt;I’ll make a guess that John Watson wouldn’t like to be called Joe.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;forProp()&lt;/code&gt; function is type-safe, as it won’t accept non-existent properties:&lt;/p&gt;&lt;pre&gt;// Type error!
// Person has no property middleName
let middleNameLens =
  forProp&amp;lt;Person&amp;gt;()("middleName");&lt;/pre&gt;&lt;p&gt;We’ll not talk about the implementation, but you can check out &lt;a href="https://github.com/denisdefreyne/lenses-demo/blob/main/src/lenses.ts"&gt;its implementation&lt;/a&gt; in the demo sandbox.&lt;/p&gt;&lt;h2 id="lenses-for-forms" data-skip-toc="false" data-numbering="3"&gt;&lt;span&gt;&lt;span&gt;Lenses for forms&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;A lens-powered form field needs three properties:&lt;/p&gt;&lt;pre&gt;interface BareTextFieldProps&amp;lt;T&amp;gt; {
  lens: Lens&amp;lt;T, string&amp;gt;;
  top: T;
  setTop: (t: T) =&amp;gt; void;
}&lt;/pre&gt;&lt;p&gt;A form field needs the lens (for getting and setting the data for this field), but also &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;setTop()&lt;/code&gt;, which are used for getting and setting the top-level object.&lt;/p&gt;&lt;p&gt;Note the similarity between &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;setTop()&lt;/code&gt; and what the React &lt;code&gt;useState&lt;/code&gt; hook returns — we’ll come back to this later.&lt;/p&gt;&lt;p&gt;This minimalist text field’s implementation is as follows:&lt;/p&gt;&lt;pre&gt;export let BareTextField = &amp;lt;T extends any&amp;gt;({
  lens,
  top,
  setTop
}: BareTextFieldProps&amp;lt;T&amp;gt;) =&amp;gt; {
  // Read value through lens
  let value = &lt;mark&gt;lens.get(top)&lt;/mark&gt;;

  // Replace top with new instance
  // updated through lens
  let setValue = (newValue: string) =&amp;gt; {
    &lt;mark&gt;setTop(lens.set(top, newValue))&lt;/mark&gt;;
  };

  return (
    &amp;lt;input
      type="text"
      value={value}
      onChange={e =&amp;gt; setValue(e.target.value)}
    /&amp;gt;
  );
};&lt;/pre&gt;&lt;p&gt;This React component is a controlled component, so the wrapped &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; component is given both a &lt;code&gt;value&lt;/code&gt; prop and an &lt;code&gt;onChange&lt;/code&gt; prop.&lt;/p&gt;&lt;h3 id="minimal-form-example" data-skip-toc="false" data-numbering="3.1"&gt;&lt;span&gt;&lt;span&gt;Minimal form example&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;We’ll create a form for a new person. First, we’ll need our lenses:&lt;/p&gt;&lt;pre&gt;let firstNameLens =
  forProp&amp;lt;Person&amp;gt;()("firstName");

let lastNameLens =
  forProp&amp;lt;Person&amp;gt;()("lastName");&lt;/pre&gt;&lt;p&gt;We’ll also need a way to create a blank person object:&lt;/p&gt;&lt;pre&gt;let newPerson = (): Person =&amp;gt; ({
  firstName: "",
  lastName: ""
});&lt;/pre&gt;&lt;p&gt;The skeleton of our form will look like this:&lt;/p&gt;&lt;pre&gt;let PersonForm = () =&amp;gt; {
  let [person, setPerson] = useState(newPerson);

  return (
    &amp;lt;&amp;gt;
      {/* to do: add form fields here */}
      &amp;lt;pre&amp;gt;{JSON.stringify(person, null, 2)}&amp;lt;/pre&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/pre&gt;&lt;p&gt;When the form is created, the person variable is initialized to a new person.&lt;/p&gt;&lt;p&gt;At the end of the form, we show the pretty-printed representation of the person, so that you can see that it indeed is updating the person properly.&lt;/p&gt;&lt;p&gt;Let’s add the fields for the first name and last name:&lt;/p&gt;&lt;pre&gt;&amp;lt;&amp;gt;
  &amp;lt;BareTextField
    top={person}
    setTop={setPerson}
    lens={firstNameLens}
  /&amp;gt;

  &amp;lt;BareTextField
    top={person}
    setTop={setPerson}
    lens={lastNameLens}
  /&amp;gt;

  &amp;lt;pre&amp;gt;{JSON.stringify(person, null, 2)}&amp;lt;/pre&amp;gt;
&amp;lt;/&amp;gt;&lt;/pre&gt;&lt;p&gt;We can reduce the amount of boilerplate by creating an object &lt;code&gt;f&lt;/code&gt; that contains &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;setTop()&lt;/code&gt;, so that we can pass it to the text fields succinctly:&lt;/p&gt;&lt;pre&gt;let PersonForm = () =&amp;gt; {
  let [person, setPerson] = useState(newPerson);
  &lt;mark&gt;let f = { top: person, setTop: setPerson };&lt;/mark&gt;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;BareTextField &lt;mark&gt;{...f}&lt;/mark&gt; lens={firstNameLens} /&amp;gt;
      &amp;lt;BareTextField &lt;mark&gt;{...f}&lt;/mark&gt; lens={lastNameLens} /&amp;gt;
      &amp;lt;pre&amp;gt;{JSON.stringify(person, null, 2)}&amp;lt;/pre&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/pre&gt;&lt;p&gt;With this approach, you can build forms with nested objects in a terse and type-safe way.&lt;/p&gt;&lt;h3 id="prettier-form-example" data-skip-toc="false" data-numbering="3.2"&gt;&lt;span&gt;&lt;span&gt;Prettier form example&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The text field we’ve created so far is nothing but a wrapper for an input element. We can build a more full-fledged text field by adding a label and some styling (I am partial to utility-first CSS):&lt;/p&gt;&lt;pre&gt;interface TextFieldProps&amp;lt;T&amp;gt; extends BareTextFieldProps&amp;lt;T&amp;gt; {
  label: string;
}

let TextField = &amp;lt;T extends any&amp;gt;({
  lens,
  top,
  setTop,
  label
}: TextFieldProps&amp;lt;T&amp;gt;) =&amp;gt; (
  &amp;lt;label className="block pb-6"&amp;gt;
    &amp;lt;div className="font-bold"&amp;gt;{label}&amp;lt;/div&amp;gt;
    &amp;lt;BareTextField
      lens={lens}
      top={top}
      setTop={setTop}
    /&amp;gt;
  &amp;lt;/label&amp;gt;
);&lt;/pre&gt;&lt;p&gt;Once we replace our &lt;code&gt;BareTextField&lt;/code&gt; usage in the form with &lt;code&gt;TextField&lt;/code&gt; (now with label), we get a nicer form:&lt;/p&gt;&lt;pre&gt;&amp;lt;&amp;gt;
  &amp;lt;TextField
    {...f}
    lens={firstNameLens}
    label="First name"
  /&amp;gt;
  &amp;lt;TextField
    {...f}
    lens={lastNameLens}
    label="Last name"
  /&amp;gt;
  &amp;lt;pre&amp;gt;{JSON.stringify(person, null, 2)}&amp;lt;/pre&amp;gt;
&amp;lt;/&amp;gt;&lt;/pre&gt;&lt;h2 id="handling-nested-data-by-composing-lenses" data-skip-toc="false" data-numbering="4"&gt;&lt;span&gt;&lt;span&gt;Handling nested data by composing lenses&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We are able to build forms for simple objects now, but not for nested objects. Let’s fix that.&lt;/p&gt;&lt;p&gt;Image a &lt;code&gt;Person&lt;/code&gt; type with an address inside it:&lt;/p&gt;&lt;pre&gt;type Address = {
  street: string;
  number: string;
};

type Person = {
  firstName: string;
  lastName: string;
  address: Address;
};&lt;/pre&gt;&lt;p&gt;Let’s take Sherlock as an example person:&lt;/p&gt;&lt;pre&gt;let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes",
  address: {
    street: "Butcher Street",
    number: "221b"
  }
}&lt;/pre&gt;&lt;p&gt;We can get Sherlock’s street easily:&lt;/p&gt;&lt;pre&gt;sherlock.address.street&lt;/pre&gt;&lt;p&gt;Updating Sherlock’s street is more cumbersome without lenses:&lt;/p&gt;&lt;pre&gt;let sherlock2 = {
  ...sherlock,
  address: {
    ...sherlock.address,
    street: &lt;mark&gt;"Baker Street"&lt;/mark&gt;
  }
}&lt;/pre&gt;&lt;p&gt;If &lt;code&gt;Address&lt;/code&gt; were a standalone type, we’d be able to update it succinctly with a lens:&lt;/p&gt;&lt;pre&gt;let sherlocksHome: Address = {
  street: "Butcher Street",
  number: "221b"
}

let streetLens =
  forProp&amp;lt;Address&amp;gt;()("street");

streetLens.set(sherlocksHome, "Baker Street");
  // -&amp;gt; { street: &lt;mark&gt;"Baker Street"&lt;/mark&gt;, number: "221b" }&lt;/pre&gt;&lt;p&gt;We can, however, create a lens for a person’s address, and for an address’ street, and &lt;em&gt;compose&lt;/em&gt; them, so that we get a lens for a person’s street:&lt;/p&gt;&lt;pre&gt;let addressLens =
  forProp&amp;lt;Person&amp;gt;()("address");

let streetLens =
  forProp&amp;lt;Address&amp;gt;()("street");

let addressStreetLens =
  &lt;mark&gt;compose&lt;/mark&gt;(addressLens, streetLens);&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;addressStreetLens&lt;/code&gt; lens “drills down” into the person type. It behaves like any other lens:&lt;/p&gt;&lt;pre&gt;let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes",
  address: {
    street: "Butcher Street",
    number: "221b"
  }
}

addressStreetLens.set(sherlock, "Baker Street");
  // -&amp;gt; {
  //      firstName: "Sherlock",
  //      lastName: "Holmes",
  //      address: {
  //        street: &lt;mark&gt;"Baker Street"&lt;/mark&gt;,
  //        number: "221b"
  //      }
  //    }&lt;/pre&gt;&lt;p&gt;This works for forms too, like any other lens. Let’s create the lenses we need first:&lt;/p&gt;&lt;pre&gt;let addressLens =
  forProp&amp;lt;Person&amp;gt;()("address")
let streetLens =
  forProp&amp;lt;Address&amp;gt;()("street");
let houseNumberLens =
  forProp&amp;lt;Address&amp;gt;()("number");
let addressStreetLens =
  compose(addressLens, streetLens);
let addressNumberLens =
  compose(addressLens, houseNumberLens);&lt;/pre&gt;&lt;p&gt;Now we can use them in a form:&lt;/p&gt;&lt;pre&gt;&amp;lt;&amp;gt;
  &amp;lt;TextField
    {...f}
    lens={firstNameLens}
    label="First name" /&amp;gt;

  &amp;lt;TextField
    {...f}
    lens={lastNameLens}
    label="Last name" /&amp;gt;

  &amp;lt;TextField
    {...f}
    &lt;mark&gt;lens={addressStreetLens}&lt;/mark&gt;
    label="Street" /&amp;gt;

  &amp;lt;TextField
    {...f}
    &lt;mark&gt;lens={addressNumberLens}&lt;/mark&gt;
    label="House number" /&amp;gt;

  &amp;lt;pre&amp;gt;{JSON.stringify(person, null, 2)}&amp;lt;/pre&amp;gt;
&amp;lt;/&amp;gt;&lt;/pre&gt;&lt;p&gt;With &lt;code&gt;compose()&lt;/code&gt;, you can build type-safe forms for deeply-nested data structures.&lt;/p&gt;&lt;p&gt;The implementation of &lt;code&gt;compose()&lt;/code&gt; looks complex, but it is worth looking at:&lt;/p&gt;&lt;pre&gt;let compose = &amp;lt;T, S, F&amp;gt;(
  ts: Lens&amp;lt;T, S&amp;gt;,
  sf: Lens&amp;lt;S, F&amp;gt;
): Lens&amp;lt;T, F&amp;gt; =&amp;gt; ({
  get: t =&amp;gt; sf.get(ts.get(t)),
  set: (t, f) =&amp;gt; ts.set(t, sf.set(ts.get(t), f))
});&lt;/pre&gt;&lt;p&gt;Pay attention to the type signature: given a &lt;code&gt;Lens&amp;lt;T, S&amp;gt;&lt;/code&gt; and a &lt;code&gt;Lens&amp;lt;S, F&amp;gt;&lt;/code&gt;, return a &lt;code&gt;Lens&amp;lt;T, F&amp;gt;&lt;/code&gt;. Once the type signature is in place, the implementation follows: the type system guides the implementation, and the type system will virtually guarantee correctness.&lt;/p&gt;&lt;h2 id="handling-lists" data-skip-toc="false" data-numbering="5"&gt;&lt;span&gt;&lt;span&gt;Handling lists&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We already saw how to create a lens for a property of an object, using &lt;code&gt;forProp()&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes"
};

firstNameLens.get(sherlock);
  // -&amp;gt; "Sherlock"

firstNameLens.set(sherlock, "Locky");
  // -&amp;gt; {
  //      firstName: "Locky",
  //      lastName: "Holmes"
  //    }&lt;/pre&gt;&lt;p&gt;While handling properties of an object is done with &lt;code&gt;forProp()&lt;/code&gt;, handling elements of a list can done with &lt;code&gt;forIndex()&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;let hobbies = ["programming", "arguing"];

let first = &lt;mark&gt;forIndex&lt;/mark&gt;&amp;lt;string&amp;gt;(0);

first.get(hobbies);
  // -&amp;gt; "programming"

first.set(hobbies, "coding");
  // -&amp;gt; ["coding", "arguing"]&lt;/pre&gt;&lt;p&gt;In practice, though, &lt;code&gt;forIndex()&lt;/code&gt; is not nearly as useful as &lt;code&gt;forProp()&lt;/code&gt;. The &lt;code&gt;forProp()&lt;/code&gt; function works well because we know the properties of an object ahead of time. For lists, on the other hand, the size is not known ahead of time, as lists can grow and shrink during execution.&lt;/p&gt;&lt;p&gt;To get a better understanding of how lists and lenses interact, let’s imagine a Person type with a list of hobbies:&lt;/p&gt;&lt;pre&gt;type Person = {
  firstName: string;
  lastName: string;
  &lt;mark&gt;hobbies: string[];&lt;/mark&gt;
};&lt;/pre&gt;&lt;p&gt;We can create a lens for the list of hobbies:&lt;/p&gt;&lt;pre&gt;let hobbiesLens: Lens&amp;lt;Person, string[]&amp;gt; =
  forProp&amp;lt;Person&amp;gt;()("hobbies");&lt;/pre&gt;&lt;p&gt;A lens that focuses on a &lt;code&gt;string[]&lt;/code&gt; is not directly useful, though. We’ll want to create one text field for each hobby, and a &lt;code&gt;TextField&lt;/code&gt; component needs a lens that focuses on a &lt;code&gt;string&lt;/code&gt;, not on a &lt;code&gt;string[]&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;To access individual list elements, we need lenses for each list element. Rather than a single lens that focuses on a list of hobbies, we need a collection of lenses, each focusing on a single hobby:&lt;/p&gt;&lt;pre&gt;let hobbyLenses: Lens&amp;lt;Person, string&amp;gt;[] =
  ??? // Implemented further down&lt;/pre&gt;&lt;p&gt;Note the distinction in the type signature: &lt;code&gt;hobbiesLens&lt;/code&gt; is one lens, while &lt;code&gt;hobbyLenses&lt;/code&gt; is an array of lenses. While &lt;code&gt;hobbiesLens&lt;/code&gt; focuses on a string array, &lt;code&gt;hobbyLenses&lt;/code&gt; each focus on a single string.&lt;/p&gt;&lt;p&gt;To transform &lt;code&gt;hobbiesLens&lt;/code&gt; into &lt;code&gt;hobbyLenses&lt;/code&gt;, we need to know the number of elements in the list, so that we can generate the appropriate number of lenses. This is where &lt;code&gt;makeArray()&lt;/code&gt; is useful:&lt;/p&gt;&lt;pre&gt;let hobbyLenses: Lens&amp;lt;Person, string&amp;gt;[] =
  makeArray(hobbiesLens, person.hobbies.length);&lt;/pre&gt;&lt;p&gt;We’ve left the implementation of &lt;code&gt;makeArray()&lt;/code&gt; out, but it has this signature, in case you want to give implementing it a shot yourself (or check out the code in the demo):&lt;/p&gt;&lt;pre&gt;(
  lens: Lens&amp;lt;T, F[]&amp;gt;,
  length: number
): Lens&amp;lt;T, F&amp;gt;[]&lt;/pre&gt;&lt;p&gt;Once we have our list of lenses, we can create a &lt;code&gt;TextField&lt;/code&gt; for each of these lenses:&lt;/p&gt;&lt;pre&gt;let hobbiesLens = forProp&amp;lt;Person&amp;gt;()("hobbies");
let PersonForm = () =&amp;gt; {
  let [person, setPerson] = useState(newPerson);
  let f = { top: person, setTop: setPerson };

  let &lt;mark&gt;hobbyLenses&lt;/mark&gt; =
    &lt;mark&gt;makeArray(hobbiesLens, person.hobbies.length)&lt;/mark&gt;;

  return (
    &amp;lt;&amp;gt;
      {&lt;mark&gt;hobbyLenses&lt;/mark&gt;.map(hobbyLens =&amp;gt;
        &amp;lt;TextField
          {...f}
          lens={hobbyLens}
          label="Hobby"
        /&amp;gt;
      )}

      &amp;lt;pre&amp;gt;{JSON.stringify(person, null, 2)}&amp;lt;/pre&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/pre&gt;&lt;p&gt;We now have a form where we can edit existing list elements, but not add or remove any yet.&lt;/p&gt;&lt;h3 id="adding-and-removing-list-elements" data-skip-toc="false" data-numbering="5.1"&gt;&lt;span&gt;&lt;span&gt;Adding and removing list elements&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;While the approach above works for modifying existing elements in a list, we need the ability to add new elements to a list and remove elements from a list.&lt;/p&gt;&lt;p&gt;To add an element to a list, we can use &lt;code&gt;push()&lt;/code&gt;, whose type signature is &lt;code&gt;(top: T, lens: Lens&amp;lt;T, F[]&amp;gt;, elem: F) =&amp;gt; T&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;let hobbiesLens =
  forProp&amp;lt;Person&amp;gt;()("hobbies");

let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes",
  hobbies: &lt;mark&gt;["deducing"]&lt;/mark&gt;
}

&lt;mark&gt;push(sherlock, hobbiesLens, "sleuthing")&lt;/mark&gt;
  // -&amp;gt; {
  //      firstName: "Sherlock",
  //      lastName: "Holmes",
  //      hobbies: &lt;mark&gt;["deducing", "sleuthing"]&lt;/mark&gt;
  //    }&lt;/pre&gt;&lt;p&gt;In a React form, you could use &lt;code&gt;push()&lt;/code&gt; as follows:&lt;/p&gt;&lt;pre&gt;&amp;lt;button
  onClick={() =&amp;gt;
    setPerson(&lt;mark&gt;push&lt;/mark&gt;(sherlock, hobbiesLens, ""))
  }
&amp;gt;New hobby&amp;lt;/button&amp;gt;&lt;/pre&gt;&lt;p&gt;The implementation of &lt;code&gt;push()&lt;/code&gt; relies on &lt;code&gt;over()&lt;/code&gt;, which applies a transformation over the value that the lens focuses on:&lt;/p&gt;&lt;pre&gt;let over = &amp;lt;T, F&amp;gt;(
  top: T,
  lens: Lens&amp;lt;T, F&amp;gt;,
  fn: (f: F) =&amp;gt; F
): T =&amp;gt;
  lens.set(top, fn(lens.get(top)));&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;over()&lt;/code&gt; function is sometimes called &lt;code&gt;transform()&lt;/code&gt; or &lt;code&gt;map()&lt;/code&gt;. I prefer the latter personally, because it really feels like mapping. Here’s an example which transform’s Sherlock’s name into UPPERCASE!!!, for no particular reason:&lt;/p&gt;&lt;pre&gt;let firstNameLens =
  forProp&amp;lt;Person&amp;gt;()("firstName");

let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes"
}

&lt;mark&gt;over&lt;/mark&gt;(
  person,
  firstNameLens,
  &lt;mark&gt;(a) =&amp;gt; a.toUpperCase()&lt;/mark&gt;
);
// -&amp;gt; {
//      firstName: &lt;mark&gt;"SHERLOCK"&lt;/mark&gt;,
//      lastName: "Holmes"
//    }&lt;/pre&gt;&lt;p&gt;Once we have &lt;code&gt;over()&lt;/code&gt;, we can implement &lt;code&gt;push()&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;let push = &amp;lt;T, F&amp;gt;(
  top: T,
  lens: Lens&amp;lt;T, F[]&amp;gt;,
  elem: F
): T =&amp;gt;
  over(top, lens, elems =&amp;gt; [...elems, elem]);&lt;/pre&gt;&lt;p&gt;Now that we have &lt;code&gt;push()&lt;/code&gt;, we can add new elements to a list. We are still lacking the ability to remove elements from a list, though. For this, there’s &lt;code&gt;removeAt()&lt;/code&gt;, which removes an element at a specified index:&lt;/p&gt;&lt;pre&gt;let hobbiesLens =
  forProp&amp;lt;Person&amp;gt;()("hobbies");

let sherlock: Person = {
  firstName: "Sherlock",
  lastName: "Holmes",
  hobbies: [
    "deducing",
    "sleuthing",
    "investigating",
  ]
}

&lt;mark&gt;removeAt(sherlock, hobbiesLens, 1)&lt;/mark&gt;
  // -&amp;gt; {
  //      firstName: "Sherlock",
  //      lastName: "Holmes",
  //      hobbies: &lt;mark&gt;["deducing", "investigating"]&lt;/mark&gt;
  //    }&lt;/pre&gt;&lt;p&gt;“Sleuthing” is a dated word. Good riddance, I say.&lt;/p&gt;&lt;p&gt;In a React form, you’d use &lt;code&gt;removeAt()&lt;/code&gt; like this:&lt;/p&gt;&lt;pre&gt;let hobbyLenses =
  makeArray(hobbiesLens, person.hobbies.length);

&amp;lt;&amp;gt;
  {hobbyLenses.map((hobbyLens, &lt;mark&gt;index&lt;/mark&gt;) =&amp;gt;
    &amp;lt;&amp;gt;
      &amp;lt;TextField
        {...f}
        lens={hobbyLens}
        label="Hobby"
      /&amp;gt;

      &amp;lt;button
        onClick={() =&amp;gt;
          setPerson(
            &lt;mark&gt;removeAt&lt;/mark&gt;(sherlock, hobbiesLens, &lt;mark&gt;index&lt;/mark&gt;)
          )
        }
      &amp;gt;Remove hobby&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )}
&amp;lt;/&amp;gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;removeAt()&lt;/code&gt; function can also implemented using &lt;code&gt;over()&lt;/code&gt;, with some appropriate use of &lt;code&gt;slice()&lt;/code&gt; to retain only the elements that are not to be removed:&lt;/p&gt;&lt;pre&gt;let removeAt =
  &amp;lt;T, F&amp;gt;(
    top: T,
    lens: Lens&amp;lt;T, F[]&amp;gt;,
    idx: number
  ): T =&amp;gt;
    over(
      top,
      lens,
      elems =&amp;gt; [
        ...elems.slice(0, idx),
        ...elems.slice(idx + 1)
      ]
    );&lt;/pre&gt;&lt;p&gt;With all this in place, we have a type-safe way of managing lists.&lt;/p&gt;&lt;h2 id="improving-on-single-select-dropdowns" data-skip-toc="false" data-numbering="6"&gt;&lt;span&gt;&lt;span&gt;Improving on single-select dropdowns&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;HTML provides the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element to create a dropdown list. Each element of this dropdown list, an &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt;, has a value which identifies the selected option:&lt;/p&gt;&lt;pre&gt;&amp;lt;select&amp;gt;
  &amp;lt;option&amp;gt;&amp;lt;/option&amp;gt;
  &amp;lt;option value="mari"&amp;gt;Marina&amp;lt;/option&amp;gt;
  &amp;lt;option value="star"&amp;gt;Stardust&amp;lt;/option&amp;gt;
  &amp;lt;option value="ruby"&amp;gt;Ruby&amp;lt;/option&amp;gt;
  &amp;lt;option value="sapp"&amp;gt;Sapphire&amp;lt;/option&amp;gt;
  &amp;lt;option value="elec"&amp;gt;Electric&amp;lt;/option&amp;gt;
  &amp;lt;option value="mint"&amp;gt;Mint&amp;lt;/option&amp;gt;
  &amp;lt;option value="slat"&amp;gt;Slate&amp;lt;/option&amp;gt;
&amp;lt;/select&amp;gt;&lt;/pre&gt;&lt;p&gt;If we wanted to give a &lt;code&gt;Person&lt;/code&gt; a favorite color, we could add a string property:&lt;/p&gt;&lt;pre&gt;type Person = {
  firstName: string;
  lastName: string;
  favoriteColor: string | null;
};&lt;/pre&gt;&lt;p&gt;We could then use lenses to create a &lt;code&gt;SingleSelectField&lt;/code&gt;, similar to our TextField. An implementation for this approach wouldn’t be too challenging.&lt;/p&gt;&lt;p&gt;There’s a limitation with this approach: the single-select dropdown values have to be strings. While this is common when building HTML single-select fields, we can do better, and treat dropdown values as full objects instead.&lt;/p&gt;&lt;p&gt;Imagine a &lt;code&gt;Color&lt;/code&gt; type, and a &lt;code&gt;Person&lt;/code&gt; whose &lt;code&gt;favoriteColor&lt;/code&gt; property is a reference to a &lt;code&gt;Color&lt;/code&gt; type:&lt;/p&gt;&lt;pre&gt;type Color = {
  id: string;
  name: string;
  hex: string;
};

type Person = {
  firstName: string;
  lastName: string;
  favoriteColor: Color | null;
};&lt;/pre&gt;&lt;p&gt;It’s beneficial to have a reference to &lt;code&gt;Color&lt;/code&gt; rather than a &lt;code&gt;string&lt;/code&gt;, because it allows us to encode additional information besides a value and a display label. For example, if a person has selected their favorite color, we can show a welcome message in their selected favorite color:&lt;/p&gt;&lt;pre&gt;&amp;lt;div style={{ color: person.favoriteColor?.hex || "#000" }}&amp;gt;
  Hello, {person.firstName}!
&amp;lt;/div&amp;gt;&lt;/pre&gt;&lt;p&gt;The options for our single-select dropdown will be objects, and so we’ll need to define that list of objects as an array:&lt;/p&gt;&lt;pre&gt;let allColors: Array&amp;lt;Color&amp;gt; = [
  { id: "mari", hex: "#0089a8", name: "Marina" },
  { id: "star", hex: "#e74132", name: "Stardust" },
  { id: "ruby", hex: "#bc1a50", name: "Ruby" },
  { id: "sapp", hex: "#45439d", name: "Sapphire" },
  { id: "elec", hex: "#c2d62e", name: "Electric" },
  { id: "mint", hex: "#29bc75", name: "Mint" },
  { id: "slat", hex: "#546173", name: "Slate" },
];&lt;/pre&gt;&lt;p&gt;I can’t decide whether my favorite color is &lt;span style="font-weight: bold; color: #e74132"&gt;Stardust&lt;/span&gt; (so warm and powerful!) or &lt;span style="font-weight: bold; color: #29bc75"&gt;Mint&lt;/span&gt; (so fresh and relaxing!).&lt;/p&gt;&lt;p&gt;We’ll also need a lens for a person’s favorite color:&lt;/p&gt;&lt;pre&gt;let favoriteColorLens: Lens&amp;lt;Person, Color&amp;gt; =
  forProp&amp;lt;Person&amp;gt;()("favoriteColor");&lt;/pre&gt;&lt;p&gt;We’ll create a &lt;code&gt;DropdownField&lt;/code&gt; React component later on, but for now, let’s look at how we’d use it:&lt;/p&gt;&lt;pre&gt;&amp;lt;DropdownField
  {...f}
  lens={favoriteColorLens}
  values={allColors}
  renderValue={m =&amp;gt; m.name}
/&amp;gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;DropdownField&lt;/code&gt; has the usual properties top and setTop (passed in using &lt;code&gt;{...f}&lt;/code&gt;), as well as lens, but there are two additional properties: the &lt;code&gt;values&lt;/code&gt; property is the list of all option objects, and the &lt;code&gt;renderValue&lt;/code&gt; property is a function that returns the display label:&lt;/p&gt;&lt;pre&gt;interface DropdownFieldProps&amp;lt;T, F&amp;gt; {
  lens: Lens&amp;lt;T, F | null&amp;gt;;

  top: T;
  setTop: (top: T) =&amp;gt; void;

  &lt;mark&gt;values&lt;/mark&gt;: Array&amp;lt;F&amp;gt;;
  &lt;mark&gt;renderValue&lt;/mark&gt;: (f: F) =&amp;gt; string;
}&lt;/pre&gt;&lt;p&gt;We’ll create a &lt;code&gt;DropdownField&lt;/code&gt; React component, which will wrap a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; HTML element. We will require value objects to have an id (hence the &lt;code&gt;F extends { id: string }&lt;/code&gt;):&lt;/p&gt;&lt;pre&gt;let DropdownField = &amp;lt;T, F extends { id: string }&amp;gt;(
  props: DropdownFieldProps&amp;lt;T, F&amp;gt;
) =&amp;gt; {&lt;/pre&gt;&lt;p&gt;This required id will be used as the &lt;code&gt;value&lt;/code&gt; property of an &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; later on.&lt;/p&gt;&lt;p&gt;We’ll use the lens to get the currently-selected option:&lt;/p&gt;&lt;pre&gt;  let value: F | null = props.lens.get(props.top);&lt;/pre&gt;&lt;p&gt;We’ll need a callback function, for use later on, that is triggered when the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; option changes. We can get the id of the currently-selected option, but we’ll have to loop through all options to find the one that matches:&lt;/p&gt;&lt;pre&gt;  let onChange =
    (ev: React.ChangeEvent&amp;lt;HTMLSelectElement&amp;gt;) =&amp;gt; {
      let id = ev.target.value;

      let value =
        props.values.find(v =&amp;gt; v.id === id)
        || null;

      props.setTop(
        props.lens.set(props.top, value)
      );
    };&lt;/pre&gt;&lt;p&gt;To render the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element, we loop over all values and generate &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; elements for each of them, and we set up the &lt;code&gt;value&lt;/code&gt; and &lt;code&gt;onChange&lt;/code&gt; properties of the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element:&lt;/p&gt;&lt;pre&gt;  return (
    &amp;lt;select
      value={value?.id}
      onChange={onChange}
    &amp;gt;
      &amp;lt;option value=""&amp;gt;&amp;lt;/option&amp;gt;
      {props.values.map(value =&amp;gt; (
        &amp;lt;option value={value.id} key={value.id}&amp;gt;
          {props.renderValue(value)}
        &amp;lt;/option&amp;gt;
      ))}
    &amp;lt;/select&amp;gt;
  );&lt;/pre&gt;&lt;p&gt;Note that the &lt;code&gt;DropdownField&lt;/code&gt; implementation has some assumptions built-in. There is always the empty option, and the value type is nullable (&lt;code&gt;F | null&lt;/code&gt;, rather than just &lt;code&gt;F&lt;/code&gt;). Additionally, each option object must have an id property. These assumptions might not hold in all situations.&lt;/p&gt;&lt;h3 id="dropdowns-versus-radio-buttons" data-skip-toc="false" data-numbering="6.1"&gt;&lt;span&gt;&lt;span&gt;Dropdowns versus radio buttons&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Let’s take a moment to think about UX. A &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element is a kind of single-select form field. Another kind of single-select form field is a set of radio buttons (&lt;code&gt;&amp;lt;input type="radio"&amp;gt;&lt;/code&gt;). In HTML, a select dropdown and a set of radio buttons is implemented quite differently, even though they serve a near-identical purpose: selecting one item from a collection.&lt;/p&gt;&lt;p&gt;We could define a &lt;code&gt;RadioButtonsField&lt;/code&gt; component, whose usage resembles &lt;code&gt;DropdownField&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&amp;lt;&lt;mark&gt;RadioButtonsField&lt;/mark&gt;
  {...f}
  lens={favoriteColorLens}
  values={allColors}
  renderValue={m =&amp;gt; m.name}
/&amp;gt;&lt;/pre&gt;&lt;p&gt;The type signature of a &lt;code&gt;RadioButtonsField&lt;/code&gt; is nearly identical to that of a &lt;code&gt;DropdownField&lt;/code&gt;. The only difference is the type of the &lt;code&gt;renderValue&lt;/code&gt; prop: for &lt;code&gt;DropdownField&lt;/code&gt;, &lt;code&gt;renderValue&lt;/code&gt; has to return a &lt;code&gt;string&lt;/code&gt;, while for &lt;code&gt;DropdownField&lt;/code&gt;, it can also return a &lt;code&gt;JSX.Element&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;This approach makes it trivial to swap a &lt;code&gt;DropdownField&lt;/code&gt; for a &lt;code&gt;RadioButtonsField&lt;/code&gt;. In plain-old HTML or React, this would not be the case, because dropdowns and radio buttons have wildly different implementations. Ideally, components with a similar purpose have a (near-)identical type signatures. This way, we can delay UX decisions around which kind of single-select field to use (dropdown or radio buttons), as we’ll be able to change one for the other trivially.&lt;/p&gt;&lt;h2 id="future-work" data-skip-toc="false" data-numbering="7"&gt;&lt;span&gt;&lt;span&gt;Future work&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;While lenses are an effective way of building forms, there are three areas where more research and development is needed to make lenses stand out as an approach to building React forms:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validation:&lt;/strong&gt; Lenses can be used in combination with common validation techniques, such as HTML’s built-in validation functionality and schema-based validation. However, these techniques don’t integrate neatly with lenses, and further research is needed to come with an approach where validation feels like a first-class concern.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Form helpers:&lt;/strong&gt; While lenses themselves are simple, using them effectively for forms requires implementations for all types of form fields, from simple text fields to different types of multi-select lists. The &lt;a href="https://github.com/denisdefreyne/lenses-demo"&gt;demo&lt;/a&gt; contains the minimal implementation of some types of form fields, which ideally would grow and be properly packaged as an open-source package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; The performance of this particular implementation of lenses, and implementations of the form fields, has not been a cause for concern so far. Still, it is likely that situations will arise where the performance of lenses is just not adequate. More work needs to be done to ensure that lenses are an appropriate solution, even for unusually large and complex forms.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;h2 id="closing-thoughts" data-skip-toc="false" data-numbering="8"&gt;&lt;span&gt;&lt;span&gt;Closing thoughts&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;To me, lenses have proven their worth, and will have a prominent place in my toolbox for building forms. No other approach to building forms gives me the same development speed or gives me as much confidence.&lt;/p&gt;&lt;p&gt;With lenses, I can be confident that the forms I build work, with only a minimum of testing. The real-time IDE feedback and autocompletion means I can work faster, without compromising quality.&lt;/p&gt;&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol class="stack-3"&gt;
&lt;li id="fn:1" role="doc-endnote"&gt;&lt;p&gt;The approach outlined in this article will work with JavaScript as well, but you’ll not get the type-safety benefits. &lt;a href="#fnref:1" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn:2" role="doc-endnote"&gt;&lt;p&gt;I make typign mistakes all the time, and have trouble detecting them — perhaps I’m mildly dyslexic? &lt;a href="#fnref:2" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn:3" role="doc-endnote"&gt;&lt;p&gt;The double function invocation is necessary! When creating a lens using &lt;code&gt;forProp&lt;/code&gt;, we need to specify the top type. If &lt;code&gt;forProp&lt;/code&gt; only had a single function invocation, we’d have to specify both the top type &lt;em&gt;and&lt;/em&gt; the focus type, because Type­Script does not (yet) support partial type inference. We’re working around this limitation by having two function invocations. See &lt;a href="https://github.com/microsoft/TypeScript/issues/26242"&gt;Type­Script issue #26242&lt;/a&gt; for details. &lt;a href="#fnref:3" class="reversefootnote" role="doc-backlink"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <summary type="html">Lenses enable building complex forms quickly and with confidence. Lenses themselves are simple, but they provide significant benefits when used with React forms with TypeScript, reducing the change of mistakes and speeding up the feedback loop.</summary>
  </entry>
</feed>

