<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Andy Croll]]></title>
  <link href="https://andycroll.com/index.xml" rel="self"/>
  <link href="https://andycroll.com/"/>
  <updated>2026-04-23T00:47:36+00:00</updated>
  <id>https://andycroll.com/</id>
  <author>
    <name><![CDATA[Andy Croll]]></name>
    <email><![CDATA[andy@goodscary.com]]></email>
  </author>
  <generator uri="http://jekyllrb.com/">Jekyll</generator>

  
  <entry>
    <title type="html"><![CDATA[Use Rails Combined Credentials]]></title>
    <link href="https://andycroll.com/ruby/use-rails-combined-credentials/"/>
    <updated>2026-04-13T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/use-rails-combined-credentials</id>
    <content type="html"><![CDATA[<p>To deal with secrets and credential handling most Rails apps have ended up with a hotchpotch of <code class="language-plaintext highlighter-rouge">ENV.fetch</code> calls and <code class="language-plaintext highlighter-rouge">credentials.dig</code> lookups throughout the codebase, depending on where each secret lives.</p>

<p>Rails edge — and the upcoming 8.2 — fixes this.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…mixing ENV and credential lookups:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeChargeService</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@api_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"STRIPE_API_KEY"</span><span class="p">)</span>
    <span class="vi">@webhook_secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:webhook_secret</span><span class="p">)</span>
    <span class="vi">@price_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"STRIPE_PRICE_ID"</span><span class="p">)</span> <span class="p">{</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:price_id</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the combined credentials API:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeChargeService</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@api_key</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:api_key</span><span class="p">)</span>
    <span class="vi">@webhook_secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:webhook_secret</span><span class="p">)</span>
    <span class="vi">@price_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:price_id</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">require</code> raises a <code class="language-plaintext highlighter-rouge">KeyError</code> if the key is missing from all backends. For optional values, use <code class="language-plaintext highlighter-rouge">option</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">option</span><span class="p">(</span><span class="ss">:appsignal</span><span class="p">,</span> <span class="ss">:push_api_key</span><span class="p">,</span> <span class="ss">default: </span><span class="kp">nil</span><span class="p">)</span>
<span class="c1"># Returns nil if missing — AppSignal just won't report</span>
</code></pre></div></div>

<p>To keep production secrets separate, run <code class="language-plaintext highlighter-rouge">bin/rails credentials:edit --environment production</code>. This creates a separate encrypted file with its own key:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config/credentials.yml.enc         ← shared (dev/test)
config/master.key                  ← decrypts the shared file

config/credentials/production.yml.enc  ← production only
config/credentials/production.key      ← decrypts production
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">production.yml.enc</code> exists, Rails uses it exclusively in production — there’s no inheritance from the shared file, so duplicate any keys you need. To decrypt in production, set <code class="language-plaintext highlighter-rouge">RAILS_MASTER_KEY</code> in your hosting provider to the contents of <code class="language-plaintext highlighter-rouge">production.key</code>.</p>

<h2 id="why">Why?</h2>

<p><code class="language-plaintext highlighter-rouge">Rails.app.creds</code> checks ENV first, then falls back to encrypted credentials. You don’t need to know or care where a value is stored.</p>

<p>Nested keys like <code class="language-plaintext highlighter-rouge">:stripe, :api_key</code> map to double-underscored ENV names (<code class="language-plaintext highlighter-rouge">STRIPE__API_KEY</code>). A single key like <code class="language-plaintext highlighter-rouge">:postmark_api_token</code> checks <code class="language-plaintext highlighter-rouge">ENV["POSTMARK_API_TOKEN"]</code>.</p>

<p>This means you can move secrets between ENV and encrypted credentials without changing application code. Deploying to a provider that injects secrets via ENV? It just works. Want to move a key into the encrypted file instead? Remove the ENV variable and add it to your credentials. Your code stays the same.</p>

<p>I’ve <a href="/ruby/wrap-your-environment-variables-in-a-settings-object/">previously recommended</a> wrapping ENV in a custom Settings object. This built-in approach is better — the same clean interface with the added fallback to encrypted credentials.</p>

<h2 id="why-not">Why not?</h2>

<p>This isn’t in a released version of Rails yet — you need Rails edge (<code class="language-plaintext highlighter-rouge">main</code>), and it’s expected in Rails 8.2. If you’re on 8.1 or older, a <a href="/ruby/wrap-your-environment-variables-in-a-settings-object/">custom Settings wrapper</a> still works well.</p>

<h3 id="other-considerations">Other Considerations</h3>

<p>You can also create <code class="language-plaintext highlighter-rouge">development.yml.enc</code> and <code class="language-plaintext highlighter-rouge">test.yml.enc</code>, but I think the shared file plus a production override is clearer — and you shouldn’t be calling real APIs in your test environment anyhow.</p>

<p>Keep separate encryption keys for each environment. You could share one, but a leaked development key shouldn’t expose production secrets.</p>

<h2 id="mea-culpa">Mea Culpa</h2>

<p>I originally published this post saying <code class="language-plaintext highlighter-rouge">Rails.app.creds</code> had shipped in Rails 8.1. It hasn’t — it’s on Rails <code class="language-plaintext highlighter-rouge">main</code> and is expected in 8.2. I’ve been running Rails edge on a couple of projects and assumed this had already been released. Apologies for the confusion, and thanks to everyone who pointed it out.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Teach Rails Irregular Plurals with Inflections]]></title>
    <link href="https://andycroll.com/ruby/teach-rails-irregular-plurals-with-inflections/"/>
    <updated>2026-03-30T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/teach-rails-irregular-plurals-with-inflections</id>
    <content type="html"><![CDATA[<p>English has plenty of irregular plurals. Criterion becomes criteria, not criterions. Rails handles many common ones already, but your domain might include words it doesn’t know about.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…accepting Rails’s best guess at a plural:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"criterion"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "criterions"</span>
<span class="s2">"matrix"</span><span class="p">.</span><span class="nf">pluralize</span>     <span class="c1">#=&gt; "matrices"  # this one Rails knows!</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.irregular</code> to teach Rails the correct pair:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"criterion"</span><span class="p">,</span> <span class="s2">"criteria"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"criterion"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "criteria"</span>
<span class="s2">"criteria"</span><span class="p">.</span><span class="nf">singularize</span> <span class="c1">#=&gt; "criterion"</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>Give <code class="language-plaintext highlighter-rouge">irregular</code> the singular and plural forms and Rails handles both directions—<code class="language-plaintext highlighter-rouge">pluralize</code> and <code class="language-plaintext highlighter-rouge">singularize</code> both work correctly.</p>

<p>A <code class="language-plaintext highlighter-rouge">Criterion</code> model will look for a <code class="language-plaintext highlighter-rouge">criteria</code> table. <code class="language-plaintext highlighter-rouge">resources :criteria</code> will route to <code class="language-plaintext highlighter-rouge">CriteriaController</code>. Association names, fixtures, and factory names all follow suit.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Criterion</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># table: criteria</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Survey</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_many</span> <span class="ss">:criteria</span>  <span class="c1"># works as expected</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can declare as many as you need:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"criterion"</span><span class="p">,</span> <span class="s2">"criteria"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"goose"</span><span class="p">,</span> <span class="s2">"geese"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Although, unless you’re building some kind of flighted animal tracker, you probably won’t need that second one.</p>

<p>Rails already knows a handful of irregular plurals: person/people, man/men, child/children, sex/sexes, move/moves, and—crucially—zombie/zombies are built in. Rails’s pluralisation rules are regex-based, so the <code class="language-plaintext highlighter-rouge">(m)an → (m)en</code> pattern also covers woman/women. But that’s it—words like tooth/teeth, foot/feet, mouse/mice, and goose/geese are not handled by default. Check the <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">default inflections</a> to see what’s already covered.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-irregular">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>Before adding an irregular inflection, check whether Rails already knows the word. Try it in a console:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"person"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "people"  — already works</span>
<span class="s2">"axis"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "axes"    — already works</span>
</code></pre></div></div>

<p>If it’s already correct, adding it to your initialiser is just noise.</p>

<p>If the word never appears as a model or resource name, there’s no reason to declare it.</p>

<p>For words that don’t change between singular and plural (like “sheep” or “metadata”), you need <code class="language-plaintext highlighter-rouge">inflect.uncountable</code>. For casing issues with acronyms like API or CSV, look at <code class="language-plaintext highlighter-rouge">inflect.acronym</code>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Handle Uncountable Words in Rails Inflections]]></title>
    <link href="https://andycroll.com/ruby/handle-uncountable-words-in-rails-inflections/"/>
    <updated>2026-03-23T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/handle-uncountable-words-in-rails-inflections</id>
    <content type="html"><![CDATA[<p>Some English words don’t have a separate plural form. “Staff” is staff, “metadata” is metadata, “feedback” is feedback. Rails doesn’t always know this—it will happily generate a <code class="language-plaintext highlighter-rouge">staffs</code> table or a <code class="language-plaintext highlighter-rouge">metadatas</code> route if you let it.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…fighting Rails when it pluralises words that shouldn’t change:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"staff"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "staffs"</span>
<span class="s2">"metadata"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "metadatas"</span>
<span class="s2">"feedback"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "feedbacks"</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.uncountable</code> to tell Rails these words stay the same:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">uncountable</span> <span class="sx">%w[staff metadata feedback]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"staff"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "staff"</span>
<span class="s2">"metadata"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "metadata"</span>
<span class="s2">"staff"</span><span class="p">.</span><span class="nf">singularize</span>  <span class="c1">#=&gt; "staff"</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>Table names, route helpers, association names, and autoloading all depend on correct inflection. When Rails gets it wrong, you end up with a <code class="language-plaintext highlighter-rouge">staffs</code> table or <code class="language-plaintext highlighter-rouge">metadatas_path</code> route helpers.</p>

<p>Declaring a word as uncountable fixes this everywhere at once. The <code class="language-plaintext highlighter-rouge">Staff</code> model maps to the <code class="language-plaintext highlighter-rouge">staff</code> table. <code class="language-plaintext highlighter-rouge">resources :staff</code> generates the routes you’d expect.</p>

<p>Words worth declaring uncountable: <code class="language-plaintext highlighter-rouge">staff</code>, <code class="language-plaintext highlighter-rouge">metadata</code>, <code class="language-plaintext highlighter-rouge">feedback</code>, <code class="language-plaintext highlighter-rouge">analytics</code>, <code class="language-plaintext highlighter-rouge">aircraft</code>, <code class="language-plaintext highlighter-rouge">software</code>. You only need to add ones you’re actually using as model or resource names. You can pass a single string or an array.</p>

<p>Rails already handles some common uncountable words—<code class="language-plaintext highlighter-rouge">equipment</code>, <code class="language-plaintext highlighter-rouge">information</code>, <code class="language-plaintext highlighter-rouge">rice</code>, <code class="language-plaintext highlighter-rouge">money</code>, <code class="language-plaintext highlighter-rouge">species</code>, <code class="language-plaintext highlighter-rouge">series</code>, <code class="language-plaintext highlighter-rouge">fish</code>, <code class="language-plaintext highlighter-rouge">sheep</code>, <code class="language-plaintext highlighter-rouge">jeans</code>, and <code class="language-plaintext highlighter-rouge">police</code> work out of the box. Check the <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">default inflections</a> to see the full list before adding your own.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-uncountable">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>Uncountable words make associations slightly less intuitive. <code class="language-plaintext highlighter-rouge">has_many :staff</code> reads naturally, but <code class="language-plaintext highlighter-rouge">Staff.all</code> returning multiple records from a <code class="language-plaintext highlighter-rouge">staff</code> table can briefly confuse developers expecting a <code class="language-plaintext highlighter-rouge">staffs</code> table.</p>

<p>If the word is domain-specific jargon your team invented, a regular plural might actually be clearer. Reserve <code class="language-plaintext highlighter-rouge">uncountable</code> for genuinely uncountable English words, not as a shortcut to avoid a table name you don’t like.</p>

<p>This only affects pluralisation. For casing issues with acronyms like API or CSV, that’s <code class="language-plaintext highlighter-rouge">inflect.acronym</code>. For words with non-standard plurals like criterion/criteria, that’s <code class="language-plaintext highlighter-rouge">inflect.irregular</code>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Declare Acronyms in Rails Inflections]]></title>
    <link href="https://andycroll.com/ruby/declare-acronyms-in-rails-inflections/"/>
    <updated>2026-03-16T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/declare-acronyms-in-rails-inflections</id>
    <content type="html"><![CDATA[<p>A lot of Rails’s naming magic comes from its clever use of inflections. <code class="language-plaintext highlighter-rouge">user.rb</code> defines the <code class="language-plaintext highlighter-rouge">User</code> class, backed by the <code class="language-plaintext highlighter-rouge">users</code> table, managed by <code class="language-plaintext highlighter-rouge">UsersController</code>, accessible at the <code class="language-plaintext highlighter-rouge">/users/</code> routes.</p>

<p>Every Rails app generates <code class="language-plaintext highlighter-rouge">config/initializers/inflections.rb</code> to let you customise this behaviour. Most developers leave it empty. Then one day you namespace a controller under <code class="language-plaintext highlighter-rouge">API</code> and Rails starts generating <code class="language-plaintext highlighter-rouge">Api::UsersController</code> instead of <code class="language-plaintext highlighter-rouge">API::UsersController</code>.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…accepting the wrong casing in your class names:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/users_controller.rb</span>
<span class="k">class</span> <span class="nc">Api::UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.acronym</code> to teach Rails the correct casing:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"API"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now Rails expects <code class="language-plaintext highlighter-rouge">API::UsersController</code>. The file path stays lowercase (<code class="language-plaintext highlighter-rouge">app/controllers/api/</code>), but the class name uses the acronym:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/users_controller.rb</span>
<span class="k">class</span> <span class="nc">API::UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>The <code class="language-plaintext highlighter-rouge">acronym</code> method tells ActiveSupport’s inflector to preserve the casing you specify. It affects <code class="language-plaintext highlighter-rouge">camelize</code>, <code class="language-plaintext highlighter-rouge">underscore</code>, <code class="language-plaintext highlighter-rouge">classify</code>, and <code class="language-plaintext highlighter-rouge">titleize</code>—which means it also affects autoloading and URL helpers.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"api"</span><span class="p">.</span><span class="nf">camelize</span>        <span class="c1">#=&gt; "API"</span>
<span class="s2">"API"</span><span class="p">.</span><span class="nf">underscore</span>      <span class="c1">#=&gt; "api"</span>
<span class="s2">"api/users"</span><span class="p">.</span><span class="nf">camelize</span>  <span class="c1">#=&gt; "API::Users"</span>
</code></pre></div></div>

<p>Without the acronym declaration, you get <code class="language-plaintext highlighter-rouge">Api</code> instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"api"</span><span class="p">.</span><span class="nf">camelize</span>  <span class="c1">#=&gt; "Api"</span>
</code></pre></div></div>

<p>Unlike irregular plurals and uncountable words, Rails ships with <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">no built-in acronyms</a>—every one you need, you have to declare yourself. Common ones worth adding: <code class="language-plaintext highlighter-rouge">API</code>, <code class="language-plaintext highlighter-rouge">SMS</code>, <code class="language-plaintext highlighter-rouge">CSV</code>, <code class="language-plaintext highlighter-rouge">HTML</code>, <code class="language-plaintext highlighter-rouge">PDF</code>. You need one call per term:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"API"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"SMS"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">acronym</span> <span class="s2">"CSV"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This also works for mixed-case words like <code class="language-plaintext highlighter-rouge">GraphQL</code> or <code class="language-plaintext highlighter-rouge">GitHub</code>. <code class="language-plaintext highlighter-rouge">inflect.acronym "GraphQL"</code> ensures <code class="language-plaintext highlighter-rouge">"graphql".camelize</code> returns <code class="language-plaintext highlighter-rouge">"GraphQL"</code> rather than <code class="language-plaintext highlighter-rouge">"Graphql"</code>.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-acronym">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<p>Note that because these changes are in an initializer, you’ll need to restart your Rails server after making changes.</p>

<h2 id="why-not">Why not?</h2>

<p>Keep the list short. Every entry changes how <code class="language-plaintext highlighter-rouge">camelize</code>, <code class="language-plaintext highlighter-rouge">titleize</code>, <code class="language-plaintext highlighter-rouge">humanize</code>, and <code class="language-plaintext highlighter-rouge">underscore</code> behave for the specified words across your entire app. Only add acronyms you’re actively using—whether in class names, attribute labels, or view helpers.</p>

<p>This only affects casing, not pluralisation. For words with non-standard plurals like criterion/criteria, you’ll want <code class="language-plaintext highlighter-rouge">inflect.irregular</code>. For words that don’t pluralise at all, the method to look at is <code class="language-plaintext highlighter-rouge">inflect.uncountable</code>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Group Repeated Options with with_options]]></title>
    <link href="https://andycroll.com/ruby/with-options-group-shared-config/"/>
    <updated>2026-03-09T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/with-options-group-shared-config</id>
    <content type="html"><![CDATA[<p>When multiple validations share the same <code class="language-plaintext highlighter-rouge">if:</code> condition, or multiple callbacks share the same <code class="language-plaintext highlighter-rouge">only:</code> constraint, you end up repeating yourself. <code class="language-plaintext highlighter-rouge">with_options</code> groups them together.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…repeating conditions across multiple validations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Article</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">validates</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">if: :published?</span>
  <span class="n">validates</span> <span class="ss">:body</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">100</span> <span class="p">},</span> <span class="ss">if: :published?</span>
  <span class="n">validates</span> <span class="ss">:author</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">if: :published?</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the <code class="language-plaintext highlighter-rouge">with_options</code> method to group common configurations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Article</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">with_options</span> <span class="ss">if: :published?</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
    <span class="n">validates</span> <span class="ss">:body</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">100</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:author</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Works well in controllers too:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AdminController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">with_options</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span> <span class="k">do</span>
    <span class="n">before_action</span> <span class="ss">:require_admin</span>
    <span class="n">before_action</span> <span class="ss">:set_audit_trail</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And associations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">with_options</span> <span class="ss">dependent: :destroy</span> <span class="k">do</span>
    <span class="n">has_many</span> <span class="ss">:posts</span>
    <span class="n">has_many</span> <span class="ss">:comments</span>
    <span class="n">has_many</span> <span class="ss">:likes</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p><code class="language-plaintext highlighter-rouge">with_options</code> merges the given options into every method call inside the block. In the validation example, <code class="language-plaintext highlighter-rouge">if: :published?</code> gets added to each <code class="language-plaintext highlighter-rouge">validates</code> call automatically.</p>

<p>This syntax groups related configuration visually—it’s immediately clear these rules only apply to published articles. If you have to change the condition; it is only once, not three times.</p>

<p>This also works in <code class="language-plaintext highlighter-rouge">routes.rb</code> too:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">with_options</span> <span class="ss">controller: </span><span class="s2">"admin/reports"</span> <span class="k">do</span>
  <span class="n">get</span> <span class="s2">"daily"</span><span class="p">,</span> <span class="ss">action: :daily</span>
  <span class="n">get</span> <span class="s2">"weekly"</span><span class="p">,</span> <span class="ss">action: :weekly</span>
  <span class="n">get</span> <span class="s2">"monthly"</span><span class="p">,</span> <span class="ss">action: :monthly</span>
<span class="k">end</span>
</code></pre></div></div>

<p>See the <a href="https://api.rubyonrails.org/classes/Object.html#method-i-with_options">with_options documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>With only two items, the block adds more lines than it saves. Three or more is where <code class="language-plaintext highlighter-rouge">with_options</code> starts to pay off.</p>

<p>Nesting multiple <code class="language-plaintext highlighter-rouge">with_options</code> blocks gets hard to follow. If you find yourself nesting, reconsider.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Customize Model URLs with to_param]]></title>
    <link href="https://andycroll.com/ruby/to-param-seo-friendly-urls/"/>
    <updated>2026-03-02T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/to-param-seo-friendly-urls</id>
    <content type="html"><![CDATA[<p>Rails models default to using their ID in URLs: <code class="language-plaintext highlighter-rouge">/articles/42</code>. The <code class="language-plaintext highlighter-rouge">to_param</code> method lets you customize this—use a slug, hide the ID, or combine both for readable URLs. Exposing IDs isn’t dangerous if you scope access properly, but you might want cleaner paths.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…exposing IDs in your URLs:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">redirect_to</span> <span class="n">article_path</span><span class="p">(</span><span class="vi">@article</span><span class="p">)</span>
<span class="c1"># =&gt; "/articles/42"</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the <code class="language-plaintext highlighter-rouge">to_param</code> method to return a slug instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Article</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">before_save</span> <span class="p">{</span> <span class="nb">self</span><span class="p">.</span><span class="nf">slug</span> <span class="o">=</span> <span class="n">title</span><span class="p">.</span><span class="nf">parameterize</span> <span class="p">}</span>

  <span class="k">def</span> <span class="nf">to_param</span> <span class="o">=</span> <span class="n">slug</span>
<span class="k">end</span>

<span class="n">redirect_to</span> <span class="n">article_path</span><span class="p">(</span><span class="vi">@article</span><span class="p">)</span>
<span class="c1"># =&gt; "/articles/understanding-rails-extensions"</span>
</code></pre></div></div>

<p>Then find records by slug in your controller:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">set_article</span>
  <span class="vi">@article</span> <span class="o">=</span> <span class="no">Article</span><span class="p">.</span><span class="nf">find_by!</span><span class="p">(</span><span class="ss">slug: </span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
</code></pre></div></div>

<p>For reliable lookups with readable URLs, prefix the slug with the ID:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">to_param</span>
  <span class="s2">"</span><span class="si">#{</span><span class="nb">id</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">title</span><span class="p">.</span><span class="nf">parameterize</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>

<span class="c1"># =&gt; "/articles/42-understanding-rails-extensions"</span>
</code></pre></div></div>

<p>The ID lookup just works—<code class="language-plaintext highlighter-rouge">"42-understanding-rails-extensions".to_i</code> returns <code class="language-plaintext highlighter-rouge">42</code>, so <code class="language-plaintext highlighter-rouge">Article.find(params[:id])</code> needs no parsing.</p>

<h2 id="why">Why?</h2>

<p>Rails URL helpers call <code class="language-plaintext highlighter-rouge">to_param</code> automatically when generating URLs. Override it to create cleaner, more descriptive paths without changing controller code.</p>

<p>Descriptive URLs are easier to read in logs, analytics, and when shared. They give users context about content before clicking. For content-heavy sites, slug-based URLs can help with SEO.</p>

<p>Hiding IDs also keeps URLs stable if you ever migrate databases or change ID schemes.</p>

<h2 id="why-not">Why not?</h2>

<p>Pure slug URLs require uniqueness on the relevant column, an index in the database, and method changes everywhere you <code class="language-plaintext highlighter-rouge">find</code> the model. If slugs can change, you’ll need redirects for old URLs.</p>

<p>For production apps, consider <a href="https://github.com/norman/friendly_id">friendly_id</a>—it handles slug generation, duplicates, history tracking, and scoped slugs. Or try <a href="https://github.com/excid3/prefixed_ids">prefixed_ids</a> for Stripe-style encoded IDs like <code class="language-plaintext highlighter-rouge">user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe</code> that hide sequential IDs without needing slugs.</p>

<p>The ID-prefixed approach avoids uniqueness issues but still exposes the ID. Hiding IDs entirely means extra complexity in exchange for security through obscurity—a trade-off worth considering carefully.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Use StringInquirer for Readable Predicate Methods]]></title>
    <link href="https://andycroll.com/ruby/use-stringinquirer-for-readable-predicate-methods/"/>
    <updated>2026-02-10T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/use-stringinquirer-for-readable-predicate-methods</id>
    <content type="html"><![CDATA[<p>You’ve probably seen <code class="language-plaintext highlighter-rouge">Rails.env.production?</code> in your codebase to ensure that certain code only runs in production. Instead of having to compare strings, <code class="language-plaintext highlighter-rouge">Rails.env == "production"</code>, Rails wraps the string in an <a href="https://api.rubyonrails.org/classes/ActiveSupport/StringInquirer.html"><code class="language-plaintext highlighter-rouge">ActiveSupport::StringInquirer</code></a> so you get readable methods like <code class="language-plaintext highlighter-rouge">.production?</code> and <code class="language-plaintext highlighter-rouge">.development?</code>.</p>

<p>Active Support also adds an <a href="https://api.rubyonrails.org/classes/String.html#method-i-inquiry"><code class="language-plaintext highlighter-rouge">inquiry</code> method to <code class="language-plaintext highlighter-rouge">String</code></a> so you can use this same pattern in your own code.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…comparing strings with <code class="language-plaintext highlighter-rouge">==</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Writing</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># category is a string: "story", "nursery_rhyme", "song", "article", "social_post"</span>

  <span class="k">def</span> <span class="nf">online?</span>
    <span class="n">category</span> <span class="o">==</span> <span class="s2">"article"</span> <span class="o">||</span> <span class="n">category</span> <span class="o">==</span> <span class="s2">"social_post"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the <code class="language-plaintext highlighter-rouge">inquiry</code> method to ask questions of your string attributes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Writing</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nf">category</span> <span class="o">=</span> <span class="k">super</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">inquiry</span>

  <span class="k">def</span> <span class="nf">online?</span> <span class="o">=</span> <span class="n">category</span><span class="p">.</span><span class="nf">article?</span> <span class="o">||</span> <span class="n">category</span><span class="p">.</span><span class="nf">social_post?</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The overridden <code class="language-plaintext highlighter-rouge">category</code> getter calls <code class="language-plaintext highlighter-rouge">super</code> to get the original value, then wraps it in a <code class="language-plaintext highlighter-rouge">StringInquirer</code> via <code class="language-plaintext highlighter-rouge">.to_s.inquiry</code>. Now calling <code class="language-plaintext highlighter-rouge">.article?</code> checks whether the string equals <code class="language-plaintext highlighter-rouge">"article"</code>.</p>

<h2 id="why">Why?</h2>

<p>The code reads like English. “Is the category an article?” becomes <code class="language-plaintext highlighter-rouge">category.article?</code> rather than <code class="language-plaintext highlighter-rouge">category == "article"</code>.</p>

<p>This is the same pattern Rails uses internally. When you write <code class="language-plaintext highlighter-rouge">Rails.env.production?</code>, you’re calling a predicate method on a <code class="language-plaintext highlighter-rouge">StringInquirer</code>. Applying the pattern to your own code feels natural.</p>

<p>It works well with dynamic data such as API responses or CSV imports where defining an enum upfront isn’t practical.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">status</span> <span class="o">=</span> <span class="n">api_response</span><span class="p">[</span><span class="s2">"status"</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">inquiry</span>
<span class="n">status</span><span class="p">.</span><span class="nf">pending?</span>   <span class="c1">#=&gt; true if status == "pending"</span>
<span class="n">status</span><span class="p">.</span><span class="nf">complete?</span>  <span class="c1">#=&gt; true if status == "complete"</span>
</code></pre></div></div>

<h2 id="why-not">Why not?</h2>

<p>If your values are a known, fixed set, prefer Rails enums:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Writing</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">enum</span> <span class="ss">:category</span><span class="p">,</span> <span class="sx">%w[story nursery_rhyme song article social_post]</span><span class="p">.</span><span class="nf">index_by</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:itself</span><span class="p">)</span>
<span class="k">end</span>

<span class="n">writing</span><span class="p">.</span><span class="nf">article?</span>       <span class="c1"># same predicate methods</span>
<span class="no">Writing</span><span class="p">.</span><span class="nf">social_post</span>    <span class="c1"># scopes for free</span>
<span class="n">writing</span><span class="p">.</span><span class="nf">song!</span>          <span class="c1"># and bang assignment methods</span>
</code></pre></div></div>

<p>Enums give you database-backed validation and automatic scopes. Use <code class="language-plaintext highlighter-rouge">StringInquirer</code> when the values are too dynamic for an enum, or when you’re working with external data you don’t control.</p>

<h2 id="extra-nuances">Extra nuances</h2>

<p>You might sometimes want a bare attribute call to return <code class="language-plaintext highlighter-rouge">nil</code>. With a <code class="language-plaintext highlighter-rouge">StringInquirer</code> it will always be <code class="language-plaintext highlighter-rouge">""</code>. Either adjust checks to look for <code class="language-plaintext highlighter-rouge">.blank?</code> rather than <code class="language-plaintext highlighter-rouge">.nil?</code> or modify the implementation using the <a href="/ruby/rails-try-vs-safe-lonely-navigation-operator-ampersand-dot">lonely operator</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">category</span> <span class="o">=</span> <span class="k">super</span><span class="p">.</span><span class="nf">presence</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">inquiry</span>
</code></pre></div></div>

<p>However, this requires safe navigation (<code class="language-plaintext highlighter-rouge">&amp;.</code>) when calling predicates, <code class="language-plaintext highlighter-rouge">category&amp;.news?</code>, so you’re exchanging more database-accurate nilness for ugly calls.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Prefer in? Over include? for Readable Conditions]]></title>
    <link href="https://andycroll.com/ruby/prefer-in-over-include-for-readable-conditions/"/>
    <updated>2026-02-02T02:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/prefer-in-over-include-for-readable-conditions</id>
    <content type="html"><![CDATA[<p>When checking if a value exists within a collection, Ruby’s <code class="language-plaintext highlighter-rouge">include?</code> method does the job, but Rails provides a more natural alternative through Active Support’s <a href="https://api.rubyonrails.org/classes/Object.html#method-i-in-3F"><code class="language-plaintext highlighter-rouge">in?</code> method</a>.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…reading your conditions backwards with <code class="language-plaintext highlighter-rouge">include?</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nsync</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Justin"</span><span class="p">,</span> <span class="s2">"JC"</span><span class="p">,</span> <span class="s2">"Chris"</span><span class="p">,</span> <span class="s2">"Joey"</span><span class="p">,</span> <span class="s2">"Lance"</span><span class="p">]</span>

<span class="k">if</span> <span class="n">nsync</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">candidate</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">candidate</span><span class="si">}</span><span class="s2"> is in the band"</span>
<span class="k">end</span>

<span class="c1"># Or inline</span>
<span class="k">if</span> <span class="p">[</span><span class="s2">"Justin"</span><span class="p">,</span> <span class="s2">"JC"</span><span class="p">,</span> <span class="s2">"Chris"</span><span class="p">,</span> <span class="s2">"Joey"</span><span class="p">,</span> <span class="s2">"Lance"</span><span class="p">].</span><span class="nf">include?</span><span class="p">(</span><span class="n">member</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">member</span><span class="si">}</span><span class="s2"> can join the inevitable reunion tour"</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…Rails’s <code class="language-plaintext highlighter-rouge">in?</code> method for more natural reading:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nsync</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Justin"</span><span class="p">,</span> <span class="s2">"JC"</span><span class="p">,</span> <span class="s2">"Chris"</span><span class="p">,</span> <span class="s2">"Joey"</span><span class="p">,</span> <span class="s2">"Lance"</span><span class="p">]</span>

<span class="k">if</span> <span class="n">candidate</span><span class="p">.</span><span class="nf">in?</span><span class="p">(</span><span class="n">nsync</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">candidate</span><span class="si">}</span><span class="s2"> is in the band"</span>
<span class="k">end</span>

<span class="c1"># Reads naturally even inline</span>
<span class="k">if</span> <span class="n">member</span><span class="p">.</span><span class="nf">in?</span><span class="p">([</span><span class="s2">"Justin"</span><span class="p">,</span> <span class="s2">"JC"</span><span class="p">,</span> <span class="s2">"Chris"</span><span class="p">,</span> <span class="s2">"Joey"</span><span class="p">,</span> <span class="s2">"Lance"</span><span class="p">])</span>
  <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">member</span><span class="si">}</span><span class="s2"> can join the inevitable reunion tour"</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>The <code class="language-plaintext highlighter-rouge">in?</code> method reads like English. “Is Justin in NSYNC?” becomes <code class="language-plaintext highlighter-rouge">"Justin".in?(nsync)</code>. Compare that to <code class="language-plaintext highlighter-rouge">nsync.include?("Justin")</code>, which reads as “Does NSYNC include Justin?”—grammatically correct but less intuitive.</p>

<p>The <code class="language-plaintext highlighter-rouge">in?</code> method works with anything that responds to <code class="language-plaintext highlighter-rouge">include?</code>: arrays, ranges, sets, and strings.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"JC"</span><span class="p">.</span><span class="nf">in?</span><span class="p">(</span><span class="s2">"JC Chasez"</span><span class="p">)</span>                            <span class="c1">#=&gt; true</span>
<span class="mi">5</span><span class="p">.</span><span class="nf">in?</span><span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">10</span><span class="p">)</span>                                     <span class="c1">#=&gt; true</span>
<span class="ss">:harmony</span><span class="p">.</span><span class="nf">in?</span><span class="p">(</span><span class="no">Set</span><span class="p">[</span><span class="ss">:melody</span><span class="p">,</span> <span class="ss">:harmony</span><span class="p">,</span> <span class="ss">:rhythm</span><span class="p">])</span>   <span class="c1">#=&gt; true</span>
</code></pre></div></div>

<p>It also handles <code class="language-plaintext highlighter-rouge">nil</code> gracefully, returning <code class="language-plaintext highlighter-rouge">false</code> rather than raising an error:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"Justin"</span><span class="p">.</span><span class="nf">in?</span><span class="p">(</span><span class="kp">nil</span><span class="p">)</span>  <span class="c1">#=&gt; false</span>
</code></pre></div></div>

<h2 id="why-not">Why not?</h2>

<p>If you’re not using Rails, you’d need to add <code class="language-plaintext highlighter-rouge">activesupport</code> as a dependency. For a single method, that’s probably overkill.</p>

<p>Some teams prefer sticking with Ruby’s standard library to avoid “magic” methods that might confuse developers unfamiliar with Rails conventions. If your collection is already in a well-named variable, <code class="language-plaintext highlighter-rouge">nsync.include?(name)</code> reads <em>fine</em>.</p>

<p>Performance is identical—<code class="language-plaintext highlighter-rouge">in?</code> simply calls <code class="language-plaintext highlighter-rouge">include?</code> on the collection—so choose whichever reads better in context. For inline collections or when the subject of your conditional matters more than the collection, <code class="language-plaintext highlighter-rouge">in?</code> wins.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Simple Tailwind CSS 4 Setup for Jekyll]]></title>
    <link href="https://andycroll.com/ruby/simple-tailwind-css-4-setup-for-jekyll/"/>
    <updated>2026-01-27T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/simple-tailwind-css-4-setup-for-jekyll</id>
    <content type="html"><![CDATA[<p>Tailwind CSS 4 changed how configuration works. The JavaScript config file has been replaced by CSS-based configuration using <code class="language-plaintext highlighter-rouge">@theme</code> directives and uses the <code class="language-plaintext highlighter-rouge">tailwind</code> CLI to shake down the generated tailwind classes and minify. Here’s how to set it up with <a href="https://jekyllrb.com">Jekyll</a>.</p>

<h2 id="the-setup">The Setup</h2>

<p>Changes to four files, plus one more step if you want plugins.</p>

<h3 id="gemfile">Gemfile</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"jekyll-tailwind"</span><span class="p">,</span> <span class="ss">group: </span><span class="p">[</span><span class="ss">:jekyll_plugins</span><span class="p">]</span>
</code></pre></div></div>

<p>Run <code class="language-plaintext highlighter-rouge">bundle install</code> to fetch the gem. The <a href="https://github.com/crbelaus/jekyll-tailwind"><code class="language-plaintext highlighter-rouge">jekyll-tailwind</code></a> gem handles everything. No separate build pipeline, no PostCSS config, no watching for changes. It hooks into Jekyll’s build process.</p>

<p>Under the hood, it uses <a href="https://github.com/flavorjones/tailwindcss-ruby"><code class="language-plaintext highlighter-rouge">tailwindcss-ruby</code></a>—the same gem that powers Tailwind in Rails.</p>

<h3 id="_configyml">_config.yml</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-tailwind</span>

<span class="na">tailwind</span><span class="pi">:</span>
  <span class="na">input</span><span class="pi">:</span> <span class="s">assets/css/app.css</span>
  <span class="na">minify</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Point it at your CSS file and enable minification for production builds.</p>

<h3 id="tailwind-plugins-optional">Tailwind Plugins (optional)</h3>

<p>If you want Tailwind plugins like Typography or Forms, install them with npm:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @tailwindcss/typography @tailwindcss/forms
</code></pre></div></div>

<p>This requires Node.js on your system. If you’re only using core Tailwind utilities, skip this step.</p>

<p>For blogs, <code class="language-plaintext highlighter-rouge">@tailwindcss/typography</code> is particularly useful. It provides <code class="language-plaintext highlighter-rouge">prose</code> classes that style your markdown content with sensible defaults for headings, paragraphs, lists, code blocks, and blockquotes:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;article</span> <span class="na">class=</span><span class="s">"prose prose-lg"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;p&gt;</span>...<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/article&gt;</span>
</code></pre></div></div>

<h3 id="assetscssappcss">assets/css/app.css</h3>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@import</span> <span class="s1">"tailwindcss"</span><span class="p">;</span>
<span class="c">/* @plugin "@tailwindcss/typography"; */</span>
<span class="c">/* @plugin "@tailwindcss/forms"; */</span>

<span class="k">@theme</span> <span class="p">{</span>
  <span class="py">--font-serif</span><span class="p">:</span> <span class="s1">"Georgia"</span><span class="p">,</span> <span class="nb">serif</span><span class="p">;</span>
  <span class="py">--breakpoint-sm</span><span class="p">:</span> <span class="m">400px</span><span class="p">;</span>
  <span class="py">--breakpoint-md</span><span class="p">:</span> <span class="m">600px</span><span class="p">;</span>
  <span class="py">--breakpoint-lg</span><span class="p">:</span> <span class="m">800px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is where Tailwind 4’s changes shine. The <code class="language-plaintext highlighter-rouge">@theme</code> block replaces <code class="language-plaintext highlighter-rouge">tailwind.config.js</code>. Custom fonts, breakpoints, colors, spacing—all defined in CSS.</p>

<p>Plugins use the <code class="language-plaintext highlighter-rouge">@plugin</code> directive instead of being listed in a JavaScript config.</p>

<h3 id="_layoutsdefaulthtml">_layouts/default.html</h3>

<p>Include the stylesheet in your <a href="https://jekyllrb.com/docs/front-matter/">layout’s</a> <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"/assets/css/app.css"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<h2 id="why-this-works">Why This Works</h2>

<p>The <code class="language-plaintext highlighter-rouge">jekyll-tailwind</code> gem runs Tailwind’s CLI during Jekyll’s build. When you run <code class="language-plaintext highlighter-rouge">bundle exec jekyll serve</code>, it:</p>

<ol>
  <li>Processes your input CSS file</li>
  <li>Scans your templates for Tailwind classes</li>
  <li>Generates optimized CSS</li>
  <li>Outputs to <code class="language-plaintext highlighter-rouge">_site/assets/css/app.css</code></li>
</ol>

<p>Hot reload works. Change a class in a template, save, and the CSS rebuilds.</p>

<h2 id="why-not">Why Not?</h2>

<p>If you need complex PostCSS plugins beyond what Tailwind provides, you might want a separate build pipeline. But for most Jekyll sites, this setup handles everything.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Find the Last Matching Element with rfind]]></title>
    <link href="https://andycroll.com/ruby/find-the-last-matching-element-with-rfind/"/>
    <updated>2026-01-12T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/find-the-last-matching-element-with-rfind</id>
    <content type="html"><![CDATA[<p>Ruby 4.0 landed during Christmas 2025 with a bunch of new features. One <a href="https://bugs.ruby-lang.org/issues/21678">small but useful addition</a> is <a href="https://docs.ruby-lang.org/en/4.0/Array.html#method-i-rfind"><code class="language-plaintext highlighter-rouge">Array#rfind</code></a>, which finds the last element in an array that matches a condition.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…reversing the array or using <code class="language-plaintext highlighter-rouge">reverse_each</code> to find from the end:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">]</span>

<span class="n">numbers</span><span class="p">.</span><span class="nf">reverse</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
<span class="c1">#=&gt; 7</span>

<span class="c1"># or</span>
<span class="n">numbers</span><span class="p">.</span><span class="nf">reverse_each</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)</span>
<span class="c1">#=&gt; 8</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…Ruby 4.0’s <code class="language-plaintext highlighter-rouge">rfind</code> method:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">]</span>

<span class="n">numbers</span><span class="p">.</span><span class="nf">rfind</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
<span class="c1">#=&gt; 7</span>

<span class="c1"># with a block</span>
<span class="n">numbers</span><span class="p">.</span><span class="nf">rfind</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">even?</span> <span class="p">}</span>
<span class="c1">#=&gt; 8</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>The <code class="language-plaintext highlighter-rouge">rfind</code> method iterates backwards from the last element, returning the first element that matches the given condition. It’s the <strong>r</strong>everse of <code class="language-plaintext highlighter-rouge">find</code>, which starts from the beginning.</p>

<p>This is more efficient than <code class="language-plaintext highlighter-rouge">reverse.find</code> or <code class="language-plaintext highlighter-rouge">reverse_each.find</code> because it doesn’t create an intermediate reversed array or enumerator in memory. For large arrays, this <em>can</em> make a noticeable difference.</p>

<p>You might wonder why this was added to <code class="language-plaintext highlighter-rouge">Array</code> specifically rather than <code class="language-plaintext highlighter-rouge">Enumerable</code>. The <code class="language-plaintext highlighter-rouge">Enumerable</code> module relies on the <code class="language-plaintext highlighter-rouge">#each</code> method, which only works in the forward direction. The only way to scan backwards generically would be to convert to an array first, defeating the purpose. However, arrays can be traversed in either direction efficiently by the nature of their implementation in the Ruby VM.</p>

<h2 id="why-not">Why not?</h2>

<p>If you’re not yet on Ruby 4.0, you’ll need to stick with <code class="language-plaintext highlighter-rouge">reverse_each.find</code> for now. It’s not a dramatic difference, but <code class="language-plaintext highlighter-rouge">rfind</code> is cleaner and more intentional.</p>

<p>For very small arrays, the performance difference is negligible, so the main benefit is readability—your intent is clearer when you use <code class="language-plaintext highlighter-rouge">rfind</code>.</p>

<h2 id="did-you-know">Did you know?</h2>

<p>In the same changeset, <a href="https://kddnewton.com">Kevin Newton</a> also added a specific implementation of <code class="language-plaintext highlighter-rouge">Array#find</code> (and its alias <code class="language-plaintext highlighter-rouge">#detect</code>) rather than relying on the <code class="language-plaintext highlighter-rouge">Enumerable</code> version. This is a performance improvement for arrays.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Year in Review 2025]]></title>
    <link href="https://andycroll.com/other/year-in-review-2025/"/>
    <updated>2026-01-01T10:00:00+00:00</updated>
    <id>https://andycroll.com/other/year-in-review-2025</id>
    <content type="html"><![CDATA[<p>House moves, robot armies, and scratching itches.</p>

<h1 id="januaryfebruary">January—February</h1>

<p>Ten years ago, I was looking at the prices of T-shirt printing machines and figuring out the best way to manage stock and postal services. I’ve always wanted to have a place to exercise my design eye and this year I launched <a href="https://rubytshirts.com">Ruby T-shirts</a>.</p>

<p>I use a mix of Shopify and Printful and add designs whenever they come to me. I don’t have the headspace to manage stock and posting things myself, so this setup “just” (read mostly) works. From a financial perspective it just about breaks even: I’ve had over 100 orders this year and the relatively low cost cost of running a Shopify store combined with the low margins means it’s probably bought me a few negronis over the 12 months. But as an outlet for something I’ve always wanted to do, it scratches an itch. I absolutely love it.</p>

<p>It was also a good place to put the first ever Brighton Ruby T-shirt. Having struggled with socks a couple of years ago, I didn’t fancy printing 500 T-shirts of which only 30 were going to get used and 100 would remain in my loft in either very large or very small sizes.</p>

<p>Best T-shirts? Probably “<a href="https://rubytshirts.com/products/_why-the-foxes">_why Foxes Recreated</a>”. Also the stylised text designs: the famous <a href="https://rubytshirts.com/products/full-stack-and-ruby">Beatles T-shirt</a>, but for full stack Rails. Or perhaps “<a href="https://rubytshirts.com/products/my-heart-is-ruby">My Heart is Ruby</a>”.</p>

<p>At <a href="https://coveragebook.com">CoverageBook</a>, we welcomed our newest junior <a href="https://www.linkedin.com/in/darcyluo/">Darcy</a>: the third year in a row of hiring from <a href="https://www.lewagon.com">Le Wagon</a>. It really feels like our hiring process has been a massive success, optimising for curiosity, responsibility and ambition. This is above any specific Ruby skills (other than an inkling of Ruby-ish-ness in  the coding test). In 12 weeks on a hard, but by no means complete, introduction to programming web apps if you’re hiring for anything other than the <em>human</em> you’re doing it wrong.</p>

<h1 id="marchapril">March—April</h1>

<p>Late March, while on a short countryside break, I was introduced, painfully, to the concept of a “re-root canal” which, after a bunch of very strong painkillers and anti-inflammatories, finally happened in April. Would <em>not</em> recommend.</p>

<p>We also, on a bit of a whim, went to look at a house with the idea that we’d look and immediately discount the idea of moving (as we had done in years past). Except this time… we didn’t, and I’m now writing this post from that very house.</p>

<p>CoverageBook projects for the year were foundational. Removing costs, re-envisioning our data collection to scale further than the 10x it’s currently doing from when I first built the current version. Also moving everything back into the main application; the separation has served its purpose.</p>

<p>2025 was about making everything clearer, more straightforward and more maintainable with our ambitious small team.</p>

<p>Plus we shipped a bunch of cool (smaller) features and platforms throughout the year.</p>

<h1 id="mayjune">May—June</h1>

<p><a href="https://brightonruby.com">Brighton Ruby</a>: this year’s lineup was a massive hit with everyone who attended. Attendance was where it has been the last few years, around 450 people.</p>

<p>A small team of folks helped me out: Olly, Olivia and Ella from my team at CoverageBook pitched in on the day. My secret weapon for the last couple of years has been Anne-Marie from <a href="https://evetribe.co.uk">Eve Tribe</a>, who manages the back of house so I can lark about on stage.</p>

<h1 id="julyaugust">July—August</h1>

<p>A big chunk of July I spent in the south of France, as has been the case the last few years, with a mixture of visiting friends, drama camp for the kids, and enormous amounts of excellent pastries ordered with my functional “shop French”.</p>

<p>Mostly stayed in Brighton for August in preparation for moving house. Lots of boxes, loft clearing and last-minute wobbles.</p>

<p>It was also around this time that I got stuck into properly exploring LLM-based tools to assist with coding. Initially using <a href="https://claude.ai/code">Claude Code</a> fairly unsuccessfully in our many-year-old codebase, but gradually adding some process based on Brian Casel’s work on <a href="https://buildermethods.com/agent-os">AgentOS</a> and seeing other uses such as Gary beginning to vibe code some prototypes for the next iteration of CoverageBook in an extremely useful way.</p>

<h1 id="septemberoctober">September—October</h1>

<p>September took me back to Amsterdam for <a href="https://rubyonrails.org/world">Rails World</a> (two words!) again. Amanda put on an incredible show and it’s a great opportunity for me to spend time hanging out with folks who otherwise I wouldn’t see but I consider to be my friends. Such is life in a globally spread community of programmers.</p>

<p>Unfortunately, every hour I spent in Amsterdam was an hour less I had to prepare to move the following week.</p>

<p>After 13 years living in one spot in Brighton, we moved 15 minutes across town, much closer to the sea, much closer to the marina, feeling closer to the sky and the curve of the Earth.</p>

<p>Definitely feeling the weather more as we’re super exposed to the prevailing winds. Moving to an older house from a new build has increased the amount of maintenance and challenge as we plan to take on a fairly significant building project next year to make the place truly our own.</p>

<p>The kids can now walk to school and this has certainly made life mechanically a bit easier, but I’m not going to lie: my head’s been quite full of all of the things required. New windows. Insulation. Architect. Builders. Plumbers. The weight of the impending cost and disruption of having our downstairs non-functional for weeks.</p>

<p>Half term was two trips! Pompeii, which mostly thrilled me and Jo historically. Only the gelato and pasta truly engaged the kids.</p>

<p>Then a train journey the length of the country and a couple of days eating <a href="https://greatnorthpie.co">pies</a> and <a href="https://www.grasmeregingerbread.co.uk/product/-grasmere-gingerbread/six-pieces-of-grasmere-gingerbread">gingerbread</a>, wandering the hills of the Lakes. England truly has its moments.</p>

<h1 id="novemberdecember">November—December</h1>

<p>Gary’s prototypes really came into focus over the last couple of months of the year. As did my use of AI tools as an extra junior or two in my team, and in “Andy’s stupid side projects” world. The tools being built by <a href="https://every.to">Every</a> and <a href="https://conductor.build">Conductor</a> seem to be a real boost, alongside the release of the latest model from Anthropic. Scope and style are still a challenge. But “what to build”, as always, is the biggest challenge of all.</p>

<p>Seems I’ve been building the skills to manage code review and product review for the last decade in my career. It really feels like product-focused engineers have nothing to fear but their own capacity in the new world of LLM-based coding. It’s got much quicker to put boxes inside boxes on the Internet in the last six months.</p>

<p>The open source release of <a href="https://github.com/basecamp/fizzy">Fizzy</a> has provided a real insight into ways to use Rails. Perhaps I and other larger teams have strayed from these patterns over the years, but an effort to be more vanilla was underway before that. So I feel like we’re on the right course from a maintenance perspective and, with prototypes from Gary, it’s going to be quite an interesting year to come.</p>

<p><a href="https://www.reddit.com/r/ClaudeAI/comments/1pxvd0g/software_development_became_boring_with_claude/">This perspective on the new world of coding</a> (via <a href="https://justin.searls.co">Justin</a>) seems to capture why <em>I</em> dont feel the dread of the change in my day-to-day work. Aside from the obvious financial bubble, false promises and rapacious spending of the AI industry (“aside” doing a lot of work in that sentence) we’re going to be left with an entirey new set of useful tools once this era all shakes out and we learn how to hold everything properly.</p>

<h1 id="health">Health</h1>

<p>Locked into a metronomic Monday, Wednesday, Friday—before work—gym routine based on the <a href="https://ganbarumethod.com/products/minimalift">Minimalift</a> programme. Plus took up Vets football a little more seriously toward the back half of the year. Also PB’d my local parkrun at under 23 minutes, having previously believed 25 minutes was beyond me.</p>

<p>Want to try and both extend the distance I can go at “slow speed” and improve my 5k time, while trying to stay (self-inflicted) injury-free: which is easier said than done at 46.</p>

<h1 id="watched--loved">Watched &amp; Loved</h1>

<p><a href="https://tv.apple.com/gb/show/slow-horses/umc.cmc.2szz3fdt71tl1ulnbp8utgq5o">Slow Horses</a>, <a href="https://www.disneyplus.com/en-gb/series/andor/3xsQKWG00GL5">Andor</a>, <a href="https://www.netflix.com/title/81435684">Arcane</a>, <a href="https://www.bbc.co.uk/programmes/p0bg1l8v">The Traitors</a> and its nation-gripping spin-off <a href="https://www.bbc.co.uk/programmes/p0dxgv4r">Celebrity Traitors</a>, plus (with the kids) a rewatch of <a href="https://www.netflix.com/title/70281562">Brooklyn Nine-Nine</a>.</p>

<p><a href="https://www.netflix.com/title/81906780">Sean Combs: The Reckoning</a> was fascinating and horrifying (did he? Yes, he definitely did).</p>

<p>Movie-wise, highlights were mostly rewatches. Saw the original Star Wars trilogy at the Duke of York’s with the kids; <a href="https://www.imdb.com/title/tt0090605/">Aliens</a> at the IMAX. Spent a good deal of the year introducing the kids to various classics: <a href="https://www.imdb.com/title/tt0073195/">Jaws</a>, <a href="https://www.imdb.com/title/tt0167404/">The Sixth Sense</a>, <a href="https://www.imdb.com/title/tt0133093/">The Matrix</a>, <a href="https://www.imdb.com/title/tt0095016/">Die Hard</a>, <a href="https://www.imdb.com/title/tt0109831/">Four Weddings and a Funeral</a>, and Endgame-ing the MCU.</p>

<p>I enjoyed <a href="https://www.imdb.com/title/tt21823606/">A Real Pain</a>, <a href="https://www.imdb.com/title/tt17009710/">Anatomy of a Fall</a>, <a href="https://www.imdb.com/title/tt11563598/">A Complete Unknown</a>, <a href="https://www.imdb.com/title/tt27654768/">The Ballad of Wallis Island</a>, <a href="https://www.imdb.com/title/tt20215234/">Conclave</a>, <a href="https://www.imdb.com/title/tt30264060/">Sly Lives!</a>, and (same as everyone else) <a href="https://www.imdb.com/title/tt30144839/">One Battle After Another</a>. And I double-featured <a href="https://www.imdb.com/title/tt1262426/">Wicked</a> the evening it came out with my daughter. No, you’re crying.</p>

<h1 id="played">Played</h1>

<p>Completed (!) <a href="https://store.steampowered.com/app/2366980/Thank_Goodness_Youre_Here/">Thank Goodness You’re Here</a>, which must have mystified any non-British folk who stumbled into it. Played a ways into <a href="https://store.steampowered.com/app/2366970/Arco/">Arco</a>, before its combat got merciless. Enjoyed a bit of <a href="https://store.steampowered.com/app/2344170/Prince_of_Persia_The_Lost_Crown/">Prince of Persia: The Lost Crown</a> before falling out of its scale. <a href="https://store.steampowered.com/app/711540/Lonely_Mountains_Downhill/">Lonely Mountains: Downhill</a> and <a href="https://store.steampowered.com/app/8400/Geometry_Wars_Retro_Evolved/">Geometry Wars: Retro Evolved</a> are pick-up-and-play marvels.</p>

<p>A summer <a href="https://www.nintendo.com/en-gb/Hardware/Nintendo-Switch-2/Nintendo-Switch-2-2785301.html">Switch 2</a> purchase meant more <a href="https://www.nintendo.com/en-gb/Games/Nintendo-Switch-2/Mario-Kart-World-2792769.html">Mario Kart</a> and playing some previously janky Switch 1 titles at 60 FPS.</p>

<p>Replaying <a href="https://www.playstation.com/en-gb/games/the-last-of-us-part-i/">The Last of Us</a> before I commit to the TV show. More <a href="https://www.fortnite.com">Fortnite</a>; the Simpsons season was a highlight.</p>

<p>Gleefully enjoyed the nineties stylings of <a href="https://store.steampowered.com/app/3287940/Terminator_2D_No_Fate/">Terminator 2D: No Fate</a> once I’d broken up for Xmas.</p>

<h1 id="read">Read</h1>

<p>Continued the pure pleasurable nonsense of a handful of <a href="https://www.penguin.co.uk/series/JREACHER/jack-reacher">Reacher</a> books, plus inhaled some “space” <a href="https://www.brandonsanderson.com">Brandon Sanderson</a>. Really enjoyed <a href="https://www.waterstones.com/book/babel/r-f-kuang/9780008501853">Babel</a> and <a href="https://www.waterstones.com/book/yellowface/r-f-kuang/9780008532819">Yellowface</a>, only realising they were by the same author once I’d finished the second book.</p>

<h1 id="2026">2026</h1>

<p><a href="https://brightonruby.com">Brighton Ruby</a> #14 is already selling tickets.</p>

<p>Expecting to somewhat up my “side project” game with the assistance of my robot army. <a href="https://usingrails.com">Using Rails</a> continued to grow throughout the year, one of those side projects that ticks along nicely.</p>

<p>Really looking forward to personally coaching some of those “small projects we won’t get to” at work to fruition. The team’s focus, which I’ll be on top of (but staying out of the critical path) is a major (PR industry-transforming) feature push informed by the prototyping Gary’s been doing.</p>

<p>Failed wildly to get <a href="https://whatthestackpodcast.com">What The Stack</a> off the ground and fell off the <a href="https://andycroll.com/ruby">writing</a> train with the house move. I will be attempting to get back on a more regular cadence in 2026.</p>

<p>Closed the manual matching process for <a href="https://firstrubyfriend.org">First Ruby Friend</a> in August; hoping to relaunch with a proper automated platform in the new year.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Skip Validations in Specific Contexts with except_on]]></title>
    <link href="https://andycroll.com/ruby/except_on-conditional-validations/"/>
    <updated>2025-09-07T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/except_on-conditional-validations</id>
    <content type="html"><![CDATA[<p>Rails 8.0 added <code class="language-plaintext highlighter-rouge">except_on</code> as an <a href="https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates_each">option to validations</a>. It’s the inverse of <code class="language-plaintext highlighter-rouge">on:</code> and lets you skip validations in specific contexts.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…skipping all validations in your controller:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Admin::UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
    <span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="ss">validate: </span><span class="kp">false</span><span class="p">)</span>  <span class="c1"># Skips ALL validations!</span>
      <span class="n">redirect_to</span> <span class="n">admin_users_path</span>
    <span class="k">else</span>
      <span class="n">render</span> <span class="ss">:new</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>…or using methods that bypass Active Record entirely:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">admin_create</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">new</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">save</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">update_columns</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span>  <span class="c1"># No validations, no callbacks</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This example uses a class method on the model, but you might see similar code in <em>something like</em> a <code class="language-plaintext highlighter-rouge">UserFactory</code> or <code class="language-plaintext highlighter-rouge">UserCreationService</code> in your application.</p>

<h2 id="use">Use…</h2>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">validates</span> <span class="ss">:birthday</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">except_on: :admin_create</span>
<span class="k">end</span>

<span class="c1"># Admin creates user without birthday</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Jane"</span><span class="p">)</span>
<span class="n">user</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="ss">context: :admin_create</span><span class="p">)</span> <span class="c1"># =&gt; true</span>

<span class="c1"># Regular save still requires birthday</span>
<span class="n">user</span><span class="p">.</span><span class="nf">save</span> <span class="c1"># =&gt; false</span>
</code></pre></div></div>

<p>You can also stack multiple contexts:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> 
  <span class="ss">format: </span><span class="p">{</span> <span class="ss">with: </span><span class="no">URI</span><span class="o">::</span><span class="no">MailTo</span><span class="o">::</span><span class="no">EMAIL_REGEXP</span> <span class="p">},</span>
  <span class="ss">except_on: </span><span class="p">[</span><span class="ss">:admin_create</span><span class="p">,</span> <span class="ss">:bulk_import</span><span class="p">]</span>
</code></pre></div></div>

<h3 id="extending-this-to-callbacks">Extending this to callbacks</h3>

<p><a href="https://github.com/rails/rails/pull/54665">Validation callbacks get the same treatment</a> in Rails 8.1.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">before_validation</span> <span class="ss">:normalize_email</span><span class="p">,</span> <span class="ss">except_on: :quick_signup</span>
<span class="n">after_validation</span> <span class="ss">:check_email_uniqueness</span><span class="p">,</span> <span class="ss">except_on: </span><span class="p">[</span><span class="ss">:admin_create</span><span class="p">,</span> <span class="ss">:bulk_import</span><span class="p">]</span>
</code></pre></div></div>

<p>Now your callbacks follow the same pattern as your validations. Consistency wins.</p>

<h2 id="why">Why?</h2>

<p>Your admin interface needs to create users without complete data. Your data import skips some business rules. Perhaps your API has different requirements than your web forms.</p>

<p>Previously, you’d handle this with dangerous workarounds, seperate “factory” objects, or repetitive code.</p>

<h2 id="why-not">Why not?</h2>

<p>Every skipped validation is a possible data integrity issue. But that’s also true of the other approaches.</p>

<p><em>Real</em> database constraints can act as your safety net for true data consistency.</p>

<p>A form object or separate model might make more sense for your application or team but you might be fighting the framework.</p>

<p>If you are skipping validations (even conditionally) you should also take a moment to consider whether the validation is <em>really</em> required. Does your application break when there’s no data in that attribute or not? If it doesn’t then is there a need to validate at all?</p>

<p><em><code class="language-plaintext highlighter-rouge">except_on</code> can keep validations concise while making intent clear: “validate everywhere except here.” Just remember: with great power comes great responsibility.</em> And some folks will hate this as much as they hate callbacks.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Rails World 2025]]></title>
    <link href="https://andycroll.com/ruby/rails-world-2025/"/>
    <updated>2025-09-07T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/rails-world-2025</id>
    <content type="html"><![CDATA[<p>Off I went to Amsterdam via—so incredibly civilized—Eurostar from St Pancras wearing my favourite <a href="https://rubytshirts.com">Ruby T-shirts</a>.</p>

<p>Had a day mostly to myself in Amsterdam on Wednesday, drinking coffee, eating <a href="https://puccinibomboni.com">chocolates</a>, meeting friends, visiting the <a href="https://www.annefrank.org/en/">Anne Frank House</a> and generally enjoying the vibe of the city while avoiding its insalubrious corners.</p>

<p>I even mananged a run around the city before the conference.</p>

<p><a href="https://x.com/dhh">David</a> kicked off with a typical combative mixture of what’s new in Rails, his current enthusiasms and accidental set ups for <a href="https://bsky.app/profile/tenderlove.dev">Aaron’s</a> punchlines in the closing keynote.</p>

<p>Omarchy seems very appealing and I do have an urge to buy a Framework machine. The independence from exploitative gatekeepers (e.g. Apple) is a theme that I can get behind. That is the ongoing, upstart, driven, energy that an established framework like Rails really benefits from.</p>

<p>I also deleted all the system tests for CoverageBook in 2018, so I’m well ahead on that curve.</p>

<p>He performs the stage role of “50% more DHH” incredibly well during the keynote, and then puts in an additional extraordinary amount of energy and personal human-time talking to folks in the hallways over the next couple of days. Not sure I ever saw a moment when he wasn’t in intense conversation with an attendee, or likely more.</p>

<p>I enjoyed the more “depth on new features” talks that followed David’s keynote: <a href="https://bsky.app/profile/rosa.codes">Rosa</a> (offline) <a href="https://bsky.app/profile/joemasilotti.com">Joe</a> (Hotwire Native) <a href="https://x.com/adriannakchang">Adrianna</a> (Events) <a href="https://bsky.app/profile/flavorjon.es">Mike</a> &amp; <a href="https://www.linkedin.com/in/donal-mcbreen-a8227a52/">Donal</a> (even more DB/SQLite). Plus <a href="https://bsky.app/profile/marcoroth.dev">Marco’s</a> work on the view layer is innovative, super-useful, and a huge upgrade. And the quality was as good as it’s ever been. And the talks I wanted to see, but missed through being in a different track or getting caught up in conversation will be there for me on YouTube.</p>

<p>The MCs, <a href="https://x.com/oughtputs">Harriet</a> and <a href="https://x.com/typecraft_dev">Chris</a>, made a great success of what is actually a horrible job: they’re “on” all day, under huge pressure and barely even get to see the talks. The after party was incredibly cool, much too cool for me really but had a good time anyway. Food good. Coffee plentiful. Sponsors friendly.</p>

<p>My main takeaway from this year’s event was the imperceptible additional smoothness and polish where things weren’t even wrong in the previous two editions.</p>

<p>Those improvements are the hallmark of a great event organiser. Which is what we are fortunate to have in <a href="https://www.linkedin.com/in/amandabrookeperino">Amanda Perino</a>.</p>

<p>She arrived to some scepticism in parts of our community. But her positivity energy and enthusiasm have created a festival where we can all meet. Plus I genuinely—even as a conference organiser of over a decade—have no idea how she makes it so shiny and brilliant. I do know she breaks herself a little bit so we can all enjoy ourselves.</p>

<p>As always, some of the best parts of the conference were catching up with long-term friends. I’ve been doing this work-related socialisation thing for so long that it’s an aching hole in my year if I don’t see folks for a few days.</p>

<p>I don’t know how to list it really. Every conversation always seems too brief or sometimes doesn’t happen because you only see someone in passing despite all being in the same three rooms. But there’s always next year or another conference and now I have my Ruby Passport that I need to pack with stamps.</p>

<p>Look out for an embassy at <a href="https://brightonruby.com">Brighton Ruby</a> next year.</p>

<p>Also, given that apparently 1999 was the peak of web development: I made a 88x31 badge.</p>

<p><img src="/images/2025/rails-88x31.gif" alt="Rails 88x31 badge" />
<img src="/images/2025/rails-88x31-red.gif" alt="Rails 88x31 badge alternate" /></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Further Performance Testing Enumerable’s Loveliness]]></title>
    <link href="https://andycroll.com/ruby/further-options-odd_sum-with-benchmarking/"/>
    <updated>2025-05-29T08:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/further-options-odd_sum-with-benchmarking</id>
    <content type="html"><![CDATA[<p>Ok, I’ll stop after this one, but I <a href="/ruby/benchmarking-odd_sum">said that before</a>. Plenty of fun nerd-sniping on this problem.</p>

<p>I was pointed at <code class="language-plaintext highlighter-rouge">Enumerable#partition</code> (by <a href="https://bsky.app/profile/baweaver.bsky.social/post/3lq42wj6efs2k">Brandon</a>, <a href="https://chaos.social/@citizen428">Michael</a>, <a href="https://bsky.app/profile/chastell.net/post/3lq5s3ummnc2i">Piotr</a> &amp; <a href="https://bsky.app/profile/kaspth.bsky.social/post/3lqa7ft5f5k2v">Kasper</a>) which would avoid two of the four loops in the previously “best” solution.</p>

<p>I was also nudged to benchmark my initial “loops” solution by <a href="https://ruby.social/@davetron5000/114576061212512715">Dave</a>, because straightforward loops are often extremely well optimised at the language level.</p>

<p>So… here’s all the benchmarks for that.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'benchmark/ips'</span>

<span class="n">n</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">a</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>
<span class="n">b</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>

<span class="no">Benchmark</span><span class="p">.</span><span class="nf">ips</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
  <span class="c1"># Pre-filter by odd/even, then compute products</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even'</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span> <span class="o">+</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)))</span>
    <span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="c1"># Alternatives: compute all products, then filter by sum</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'full product, +'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="p">(</span><span class="n">_1</span> <span class="o">+</span> <span class="n">_2</span><span class="p">).</span><span class="nf">odd?</span> <span class="p">}.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'full product, sum'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="n">_1</span><span class="p">.</span><span class="nf">sum</span><span class="p">.</span><span class="nf">odd?</span> <span class="p">}.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'loops'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
          <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">results</span><span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="p">(</span><span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">+</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)).</span><span class="nf">uniq</span>
  <span class="k">end</span>

  <span class="n">x</span><span class="p">.</span><span class="nf">compare!</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here’s the results:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Calculating -------------------------------------
          odd &amp; even      1.347k (± 2.2%) i/s  (742.59 μs/i) -      6.850k in   5.089081s
     full product, +    785.253  (± 1.5%) i/s    (1.27 ms/i) -      3.952k in   5.034013s
   full product, sum    719.042  (± 2.1%) i/s    (1.39 ms/i) -      3.650k in   5.078324s
               loops    893.864  (± 1.3%) i/s    (1.12 ms/i) -      4.488k in   5.021755s
           partition      1.370k (± 1.8%) i/s  (729.79 μs/i) -      6.900k in   5.037264s

Comparison:
           partition:     1370.3 i/s
          odd &amp; even:     1346.6 i/s - same-ish: difference falls within error
               loops:      893.9 i/s - 1.53x  slower
     full product, +:      785.3 i/s - 1.74x  slower
   full product, sum:      719.0 i/s - 1.91x  slower
</code></pre></div></div>

<p>Further improvements were suggested.</p>

<p>If we use a more apt method for <a href="https://docs.ruby-lang.org/en/master/Array.html#class-Array-label-Methods+for+Combining">combining arrays</a>, like <a href="https://docs.ruby-lang.org/en/master/Array.html#method-i-7C"><code class="language-plaintext highlighter-rouge">|</code></a> or <a href="https://docs.ruby-lang.org/en/master/Array.html#method-i-union"><code class="language-plaintext highlighter-rouge">union</code></a> we may see a performance improvement as we avoid the additional <code class="language-plaintext highlighter-rouge">uniq</code> loop through the final array of results.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'benchmark/ips'</span>

<span class="n">n</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">a</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>
<span class="n">b</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>

<span class="no">Benchmark</span><span class="p">.</span><span class="nf">ips</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even'</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span> <span class="o">+</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)))</span>
    <span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even union'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)).</span><span class="nf">union</span><span class="p">(</span>
        <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)))</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even |'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span> <span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'loops'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
          <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">results</span><span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'loops |'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
          <span class="n">results</span> <span class="o">|</span> <span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">results</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="p">(</span><span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">+</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)).</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition union'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">).</span><span class="nf">union</span><span class="p">(</span><span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">))</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition |'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">|</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="n">x</span><span class="p">.</span><span class="nf">compare!</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The results are pretty much a wash for the partition and odd &amp; even cases.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Calculating -------------------------------------
          odd &amp; even      1.375k (± 0.4%) i/s  (727.08 μs/i) -      6.987k in   5.080142s
    odd &amp; even union      1.334k (± 0.4%) i/s  (749.77 μs/i) -      6.700k in   5.023541s
        odd &amp; even |      1.339k (± 0.4%) i/s  (746.81 μs/i) -      6.700k in   5.003743s
               loops    896.130 (± 0.9%) i/s    (1.12 ms/i) -      4.500k in   5.022004s
             loops |      1.416k (± 1.1%) i/s  (706.43 μs/i) -      7.100k in   5.016274s
           partition      1.378k (± 0.8%) i/s  (725.47 μs/i) -      6.900k in   5.006093s
     partition union      1.349k (± 0.6%) i/s  (741.43 μs/i) -      6.834k in   5.067094s
         partition |      1.346k (± 0.4%) i/s  (742.74 μs/i) -      6.834k in   5.075995s

Comparison:
             loops |:     1415.6 i/s
           partition:     1378.4 i/s - 1.03x  slower
          odd &amp; even:     1375.4 i/s - 1.03x  slower
     partition union:     1348.7 i/s - 1.05x  slower
         partition |:     1346.4 i/s - 1.05x  slower
        odd &amp; even |:     1339.0 i/s - 1.06x  slower
    odd &amp; even union:     1333.7 i/s - 1.06x  slower
               loops:      896.1 i/s - 1.58x  slower
</code></pre></div></div>

<p>However, the performance of the “straightforward” loop solution is still significantly the worst, but when using the<code class="language-plaintext highlighter-rouge">|</code> operator in the depths of the loop, it leaps to becoming marginally better performing than all the other implementations.</p>

<p>One last thing. Ruby has a <a href="https://docs.ruby-lang.org/en/master/Enumerator/Lazy.html">lazy evaluation</a> feature which can be used to avoid overheads of creating intermediate arrays in large datasets, but it’s only available on certain methods on <code class="language-plaintext highlighter-rouge">Enumerable</code>.</p>

<p>One of those rewritten methods is <code class="language-plaintext highlighter-rouge">uniq</code>, does making the uniqueness loop a “lazy” enumerator affect performance? You can also use <code class="language-plaintext highlighter-rouge">lazy</code> with <code class="language-plaintext highlighter-rouge">each</code> in the “looping” example.</p>

<p>How do these new additions to the benchmark fare?</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'benchmark/ips'</span>

<span class="n">n</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">a</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>
<span class="n">b</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>

<span class="no">Benchmark</span><span class="p">.</span><span class="nf">ips</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even'</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span> <span class="o">+</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)))</span>
    <span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even lazy'</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span> <span class="o">+</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)))</span>
    <span class="p">.</span><span class="nf">lazy</span><span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'loops'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
          <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">results</span><span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'loops |'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
          <span class="n">results</span> <span class="o">|</span> <span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">results</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'loops | lazy'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">lazy</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">lazy</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
          <span class="n">results</span> <span class="o">|</span> <span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">results</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="p">(</span><span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">+</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)).</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition lazy'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="p">(</span><span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">+</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)).</span><span class="nf">lazy</span><span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition |'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">|</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="n">x</span><span class="p">.</span><span class="nf">compare!</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It <em>looks like</em> the <code class="language-plaintext highlighter-rouge">lazy</code> version of <code class="language-plaintext highlighter-rouge">uniq</code> is a lot faster than the non-lazy versions, or the improved “loops with <code class="language-plaintext highlighter-rouge">|</code>” example, according to this test.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Calculating -------------------------------------
          odd &amp; even      1.351k (± 2.5%) i/s  (740.37 μs/i) -      6.834k in   5.063314s
     odd &amp; even lazy      6.145k (± 1.1%) i/s  (162.74 μs/i) -     31.263k in   5.088472s
               loops    917.432 (± 0.7%) i/s    (1.09 ms/i) -      4.641k in   5.058882s
             loops |      1.481k (± 2.7%) i/s  (675.29 μs/i) -      7.446k in   5.032326s
        loops | lazy      1.466k (± 0.3%) i/s  (682.27 μs/i) -      7.436k in   5.073396s
           partition      1.373k (± 0.5%) i/s  (728.15 μs/i) -      6.936k in   5.050569s
      partition lazy      5.224k (± 1.0%) i/s  (191.41 μs/i) -     26.180k in   5.011751s
         partition |      1.339k (± 0.7%) i/s  (746.74 μs/i) -      6.732k in   5.027289s

Comparison:
     odd &amp; even lazy:     6144.7 i/s
      partition lazy:     5224.3 i/s - 1.18x  slower
             loops |:     1480.9 i/s - 4.15x  slower
        loops | lazy:     1465.7 i/s - 4.19x  slower
           partition:     1373.3 i/s - 4.47x  slower
          odd &amp; even:     1350.7 i/s - 4.55x  slower
         partition |:     1339.2 i/s - 4.59x  slower
               loops:      917.4 i/s - 6.70x  slower
</code></pre></div></div>

<p>Is there some magic behind the scenes that makes the lazy version of <code class="language-plaintext highlighter-rouge">uniq</code> faster than the non-lazy version? <strong>Sadly, no.</strong> The lack of a performance difference between the lazy and non-lazy “loop” versions should be the clue here.</p>

<p><code class="language-plaintext highlighter-rouge">uniq</code> processes the entire array immediately and returns a new array with duplicates removed. It has to examine every element to determine uniqueness.</p>

<p><code class="language-plaintext highlighter-rouge">lazy.uniq</code> returns a <code class="language-plaintext highlighter-rouge">Enumerator::Lazy</code> that <em>doesn’t do any work</em> until you start consuming elements from it! In order for this to be a fair comparison the code would have to be:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1"># e.g.</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'partition lazy'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">odd_as</span><span class="p">,</span> <span class="n">even_as</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="n">odd_bs</span><span class="p">,</span> <span class="n">even_bs</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="nf">partition</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">)</span>
    <span class="p">(</span><span class="n">odd_as</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_bs</span><span class="p">)</span> <span class="o">+</span> <span class="n">odd_bs</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">even_as</span><span class="p">)).</span><span class="nf">lazy</span><span class="p">.</span><span class="nf">uniq</span><span class="p">.</span><span class="nf">to_a</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>The results bear this out:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Calculating -------------------------------------
             loops |      1.436k (± 2.6%) i/s  (696.42 μs/i) -      7.191k in   5.011518s
        loops | lazy      1.424k (± 1.4%) i/s  (702.40 μs/i) -      7.228k in   5.078029s
           partition      1.359k (± 1.7%) i/s  (735.93 μs/i) -      6.885k in   5.068374s
      partition lazy    814.193  (± 2.9%) i/s    (1.23 ms/i) -      4.116k in   5.060084s

Comparison:
             loops |:     1435.9 i/s
        loops | lazy:     1423.7 i/s - same-ish: difference falls within error
           partition:     1358.8 i/s - 1.06x  slower
      partition lazy:      814.2 i/s - 1.76x  slower
</code></pre></div></div>

<p>What have we learned from this wild goose chase of nerd-sniping?</p>

<ol>
  <li>It’s ok to optimise for readability.</li>
  <li>It’s important to <em>accurately</em> benchmark your code when you’re trying to optimize.</li>
  <li>Ruby is delightful.</li>
  <li>I sometimes can’t let go.</li>
</ol>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Performance Testing Enumerable’s Loveliness]]></title>
    <link href="https://andycroll.com/ruby/benchmarking-odd_sum/"/>
    <updated>2025-05-26T20:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/benchmarking-odd_sum</id>
    <content type="html"><![CDATA[<p>After sharing my <a href="/ruby/cassiecodes-odd_sum-programming-exercise">solution to Cassidy Williams’ oddSum challenge</a>, <a href="https://bsky.app/profile/fxn.bsky.social/post/3lq34jgmgo22f">Xavier</a> &amp; <a href="https://ruby.social/@pointlessone@status.pointless.one/114574888052999616">Alex</a> suggested a simpler approach on social media. This got me curious: does avoiding the mathematical check actually improve performance?</p>

<p>So I decided to benchmark the solutions to find out.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'benchmark/ips'</span>

<span class="n">n</span> <span class="o">=</span> <span class="mi">10</span>
<span class="n">a</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>
<span class="n">b</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span> <span class="p">}</span>

<span class="no">Benchmark</span><span class="p">.</span><span class="nf">ips</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
  <span class="c1"># Mine: pre-filter by odd/even, then compute products</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'odd &amp; even check'</span><span class="p">)</span> <span class="k">do</span>
    <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">))</span> <span class="o">+</span>
      <span class="n">b</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:odd?</span><span class="p">).</span><span class="nf">product</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:even?</span><span class="p">)))</span>
    <span class="p">.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="c1"># Alternatives: compute all products, then filter by sum</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'full product, +'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="p">(</span><span class="n">_1</span> <span class="o">+</span> <span class="n">_2</span><span class="p">).</span><span class="nf">odd?</span> <span class="p">}.</span><span class="nf">uniq</span>
  <span class="k">end</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s1">'full product, sum'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">a</span><span class="p">.</span><span class="nf">product</span><span class="p">(</span><span class="n">b</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="n">_1</span><span class="p">.</span><span class="nf">sum</span><span class="p">.</span><span class="nf">odd?</span> <span class="p">}.</span><span class="nf">uniq</span>
  <span class="k">end</span>

  <span class="n">x</span><span class="p">.</span><span class="nf">compare!</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In the testing script I set a variable (<code class="language-plaintext highlighter-rouge">n</code>) for the size of the generated arrays. I presumed that for small arrays there’d be very little difference it would be more noticeable as the arrays grew larger. Intuitively, I’d expect the performance to be worse when using the full product approach, as the memory allocation of the intermediate array would be much larger.</p>

<p>Here’s the results when <code class="language-plaintext highlighter-rouge">n = 10</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Warming up --------------------------------------
    odd &amp; even check     9.686k i/100ms
     full product, +     7.030k i/100ms
   full product, sum     6.911k i/100ms
Calculating -------------------------------------
    odd &amp; even check     97.138k (± 0.7%) i/s   (10.29 μs/i) -    493.986k in   5.085694s
     full product, +     74.138k (± 1.8%) i/s   (13.49 μs/i) -    372.590k in   5.027459s
   full product, sum     69.031k (± 0.9%) i/s   (14.49 μs/i) -    345.550k in   5.006096s

Comparison:
    odd &amp; even check:    97137.7 i/s
     full product, +:    74137.7 i/s - 1.31x  slower
   full product, sum:    69031.3 i/s - 1.41x  slower
</code></pre></div></div>

<p>A surprising difference in comparative performance, given the small size of the array, but the individual runtimes are very small; in the microseconds.</p>

<p>And for <code class="language-plaintext highlighter-rouge">n = 1_000</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Warming up --------------------------------------
    odd &amp; even check     1.000 i/100ms
     full product, +     1.000 i/100ms
   full product, sum     1.000 i/100ms
Calculating -------------------------------------
    odd &amp; even check     12.568 (± 8.0%) i/s   (79.57 ms/i) -     63.000 in   5.023134s
     full product, +      6.891 (± 0.0%) i/s  (145.12 ms/i) -     35.000 in   5.091415s
   full product, sum      6.369 (± 0.0%) i/s  (157.00 ms/i) -     32.000 in   5.029184s

Comparison:
    odd &amp; even check:       12.6 i/s
     full product, +:        6.9 i/s - 1.82x  slower
   full product, sum:        6.4 i/s - 1.97x  slower
</code></pre></div></div>

<p>And for <code class="language-plaintext highlighter-rouge">n = 10_000</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Warming up --------------------------------------
    odd &amp; even check     1.000 i/100ms
     full product, +     1.000 i/100ms
   full product, sum     1.000 i/100ms
Calculating -------------------------------------
    odd &amp; even check      0.054 (± 0.0%) i/s    (18.57 s/i) -      1.000 in  18.574282s
     full product, +      0.021 (± 0.0%) i/s    (46.66 s/i) -      1.000 in  46.663944s
   full product, sum      0.022 (± 0.0%) i/s    (45.83 s/i) -      1.000 in  45.833142s

Comparison:
    odd &amp; even check:        0.1 i/s
   full product, sum:        0.0 i/s - 2.47x  slower
     full product, +:        0.0 i/s - 2.51x  slower
</code></pre></div></div>

<p>The main surprise, which I didn’t initially consider, is how quickly the performance degrades with any of the approaches as the array sizes increase.</p>

<p>It makes sense, as that’s a factor of O(<code class="language-plaintext highlighter-rouge">n</code>²) (or near enough) in all solutions. Thousand element arrays mean one million pairs to evaluate. Whereas ten thousand element arrays is one hundred million pairs: a 100x increase! The use of <code class="language-plaintext highlighter-rouge">.product</code> ends up creating large intermediate arrays which increase memory usage.</p>

<p>In reality, for small arrays—as suggested in the original question—the performance difference is negligible, so picking the most readable solution is the most important decision. As the arrays get beyond the thousands of elements, were we to execute this task in a typical web request, none of the solutions are going to scale well.</p>

<p>For truly large datasets we might consider streaming, lazy evaluation or parallelization, but that’s outside the scope of <em>this</em> <a href="https://en.wikipedia.org/wiki/Code_golf">code golfing</a> exercise.</p>
]]></content>
  </entry>
  
</feed>
