The Pug Automatic2023-07-13T00:00:00Zhttps://thepugautomatic.com/Henrik NyhRuby Module Builder patterns2023-07-13T00:00:00Zhttps://thepugautomatic.com/2023/07/ruby-module-builder-patterns/<p>I <a href="https://ruby.social/@henrik/110673354335407050">recently discovered</a> the Ruby <a href="https://dejimata.com/2017/5/20/the-ruby-module-builder-pattern">Module Builder pattern</a>.</p>
<p>It lets you pass in arguments to dynamically generate a module at <code>include</code> time:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> puts <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token delimiter tag">}</span></span>!"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><span class="token keyword">end</span><br /><br /><span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>greet<br /><span class="token comment"># => "Hello world!"</span></code></pre>
<p>I thought I'd share some insights and patterns I've found when using it.</p>
<h2>The instance-mixin duality</h2>
<p>Crucially (and mind-bendingly), you need to set up your module inside the <code>initialize</code> block. Methods defined on <code>Greeter</code> won't be available on including classes:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span> <span class="token operator">=</span> <span class="token comment"># …</span><br /><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greet_on_module</span></span> <span class="token operator">=</span> <span class="token string">"Hello!"</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><br /><span class="token keyword">end</span><br /><br /><span class="token comment"># This works:</span><br /><span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>greet_on_module<br /><br /><span class="token comment"># But this raises NoMethodError:</span><br /><span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>greet_on_module</code></pre>
<p>This broke my brain initially, but it makes sense when you think through it.</p>
<p>Modules have a dual nature: they are class instances, and they can also be mixed into classes and other modules.</p>
<h3>Modules are class instances</h3>
<p><code>Module</code> is a class. So are its subclasses, like <code>Greeter</code> above.</p>
<p>When you do <code>Greeter.new</code>, you get an instance of <code>Greeter</code>. This instance is a module that you can mix in.</p>
<pre class="language-ruby"><code class="language-ruby">instance <span class="token operator">=</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> instance<br /><span class="token keyword">end</span></code></pre>
<p>When you use <code>module</code>, you also get an instance, but <em>assigned to a global constant</em>:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">module</span> <span class="token constant">OtherGreeter</span><br /><span class="token keyword">end</span><br /><br />instance <span class="token operator">=</span> <span class="token constant">OtherGreeter</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> instance<br /><span class="token keyword">end</span></code></pre>
<p><a name="footnote-1-source"></a></p>
<p>So <code>Greeter</code> is a <em>class</em> whose instances are modules you can mix in; <code>OtherGreeter</code> is not a class, but is <em>itself</em> a module you can mix in<a href="https://thepugautomatic.com/2023/07/ruby-module-builder-patterns/#footnote-1" class="footnote-link">¹</a>.</p>
<p>After all, the whole point of <code>Greeter</code> is to create <em>multiple</em> modules – one for <code>"world"</code>, another for <code>"moon"</code> – so they can't all be assigned to a single <code>Greeter</code> constant.</p>
<p>And this explains why <code>greet_on_module</code> can only be called on instances of <code>Greeter</code>. It's an instance method and the instances are modules. It's equivalent to</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">module</span> <span class="token constant">OtherGreeter</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">greet_on_module</span></span> <span class="token operator">=</span> <span class="token string">"Hello!"</span><br /><span class="token keyword">end</span></code></pre>
<p>You would not expect this method to be included when you mix in the module.</p>
<h3>Modules can be mixed in</h3>
<p>Classes have instance methods that you call on instances but not on the class itself.</p>
<p>Modules also have <a href="https://ruby-doc.org/3.2/syntax/modules_and_classes_rdoc.html#label-Methods">instance methods</a> (we might think of them as "mixin methods") that are mixed in, but are not called on the module itself.</p>
<p>With the <code>module</code> keyword, we just define those in the module body:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">module</span> <span class="token constant">OtherGreeter</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greet</span></span> <span class="token operator">=</span> <span class="token string">"Hello!"</span><br /><span class="token keyword">end</span></code></pre>
<p>With <a href="https://ruby-doc.org/3.2/Module.html#method-c-new"><code>Module.new</code></a>, we define them in a block:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token builtin">Module</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token keyword">do</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greet</span></span> <span class="token operator">=</span> <span class="token string">"Hello!"</span><br /><span class="token keyword">end</span></code></pre>
<p>With <code>class … < Module</code>, we define them in the initializer:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> puts <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token delimiter tag">}</span></span>!"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>It makes sense. This is a class that creates modules. We need to create a module before we can define instance methods on it, and it's only in the initializer that we've created it.</p>
<p><a href="https://ruby-doc.org/3.2/Module.html#method-i-define_method"><code>define_method</code></a> defines instance methods on the receiver, which inside the initializer is the module we created.</p>
<p>We can't use <code>def greet</code> inside the initializer. As with any class, <code>def</code> inside the initializer defines instance methods on the class. It would be just like <code>greet_on_module</code>.</p>
<p>There is still a way to use <code>def</code>, though.</p>
<h2>Using <code>module_eval</code></h2>
<p>Sometimes <code>define_method</code> is exactly what we need, if we use the passed-in values to determine method names (as in <a href="https://github.com/rails/rails/blob/a5fc471b3f4bbd02e6be38dae023526a49e7d049/activemodel/lib/active_model/secure_password.rb#L149-L152"><code>SecurePassword</code></a>), or whether to define a method at all.</p>
<p>But especially in a more complex module, it's nice to be able to use <code>def</code> for most of it, with <code>define_method</code> oneliners only to capture passed-in data.</p>
<p><a href="https://ruby-doc.org/3.2/Module.html#method-i-module_eval"><code>module_eval</code></a> to the rescue:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">:</span><span class="token punctuation">,</span> time<span class="token punctuation">:</span><span class="token punctuation">)</span><br /> <span class="token keyword">private</span> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greeter_name</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> name <span class="token punctuation">}</span><br /> <span class="token keyword">private</span> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greeter_time</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> time <span class="token punctuation">}</span><br /><br /> module_eval <span class="token keyword">do</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greet</span></span> <span class="token operator">=</span> <span class="token string">"Good <span class="token interpolation"><span class="token delimiter tag">#{</span>greeter_time<span class="token delimiter tag">}</span></span>, <span class="token interpolation"><span class="token delimiter tag">#{</span>greeter_name<span class="token delimiter tag">}</span></span>!"</span><br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p><a name="footnote-2-source"></a></p>
<p>We still need <code>define_method</code> to make the passed-in data available to <code>def</code>ed methods – I can't think of a sensible<a href="https://thepugautomatic.com/2023/07/ruby-module-builder-patterns/#footnote-2" class="footnote-link">²</a> way around that.</p>
<p>Note that I'm defining <code>greeter_name</code> etc rather than <code>name</code>, since this method will be mixed into <code>MyClass</code>, where names could otherwise conflict.</p>
<h2>Using <code>ActiveSupport::Concern</code></h2>
<p>Here's an example of a Module Builder using <a href="https://api.rubyonrails.org/v7.0.6/classes/ActiveSupport/Concern.html#method-i-included"><code>ActiveSupport::Concern</code></a>, since it took me a few attempts to get right.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string">"active_support/concern"</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> <span class="token keyword">extend</span> <span class="token constant">ActiveSupport</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Concern</span><br /><br /> class_methods <span class="token keyword">do</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greeter_name</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> name <span class="token punctuation">}</span><br /><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">classy_greet</span></span> <span class="token operator">=</span> <span class="token string">"Classy hello <span class="token interpolation"><span class="token delimiter tag">#{</span>greeter_name<span class="token delimiter tag">}</span></span>!"</span><br /> <span class="token keyword">end</span><br /><br /> module_eval <span class="token keyword">do</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greet</span></span> <span class="token operator">=</span> <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span>greeter_name<span class="token delimiter tag">}</span></span>!"</span><br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><span class="token keyword">end</span><br /><br />puts <span class="token constant">MyClass</span><span class="token punctuation">.</span>classy_greet<br />puts <span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>greet</code></pre>
<p>Note that <code>extend ActiveSupport::Concern</code> goes inside the initializer.</p>
<p>Don't be tempted to replace <code>module_eval</code> with <a href="https://api.rubyonrails.org/v7.0.6/classes/ActiveSupport/Concern.html#method-i-included"><code>included</code></a>. Both let you use <code>def</code>, but <code>included</code> would define methods <em>on the including class</em>, not on the module. This means you can't <a href="https://thepugautomatic.com/2013/07/dsom/">override them</a> conveniently. <code>included</code> is still fine for <em>calling</em> class methods, of course.</p>
<h2>Non-initializer builders</h2>
<p><a href="https://ruby.social/@maxim/110708280090132743">Max mentioned</a> a <a href="https://notes.max.engineer/camelize-json-keys-in-rails">variation</a> on this technique, where you don't use the initializer.</p>
<p>Arguably this is a little less confusing; <code>Module.new { … }</code> is a common pattern.</p>
<p>It also lets you define multiple builders on the same module.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">module</span> <span class="token constant">Greeter</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">by_name</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> <span class="token builtin">Module</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token keyword">do</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token delimiter tag">}</span></span>!"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">loudly_by_name</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> <span class="token builtin">Module</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token keyword">do</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token string">"HELLO <span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token punctuation">.</span>upcase<span class="token delimiter tag">}</span></span>!!1"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span>by_name<span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyLoudClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span>loudly_by_name<span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><span class="token keyword">end</span><br /><br />puts <span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>greet<br />puts <span class="token constant">MyLoudClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>greet</code></pre>
<h2>Template methods</h2>
<p>You could also use <a href="https://en.wikipedia.org/wiki/Template_method_pattern">the Template Method pattern</a> – pick a conventional method name and define that on the class:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">module</span> <span class="token constant">Greeter</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greet</span></span> <span class="token operator">=</span> <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span>greeter_name<span class="token delimiter tag">}</span></span>!"</span><br /><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greeter_name</span></span> <span class="token operator">=</span> <span class="token keyword">raise</span> <span class="token constant">NoMethodError</span><span class="token punctuation">,</span> <span class="token string">"Define <span class="token interpolation"><span class="token delimiter tag">#{</span>__method__<span class="token delimiter tag">}</span></span>!"</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">greeter_name</span></span> <span class="token operator">=</span> <span class="token string">"world"</span><br /><span class="token keyword">end</span></code></pre>
<p>That's what we had before moving to module builders.</p>
<p>It's simpler to reason about, but also has some downsides:</p>
<ul>
<li>If you define the method next to the <code>include</code>, that groups them nicely, but linters may complain about the class layout if you define a method in between two <code>include</code>s. If you define the method further away, they are not grouped as nicely.</li>
<li>We expose the longer, qualified names (<code>greeter_name</code>) in the including class rather than shorter, unqualified names (<code>name</code> or a positional argument). Arguably less elegant.</li>
<li>We define an extra method instead of just passing in an argument. Arguably less elegant.</li>
<li>We can't dynamically decide method names or whether to define a method.</li>
<li>We don't get multiple modules with different identity.</li>
</ul>
<h2>Module identity</h2>
<p>Regular modules let you check if they're mixed in:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">.</span>is_a<span class="token operator">?</span><span class="token punctuation">(</span><span class="token constant">Greeter</span><span class="token punctuation">)</span><br /><span class="token constant">MyClass</span> <span class="token operator"><</span> <span class="token constant">Greeter</span><br /><span class="token constant">Greeter</span> <span class="token operator">===</span> <span class="token constant">MyClass</span><span class="token punctuation">.</span><span class="token keyword">new</span><br /><br /><span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors<br /><span class="token comment"># => [MyClass, Greeter, …]</span></code></pre>
<p>That's harder to do with these built modules.</p>
<p>The modules built from an initializer are <em>instances</em> of <code>Greeter</code>:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors<br /><span class="token comment"># => [MyClass, #<Greeter:…>, …]</span></code></pre>
<p>It's easy to see where they come from, but we can't do <code>MyClass.new.is_a?(Greeter)</code>. We'd need something like <code>MyClass.ancestors.any? { _1.is_a?(Greeter) }</code>.</p>
<p>If we <code>extend ActiveSupport::Concern</code>, this modifies <code>Greeter</code>'s <a href="https://stackoverflow.com/a/61378747/6962">singleton class</a> and we no longer see a helpful <code><#Greeter:…></code>:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors<br /><span class="token comment"># => [MyClass, #<#<Class:…>:…>, …]</span></code></pre>
<p>And the non-initializer ones are just anonymous modules with no knowledge of whence they came:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors<br /><span class="token comment"># => [MyClass, #<Module:…>, …]</span></code></pre>
<h3>Overriding <code>inspect</code></h3>
<p>We can make things nicer by overriding <code>inspect</code>:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> define_singleton_method<span class="token punctuation">(</span><span class="token symbol">:inspect</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token string">"#<Greeter:<span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token delimiter tag">}</span></span>>"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><span class="token keyword">end</span><br /><br /><span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors<br /><span class="token comment"># => [MyClass, <#Greeter:world>, …]</span></code></pre>
<h3>Assigning a constant</h3>
<p>If we wanted to go completely overboard (and we do), we could do something like <a href="https://thepugautomatic.com/2013/07/dsom/#:~:text=Polishing%20the%20inheritance%20chain">this</a>:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string">"digest"</span><br /><br /><span class="token keyword">module</span> <span class="token constant">Greeter</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">by_name</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> module_name <span class="token operator">=</span> <span class="token string">"ByName<span class="token interpolation"><span class="token delimiter tag">#{</span><span class="token constant">Digest</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">SHA1</span><span class="token punctuation">.</span>hexdigest<span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token delimiter tag">}</span></span>"</span><br /> <span class="token keyword">return</span> const_get<span class="token punctuation">(</span>module_name<span class="token punctuation">)</span> <span class="token keyword">if</span> const_defined<span class="token operator">?</span><span class="token punctuation">(</span>module_name<span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><br /><br /> const_set<span class="token punctuation">(</span>module_name<span class="token punctuation">,</span> <span class="token builtin">Module</span><span class="token punctuation">.</span><span class="token keyword">new</span> <span class="token keyword">do</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token delimiter tag">}</span></span>!"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><span class="token punctuation">)</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyClass</span><br /> <span class="token keyword">include</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span>by_name<span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span><br /><span class="token keyword">end</span><br /><br /><span class="token constant">MyClass</span><span class="token punctuation">.</span>ancestors<br /><span class="token comment"># => [MyClass, Greeter::ByName7c211433f02071597741e6ff5a8ea34789abbf43, …]</span></code></pre>
<p>If we instead build in the initializer, we need to override <code>.new</code>:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">class</span> <span class="token class-name">Greeter</span> <span class="token operator"><</span> <span class="token builtin">Module</span><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">new</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> module_name <span class="token operator">=</span> <span class="token string">"ByName<span class="token interpolation"><span class="token delimiter tag">#{</span><span class="token constant">Digest</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">SHA1</span><span class="token punctuation">.</span>hexdigest<span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token delimiter tag">}</span></span>"</span><br /> <span class="token keyword">return</span> const_get<span class="token punctuation">(</span>module_name<span class="token punctuation">)</span> <span class="token keyword">if</span> const_defined<span class="token operator">?</span><span class="token punctuation">(</span>module_name<span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><br /><br /> const_set<span class="token punctuation">(</span>module_name<span class="token punctuation">,</span> <span class="token keyword">super</span><span class="token punctuation">)</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><br /> <span class="token keyword">define_method</span><span class="token punctuation">(</span><span class="token symbol">:greet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token string">"Hello <span class="token interpolation"><span class="token delimiter tag">#{</span>name<span class="token delimiter tag">}</span></span>!"</span> <span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>And now we can check for module identity in the usual ways:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">MyClass</span> <span class="token operator"><</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"world"</span><span class="token punctuation">)</span> <span class="token comment"># => true</span><br /><span class="token constant">MyClass</span> <span class="token operator"><</span> <span class="token constant">Greeter</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token string">"moon"</span><span class="token punctuation">)</span> <span class="token comment"># => nil</span></code></pre>
<hr />
<p><a name="footnote-1"></a></p>
<h3>Footnote 1 <a href="https://thepugautomatic.com/2023/07/ruby-module-builder-patterns/#footnote-1-source">^</a></h3>
<p>I say "a module you can mix in" because <a href="https://ruby-doc.org/3.2/Class.html"><code>Class</code></a> inherits from <a href="https://ruby-doc.org/3.2/Module.html"><code>Module</code></a>. <a href="https://ruby-doc.org/3.2/syntax/modules_and_classes_rdoc.html#label-Classes">All classes are modules</a>, but not ones you can mix in.</p>
<p>Classes are modules with extra stuff. Both hold methods and constants and can have modules mixed into them. Classes add instantiation and state.</p>
<p>If you try to <code>include</code> or <code>extend</code> with a class as argument, you get a <code>TypeError</code>. They're still modules; Ruby just won't let you mix them in.</p>
<hr />
<p><a name="footnote-2"></a></p>
<h3>Footnote 2 <a href="https://thepugautomatic.com/2023/07/ruby-module-builder-patterns/#footnote-2-source">^</a></h3>
<p>A <a href="https://stackoverflow.com/questions/33762366/are-ruby-class-variables-bad">non-sensible</a> way with <a href="https://api.rubyonrails.org/v7.0.6/classes/ActiveSupport/Concern.html%60"><code>Concern</code></a> could be this 🙈:</p>
<pre class="language-ruby"><code class="language-ruby">included <span class="token punctuation">{</span> <span class="token variable">@@greeter_name</span> <span class="token operator">=</span> name <span class="token punctuation">}</span></code></pre>
Passing arguments to osascript in Raycast script commands2023-02-23T00:00:00Zhttps://thepugautomatic.com/2023/02/passing-arguments-to-osascript-in-raycast-script-commands/<p>I'm writing commands for the <a href="https://www.raycast.com/">Raycast</a> launcher.</p>
<p>It took me a while to figure out how to robustly pass arguments to AppleScript/osascript in Raycast <a href="https://github.com/raycast/script-commands">script commands</a>. This is how:</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/bin/bash</span><br /><br /><span class="token comment"># @raycast.schemaVersion 1</span><br /><span class="token comment"># @raycast.title Say something</span><br /><span class="token comment"># @raycast.mode silent</span><br /><span class="token comment"># @raycast.argument1 { "type": "text", "placeholder": "something to say" }</span><br /><br />osascript - <span class="token string">"<span class="token variable">$1</span>"</span> <span class="token operator"><<</span><span class="token string">END<br /> on run argv<br /> set arg to (item 1 of argv)<br /> say arg<br /> end<br />END</span></code></pre>
<p>We're passing the shell argument <code>$1</code> into <code>osascript</code>, where it in turn becomes the first <code>argv</code> argument.</p>
<p>If we had just interpolated <code>$1</code> directly into the AppleScript like <code>say "$1"</code>, we would effectively run an injection attack on ourselves. It would work for simple input like "hello" but would break on input like 'hello "world"'.</p>
<p>UPDATE:</p>
<p>I've since realised you can write AppleScript directly:</p>
<pre class="language-applescript"><code class="language-applescript"><span class="token comment">#!/usr/bin/env osascript</span><br /><br /><span class="token comment"># …</span><br /><br /><span class="token keyword">on</span> run argv<br /> <span class="token keyword">set</span> arg <span class="token keyword">to</span> <span class="token punctuation">(</span>item <span class="token number">1</span> <span class="token keyword">of</span> argv<span class="token punctuation">)</span><br /> say arg<br /><span class="token keyword">end</span></code></pre>
<p>I'll let the post stand – it is still useful for doing bits of AppleScript inside a shell script.</p>
Write self-deprecating comments2021-02-27T00:00:00Zhttps://thepugautomatic.com/2021/02/write-self-deprecating-comments/<p>Comments and code easily get out of sync, but there are tricks to lessen the impact.</p>
<p>Instead of</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">PaymentAPI</span><span class="token punctuation">.</span>call<span class="token punctuation">(</span><br /> mode<span class="token punctuation">:</span> <span class="token string">"X"</span><span class="token punctuation">,</span> <span class="token comment"># Disable 3D Secure verification.</span><br /> timeout<span class="token punctuation">:</span> <span class="token number">12</span><span class="token punctuation">,</span> <span class="token comment"># The smallest value that avoids errors.</span><br /><span class="token punctuation">)</span></code></pre>
<p>, write</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">PaymentAPI</span><span class="token punctuation">.</span>call<span class="token punctuation">(</span><br /> mode<span class="token punctuation">:</span> <span class="token string">"X"</span><span class="token punctuation">,</span> <span class="token comment"># "X": Disable 3D Secure verification.</span><br /> timeout<span class="token punctuation">:</span> <span class="token number">12</span><span class="token punctuation">,</span> <span class="token comment"># 12 secs is the smallest value that avoids errors.</span><br /><span class="token punctuation">)</span></code></pre>
<p>This double-entry bookkeeping means you can easily tell when the code and comment drift apart:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token constant">PaymentAPI</span><span class="token punctuation">.</span>call<span class="token punctuation">(</span><br /> mode<span class="token punctuation">:</span> <span class="token string">"Y"</span><span class="token punctuation">,</span> <span class="token comment"># "X": Disable 3D Secure verification.</span><br /> timeout<span class="token punctuation">:</span> <span class="token number">15</span><span class="token punctuation">,</span> <span class="token comment"># 12 secs is the smallest value that avoids errors.</span><br /><span class="token punctuation">)</span></code></pre>
<p>Whether the discrepancy is caught immediately by the author, or in review, or by another developer far down the line, it will be explicitly clear that the comment was not intended for the current value.</p>
<p>This technique is a great fit for short-and-cryptic values like these. Longer values would be annoying to repeat, but also tend to be more self-documenting.</p>
Using "load" with "any?" to avoid double queries from Active Record2021-02-18T00:00:00Zhttps://thepugautomatic.com/2021/02/using-load-with-any-to-avoid-double-queries-from-activerecord/<p>It's common to only list records if there are any – something like this:</p>
<div class="code-filename">index.html.erb</div>
<pre class="language-erb"><code class="language-erb"><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">if</span> <span class="token variable">@items</span><span class="token punctuation">.</span>any<span class="token operator">?</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span><br /> <span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token variable">@items</span><span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>item<span class="token operator">|</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> item<span class="token punctuation">.</span>title <span class="token delimiter punctuation">%></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><br /> <span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">end</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span><br /><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">else</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>No items!<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">end</span> <span class="token delimiter punctuation">%></span></span></code></pre>
<p>As you may be currently shouting at your screen, this specific implementation is not optimal. The <code>any?</code> will trigger one query, and <code>@items.each</code> will trigger another.</p>
<p>We can verify this in a Rails console:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token operator">></span><span class="token operator">></span> items <span class="token operator">=</span> <span class="token constant">Item</span><span class="token punctuation">.</span>where<span class="token punctuation">(</span><span class="token string">"false"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span>any<span class="token operator">?</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span><span class="token keyword">each</span><span class="token punctuation">(</span><span class="token operator">&</span><span class="token symbol">:id</span><span class="token punctuation">)</span><br /> <span class="token constant">Item</span> <span class="token constant">Exists</span> <span class="token punctuation">(</span><span class="token number">1.2</span>ms<span class="token punctuation">)</span> <span class="token constant">SELECT</span> <span class="token number">1</span> <span class="token constant">AS</span> one <span class="token constant">FROM</span> <span class="token string">"items"</span> <span class="token constant">WHERE</span> <span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token constant">LIMIT</span> <span class="token number">1</span><br /> <span class="token constant">Item</span> <span class="token constant">Load</span> <span class="token punctuation">(</span><span class="token number">12.3</span>ms<span class="token punctuation">)</span> <span class="token constant">SELECT</span> <span class="token string">"items"</span><span class="token punctuation">.</span><span class="token operator">*</span> <span class="token constant">FROM</span> <span class="token string">"items"</span> <span class="token constant">WHERE</span> <span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span></code></pre>
<p>Other than the performance implications of running two queries where one would do, there's also a (usually low) risk of timing issues where the <code>any?</code> is true, then the records are destroyed, and the second query comes back empty, rendering an empty list.</p>
<p><a href="https://api.rubyonrails.org/v6.1/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-empty-3F">The docs</a> currently recommend using <code>length</code>, e.g. like this:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token operator">></span><span class="token operator">></span> items <span class="token operator">=</span> <span class="token constant">Item</span><span class="token punctuation">.</span>where<span class="token punctuation">(</span><span class="token string">"false"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span>length<span class="token punctuation">.</span>nonzero<span class="token operator">?</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span><span class="token keyword">each</span><span class="token punctuation">(</span><span class="token operator">&</span><span class="token symbol">:id</span><span class="token punctuation">)</span><br /> <span class="token constant">Item</span> <span class="token constant">Load</span> <span class="token punctuation">(</span><span class="token number">12.3</span>ms<span class="token punctuation">)</span> <span class="token constant">SELECT</span> <span class="token string">"items"</span><span class="token punctuation">.</span><span class="token operator">*</span> <span class="token constant">FROM</span> <span class="token string">"items"</span> <span class="token constant">WHERE</span> <span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span></code></pre>
<p>This does get us a single query, but at what aesthetic cost? <code>@items.length.nonzero?</code> or <code>@items.length > 0</code> lacks the elegance of <code>@items.any?</code>. Surely the framework that gave us <a href="https://api.rubyonrails.org/v6.1/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-forty_two"><code>#forty_two</code></a> can do better!</p>
<p>So <a href="https://www.calleluks.com/">Calle</a> and I looked around and came up with something that gets us both:</p>
<div class="code-filename">index.html.erb</div>
<pre class="language-erb"><code class="language-erb"><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">if</span> <span class="token variable">@items</span><span class="token punctuation">.</span>load<span class="token punctuation">.</span>any<span class="token operator">?</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span><br /> <span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token variable">@items</span><span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>item<span class="token operator">|</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> item<span class="token punctuation">.</span>title <span class="token delimiter punctuation">%></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><br /> <span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">end</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span><br /><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">else</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>No items!<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /><span class="token erb language-erb"><span class="token delimiter punctuation"><%</span> <span class="token keyword">end</span> <span class="token delimiter punctuation">%></span></span></code></pre>
<p>By just sneaking a <a href="https://api.rubyonrails.org/v6.1/classes/ActiveRecord/Relation.html#method-i-load"><code>load</code></a> in there, we cause the <code>@items</code> collection to be loaded from the database. And <a href="https://api.rubyonrails.org/v6.1/classes/ActiveRecord/Relation.html#method-i-any-3F"><code>any?</code></a> is implemented (via <a href="https://api.rubyonrails.org/v6.1/classes/ActiveRecord/Relation.html#method-i-empty-3F"><code>empty?</code></a>) not to fire off an extra query if the collection has already been loaded.</p>
<p>We can confirm in the console that it generates a single query:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token operator">></span><span class="token operator">></span> items <span class="token operator">=</span> <span class="token constant">Item</span><span class="token punctuation">.</span>where<span class="token punctuation">(</span><span class="token string">"false"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span>load<span class="token punctuation">.</span>any<span class="token operator">?</span><span class="token punctuation">;</span> items<span class="token punctuation">.</span><span class="token keyword">each</span><span class="token punctuation">(</span><span class="token operator">&</span><span class="token symbol">:id</span><span class="token punctuation">)</span><br /> <span class="token constant">Item</span> <span class="token constant">Load</span> <span class="token punctuation">(</span><span class="token number">12.3</span>ms<span class="token punctuation">)</span> <span class="token constant">SELECT</span> <span class="token string">"items"</span><span class="token punctuation">.</span><span class="token operator">*</span> <span class="token constant">FROM</span> <span class="token string">"items"</span> <span class="token constant">WHERE</span> <span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span></code></pre>
<p>Alternatively, one could load the records in the controller, of course:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token variable">@items</span> <span class="token operator">=</span> <span class="token constant">Item</span><span class="token punctuation">.</span>some_scope<span class="token punctuation">.</span>load</code></pre>
<p>A potential downside to doing it in the controller is that it still <em>looks</em> in the view like it will trigger an extra query, if you're used to this Rails gotcha…</p>
<p>Anyway, that's it!</p>
<p>UPDATE: After writing this, I found <a href="https://www.speedshop.co/2019/01/10/three-activerecord-mistakes.html">an excellent post by Nate Berkopec</a> that covers a bunch of options (including this one!) in depth, with tables and everything. Read it too!</p>
Systematically removing code2020-11-08T00:00:00Zhttps://thepugautomatic.com/2020/11/systematically-removing-code/<p>It's easy to miss things when removing code, leaving behind unused methods, templates, CSS classes or translation keys. (Especially in a dynamic language like Ruby, without a compiler to help you spot dead code.)</p>
<p>I avoid this by removing code systematically, line by line, depth-first.</p>
<p>This is one of those things that seems obvious when you do it, but in my experience, many people do it haphazardly.</p>
<p>Say we wanted to remove the "item box" from this page:</p>
<div class="code-filename">page.html.erb</div>
<pre class="language-erb"><code class="language-erb"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>Welcome to my page!<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /><br /><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> render<span class="token punctuation">(</span><span class="token string">"item_box"</span><span class="token punctuation">,</span> item<span class="token punctuation">:</span> item<span class="token punctuation">)</span> <span class="token delimiter punctuation">%></span></span></code></pre>
<div class="code-filename">_item_box.html.erb</div>
<pre class="language-erb"><code class="language-erb"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>box box--fancy<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> item<span class="token punctuation">.</span>title <span class="token delimiter punctuation">%></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span><br /> <span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> format_description<span class="token punctuation">(</span>item<span class="token punctuation">.</span>description<span class="token punctuation">)</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> <span class="token constant">I18n</span><span class="token punctuation">.</span>translate<span class="token punctuation">(</span><span class="token string">"my.translation.key"</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>So our end goal is to remove the <code><%= render("item_box", item: item) %></code> line.</p>
<p>First, we search the project to check that <code>_item_box.html.erb</code> isn't used somewhere else, or referenced in docs that we'll need to update. It isn't, so we're OK to remove it – but before we do that, we must go through it line by line.</p>
<p>The first line is <code><div class="box box--fancy"></code>. So we search the project for these two CSS classes, checking if they're in use somewhere else. If not, we remove them from the CSS files.</p>
<p>We go deeper if required – perhaps the CSS for <code>.box--fancy</code> uses a CSS variable. Then we check if that variable is in use elsewhere. <a href="https://thepugautomatic.com/2014/03/stacked-vim-searches-down-cold/">Stacked searches in Vim</a> are helpful here.</p>
<p>Once we've checked a line in the file, we delete that line. This helps us keep track of what we've already checked.</p>
<p>So after we've checked and removed that line, we're left with</p>
<div class="code-filename">_item_box.html.erb</div>
<pre class="language-erb"><code class="language-erb"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> item<span class="token punctuation">.</span>title <span class="token delimiter punctuation">%></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span><br /> <span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> format_description<span class="token punctuation">(</span>item<span class="token punctuation">.</span>description<span class="token punctuation">)</span> <span class="token delimiter punctuation">%></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span><span class="token erb language-erb"><span class="token delimiter punctuation"><%=</span> <span class="token constant">I18n</span><span class="token punctuation">.</span>translate<span class="token punctuation">(</span><span class="token string">"my.translation.key"</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>And we continue this way, line by line. Is the <code>item.title</code> used elsewhere? If not, we should probably remove it, too. What about <code>format_description</code>, <code>item.description</code>, the <code>my.translation.key</code> translation key?</p>
<p>Again, we go deeper if required, not removing the <code>format_description</code> method until we've gone through <em>it</em> line by line.</p>
<p>When we've looked at every line in <code>_item_box.html.erb</code> and deleted them as we went, the file will be empty, and we can start popping the stack.</p>
<p>We remove the empty <code>_item_box.html.erb</code> file.</p>
<p>And we can finally remove the <code><%= render("item_box", item: item) %></code> line, fairly confident that we didn't leave dead code behind.</p>
<p>This probably sounds more tedious than it is. It tends to be quick work, and you can take shortcuts – removing a swathe of lines that don't reference anything else, or that only call methods that you know are used elsewhere.</p>
Scheduled Roborock cleaning with a prompt using iOS Shortcuts2020-09-28T00:00:00Zhttps://thepugautomatic.com/2020/09/scheduled-roborock-cleaning-with-a-prompt-using-shortcuts/<p>You can configure a <a href="http://roborock.com/">Roborock</a> robot vacuum to run automatically on a schedule.</p>
<p>I don't want that – I want to move some things out of the way first, and check that the cats haven't left us any surprises.</p>
<p>But I like the idea of a <em>semi-automatic</em> schedule, where the vacuum prompts me at a given time, and then waits for me to give the go-ahead.</p>
<p>I realised this can be achieved with <a href="https://support.apple.com/en-gb/guide/shortcuts">iOS Shortcuts</a>.</p>
<p>It's not quite a one-tap prompt – your phone will show a notification saying your automation is running, and then one saying "Tap to respond".</p>
<p><img src="https://thepugautomatic.com/images/content/2020-09-28/respond.png" alt="Screenshot" /></p>
<p>When you tap it, it shows the prompt. (This is good, really, since it lets you wrap up whatever you were doing instead of forcing you to answer the prompt immediately.)</p>
<p><img src="https://thepugautomatic.com/images/content/2020-09-28/prompt.png" alt="Screenshot" /></p>
<p>And when you've tapped "OK", the vacuum's Xiaomi Home app will tell you it ran, which you get to discard.</p>
<p><img src="https://thepugautomatic.com/images/content/2020-09-28/ran.png" alt="Screenshot" /></p>
<h2>Adding a Siri shortcut</h2>
<p>First we make it possible for Siri (and thus Shortcuts) to start the vacuum at all.</p>
<p>In the vacuum's "Xiaomi Home" app, go to the top-level "Automation" tab.</p>
<p>Press "+", select "Complete manually", select your vacuum and the "Start cleaning" action. Save.</p>
<p>Now tap "Add to Siri" until you get to the system's "Add to Siri" screen.</p>
<p>The shortcut name you specify here doesn't matter for the automation we will be adding, but if you plan on triggering it from Siri as well, pick something suitable. Then press the "Add to Siri" button.</p>
<p>You should now be able to trigger your shortcut by saying "Hey Siri, start cleaning" or whatever phrase you picked.</p>
<h2>Automate the shortcut</h2>
<p>Now open the "Shortcuts" app. You may need to <a href="https://apps.apple.com/app/shortcuts/id915249334">install it</a> first.</p>
<p>Under the top-level "Automation" tab, click "+".</p>
<p>Select "Create Personal Automation".</p>
<p>Pick "Time of Day", then specify whatever suits you – daily, weekly (some given days each week), or monthly (some given days each month).</p>
<p>Now "Add Action". Use the search functionality to find "Show Alert". Tap it to add it.</p>
<p>Modify the text (tap it, then use the keyboard) to whatever prompt you like.</p>
<p>Now tap "+" to add another step. When the automation runs, it will only proceed to this next step if you selected "OK" at the prompt.</p>
<p>Now search for "Xiaomi" and tap the action we added before, e.g. "Run scene 'Start cleaning'".</p>
<p>Press "Next". Uncheck "Ask Before Running" unless you want prompts while you prompt, dawg.</p>
<p>Press "Done" – and that's it!</p>
<p>If you're impatient to see that it works, edit it to run very soon, then edit it back to regular scheduling.</p>
Communicating between LiveViews on the same page2020-08-01T00:00:00Zhttps://thepugautomatic.com/2020/08/communicating-between-liveviews-on-the-same-page/<p>If you have multiple LiveViews on the same page, it's perhaps not obvious how they can communicate with one another. This post describes a few ways.</p>
<p>First off, consider whether you want to use multiple LiveViews at all, or if a single LiveView containing components would be more suitable. (In <a href="https://github.com/barsoom/ex-remit">my case</a>, I went with multiple LiveViews so each could be more self-contained, with its own timers and so on.)</p>
<p>I'm using LiveView 0.14.4.</p>
<h2>Parent to child via <code>:session</code> and <code>:id</code></h2>
<p>A parent LiveView <a href="http://hexdocs.pm/phoenix_live_view/0.14.3/Phoenix.LiveView.Helpers.html#live_render/3">can pass "session" data</a> to a child LiveView:</p>
<div class="code-filename">lib/my_app_web/live/parent_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">def</span> render<span class="token punctuation">(</span>assigns<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> ~L<span class="token string">"""<br /> <%= live_render @socket, MyAppWeb.ChildLive,<br /> id: :child,<br /> session: %{"hello" => @world}<br /> %><br /> """</span><br /><span class="token keyword">end</span></code></pre>
<p>But it's only passed once, when the child is mounted. If the <code>world</code> assign is later changed in the parent, the child won't update automatically.</p>
<p>We can fix this by including the assign in the child's ID:</p>
<div class="code-filename">lib/my_app_web/live/parent_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">def</span> render<span class="token punctuation">(</span>assigns<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> ~L<span class="token string">"""<br /> <%= live_render @socket, MyAppWeb.ChildLive,<br /> id: "child_<span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token attribute variable">@world</span><span class="token delimiter punctuation">}</span></span>",<br /> session: %{"hello" => @world}<br /> %><br /> """</span><br /><span class="token keyword">end</span></code></pre>
<p>Now, whenever the ID changes, the child will be unmounted and remounted.</p>
<p>A downside to remounting is that all of the child's state will be reset – we're not just updating the <code>world</code> value and leaving everything else as-is.</p>
<p>Note that you may need to do <code>id: "child_#{inspect(@some_assign)}"</code> <a href="https://thepugautomatic.com/2016/01/elixir-string-interpolation-for-the-rubyist">depending on its type</a>.</p>
<h2>Child to parent via <code>send</code></h2>
<p>Because each LiveView is a process, you can <code>send</code> messages between them, as long as you know the PID.</p>
<p>Conveniently, the socket contains the <code>parent_pid</code>, so sending a message from a child LiveView to its parent LiveView is easy:</p>
<div class="code-filename">lib/my_app_web/live/child_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token comment"># Let's assume this is triggered by clicking some link.</span><br /><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_event<span class="token punctuation">(</span><span class="token string">"say_hello_to_parent"</span><span class="token punctuation">,</span> _params<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> send<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>parent_pid<span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> <span class="token string">"world"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<div class="code-filename">lib/my_app_web/live/parent_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_info<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> message<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> IO<span class="token punctuation">.</span>inspect message<br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<p>There is also a <code>root_pid</code> to access the root LiveView, if they're nested more deeply.</p>
<p>Both <code>parent_pid</code> and <code>root_pid</code> are <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Socket.html#t:t/0">documented in the typespec</a>, which means it's fine to rely on them – they're part of the public API.</p>
<h2>Parent to child via <code>send</code></h2>
<p>The socket doesn't include child PIDs out of the box, but we can have children send their PIDs to the parent on <a href="http://hexdocs.pm/phoenix_live_view/0.14.3/Phoenix.LiveView.html#connected?/1">connected mount</a>:</p>
<div class="code-filename">lib/my_app_web/live/child_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> mount<span class="token punctuation">(</span>_params<span class="token punctuation">,</span> _session<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> <span class="token keyword">if</span> connected?<span class="token punctuation">(</span>socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> send<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>parent_pid<span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token atom symbol">:child_pid</span><span class="token punctuation">,</span> self<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>And the parent can store it:</p>
<div class="code-filename">lib/my_app_web/live/parent_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_info<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token atom symbol">:child_pid</span><span class="token punctuation">,</span> pid<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">child_pid:</span> pid<span class="token punctuation">)</span><span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<p>Now the parent can send messages to the child:</p>
<div class="code-filename">lib/my_app_web/live/parent_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token comment"># Let's assume this is triggered by clicking some link.</span><br /><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_event<span class="token punctuation">(</span><span class="token string">"say_hello_to_child"</span><span class="token punctuation">,</span> _params<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> send<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>assigns<span class="token punctuation">.</span>child_pid<span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> <span class="token string">"world"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<div class="code-filename">lib/my_app_web/live/child_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_info<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> message<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> IO<span class="token punctuation">.</span>inspect message<br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<p>Be mindful of the timing here – some callbacks in the parent (like <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_params/3"><code>handle_params</code></a>) may happen before the child PID is known.</p>
<p>If you try to <code>send</code> to a child PID after it has been unmounted, it will silently do nothing. (Just like <code>send</code>ing to any PID where the process is no longer alive.)</p>
<h2>Sibling to sibling with a shared ancestor via <code>send</code></h2>
<p>What about two sibling LiveViews?</p>
<p>If they share an ancestor LiveView, we can use a variation on the previous technique:</p>
<p>The children send their PIDs to their parent or the root, which stores them.</p>
<p>Child 1 can then send a payload like <code>{:tell_child_2, {:hello, "world"}}</code> for the parent or root to pass on:</p>
<div class="code-filename">lib/my_app_web/live/parent_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_info<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token atom symbol">:tell_child_2</span><span class="token punctuation">,</span> message<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> send<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>assigns<span class="token punctuation">.</span>child_2_pid<span class="token punctuation">,</span> message<span class="token punctuation">)</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<div class="code-filename">lib/my_app_web/live/child_2_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_info<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> message<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> IO<span class="token punctuation">.</span>inspect message<br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<h2>Anything with a shared root LiveView via PubSub</h2>
<p>Alternatively, we can use <a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html">PubSub</a> to communicate between anything on the same page (whether siblings, ancestor/descendant, or cousins twice removed), as long as they have a shared root LiveView.</p>
<p>See <a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html">the PubSub docs</a> for how to set it up. At the time of writing, you just need to add it to your supervision tree.</p>
<p>For the purposes of this blog post, we will restrict PubSub to updates within the current page. If you want to send some update for every user (e.g. new messages in a chat room), or every tab/window opened by the current user, PubSub can do that too.</p>
<p>We'll use the socket's <code>root_pid</code> in the PubSub topic as a way of uniquely identifying the current page. Two sibling LiveViews without a shared ancestor will each have their own PID as their <code>root_pid</code>, so this wouldn't work.</p>
<p>Anyone who wants to receive messages can subscribe on connected mount, and set up a handler:</p>
<div class="code-filename">lib/my_app_web/live/child_1_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> mount<span class="token punctuation">(</span>_params<span class="token punctuation">,</span> _session<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> <span class="token keyword">if</span> connected?<span class="token punctuation">(</span>socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> Phoenix<span class="token punctuation">.</span>PubSub<span class="token punctuation">.</span>subscribe<span class="token punctuation">(</span>MyApp<span class="token punctuation">.</span>PubSub<span class="token punctuation">,</span> <span class="token string">"page_<span class="token interpolation"><span class="token delimiter punctuation">#{</span>inspect<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>root_pid<span class="token punctuation">)</span><span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">)</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br /><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_info<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> message<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> IO<span class="token punctuation">.</span>inspect message<br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<p>Then any other LiveView with the same <code>root_pid</code> can send messages:</p>
<div class="code-filename">lib/my_app_web/live/child_2_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token comment"># Let's assume this is triggered by clicking some link.</span><br /><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> handle_event<span class="token punctuation">(</span><span class="token string">"say_hello_to_page"</span><span class="token punctuation">,</span> _params<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> Phoenix<span class="token punctuation">.</span>PubSub<span class="token punctuation">.</span>broadcast_from!<span class="token punctuation">(</span>MyApp<span class="token punctuation">.</span>PubSub<span class="token punctuation">,</span> self<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"page_<span class="token interpolation"><span class="token delimiter punctuation">#{</span>inspect<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>root_pid<span class="token punctuation">)</span><span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token atom symbol">:hello</span><span class="token punctuation">,</span> <span class="token string">"world"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> socket<span class="token punctuation">}</span><br /><span class="token keyword">end</span></code></pre>
<p>(By using <a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html#broadcast_from!/5"><code>broadcast_from!/5</code></a> rather than <a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html#broadcast!/4"><code>broadcast!/4</code></a>, the sending process won't itself receive the broadcast even if it's a subscriber.)</p>
<p>You could probably use <a href="https://hexdocs.pm/elixir/Process.html#register/2">the process registry</a> instead of PubSub, but process registry names must be atoms, which aren't garbage collected, so it isn't advisable – each page would use a bit more memory that is never reclaimed, and you might eventually hit <a href="https://til.hashrocket.com/posts/b9giaqz4lc-current-number-of-atoms-in-the-atoms-table">the atom limit</a>. Also, unlike PubSub, this process registry only works on the local node.</p>
<p>I assume that PubSub will use more resources than just relying on <code>send</code>, especially on a high-traffic site, since it keeps track of subscribers, but I don't have the numbers. If you measure it, let me know.</p>
<h2>Anything (without needing a shared root) via PubSub</h2>
<p>If you have a user ID or session ID, you could use that with PubSub instead of the <code>root_pid</code>… but if the same user has multiple tabs or windows open in the same browser, all those windows would be affected – not just the current one.</p>
<p>To target only the current page, you could generate a unique per-page identifier and use that in the PubSub topic:</p>
<div class="code-filename">lib/my_app_web/controllers/my_controller.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">def</span> show<span class="token punctuation">(</span>conn<span class="token punctuation">,</span> _params<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> <span class="token comment"># Assuming you use Ecto.</span><br /> page_id <span class="token operator">=</span> Ecto<span class="token punctuation">.</span>UUID<span class="token punctuation">.</span>generate<span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> render<span class="token punctuation">(</span>conn<span class="token punctuation">,</span> <span class="token atom symbol">:show</span><span class="token punctuation">,</span> <span class="token attr-name">page_id:</span> page_id<span class="token punctuation">)</span><br /><span class="token keyword">end</span></code></pre>
<div class="code-filename">lib/my_app_web/templates/my/show.html.eex</div>
<pre class="language-elixir"><code class="language-elixir"><<span class="token punctuation">%</span><span class="token operator">=</span> live_render <span class="token attribute variable">@conn</span><span class="token punctuation">,</span> MyAppWeb<span class="token punctuation">.</span>OneLive<span class="token punctuation">,</span> <span class="token attr-name">session:</span> <span class="token punctuation">%</span><span class="token punctuation">{</span><span class="token string">"page_id"</span> <span class="token operator">=></span> <span class="token attribute variable">@page_id</span><span class="token punctuation">}</span> <span class="token punctuation">%</span><span class="token operator">></span><br /><span class="token operator"><</span><span class="token punctuation">%</span><span class="token operator">=</span> live_render <span class="token attribute variable">@conn</span><span class="token punctuation">,</span> MyAppWeb<span class="token punctuation">.</span>TwoLive<span class="token punctuation">,</span> <span class="token attr-name">session:</span> <span class="token punctuation">%</span><span class="token punctuation">{</span><span class="token string">"page_id"</span> <span class="token operator">=></span> <span class="token attribute variable">@page_id</span><span class="token punctuation">}</span> <span class="token punctuation">%</span><span class="token operator">></span></code></pre>
<div class="code-filename">lib/my_app_web/live/one_live.ex</div>
<pre class="language-elixir"><code class="language-elixir"><span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /><span class="token keyword">def</span> mount<span class="token punctuation">(</span>_params<span class="token punctuation">,</span> <span class="token punctuation">%</span><span class="token punctuation">{</span><span class="token string">"page_id"</span> <span class="token operator">=></span> page_id<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> <span class="token keyword">if</span> connected?<span class="token punctuation">(</span>socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> Phoenix<span class="token punctuation">.</span>PubSub<span class="token punctuation">.</span>subscribe<span class="token punctuation">(</span>MyApp<span class="token punctuation">.</span>PubSub<span class="token punctuation">,</span> <span class="token string">"page_<span class="token interpolation"><span class="token delimiter punctuation">#{</span>page_id<span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">)</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>And so on.</p>
<h2>That's it!</h2>
<p>I'll leave it to the reader to determine which of these techniques, if any, is most suitable for your use case.</p>
<p>As always, I'm very happy to receive feedback either in the comments or <a href="https://twitter.com/henrik/status/1289690442095833089">on Twitter</a>!</p>
Optimising data-over-the-wire in Phoenix LiveView2020-07-11T00:00:00Zhttps://thepugautomatic.com/2020/07/optimising-data-over-the-wire-in-phoenix-liveview/<p><a href="https://github.com/phoenixframework/phoenix_live_view">Phoenix LiveView</a> distinguishes itself from other "server-side reactive" frameworks<a href="https://thepugautomatic.com/2020/07/optimising-data-over-the-wire-in-phoenix-liveview/#footnote" class="footnote-link">¹</a> by automatically sending minimal diffs over the wire. (That is to say, over a WebSocket.)</p>
<p>Well, mostly automatically. The size of those diffs is affected by how you write your app.</p>
<p>I tried three different ways and compared the amount of data sent over the wire: the naive approach, using <code>temporary_assigns</code>, and using components.</p>
<p>In these examples, we have a toy app that lists items numbered 1 through 300, with a button on each to replace it with a random new number.</p>
<p><img src="https://thepugautomatic.com/images/content/2020-07-11/randomise.png" alt="Screenshot of the toy app" /></p>
<p>I'm using LiveView 0.14.1 and looking at the WebSocket data using Chrome's Web Inspector.</p>
<p><img src="https://thepugautomatic.com/images/content/2020-07-11/inspector.png" alt="Screenshot of WebSocket data in Web Inspector" /></p>
<p>Please verify this information if you're on another version of LiveView – things are moving fast.</p>
<h2>1. The naive approach</h2>
<p>This might be described as the naive approach – simply looping over a list of items.</p>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">defmodule</span> MyAppWeb<span class="token punctuation">.</span>NaiveLive <span class="token keyword">do</span><br /> <span class="token keyword">use</span> Phoenix<span class="token punctuation">.</span>LiveView<br /><br /> <span class="token keyword">defmodule</span> Item <span class="token keyword">do</span><br /> <span class="token keyword">defstruct</span> <span class="token punctuation">[</span><span class="token atom symbol">:id</span><span class="token punctuation">,</span> <span class="token atom symbol">:name</span><span class="token punctuation">]</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> render<span class="token punctuation">(</span>assigns<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> ~L<span class="token string">"""<br /> <%= for item <- @items do %><br /> <p id="item-<%= item.id %>"><br /> <%= item.name %><br /> <button phx-click="randomise" phx-value-id="<%= item.id %>"><br /> Randomise<br /> </button><br /> </p><br /> <% end %><br /> """</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> mount<span class="token punctuation">(</span>_params<span class="token punctuation">,</span> _session<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> items <span class="token operator">=</span> Enum<span class="token punctuation">.</span>map<span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">..</span><span class="token number">300</span><span class="token punctuation">,</span> <span class="token keyword">fn</span> i <span class="token operator">-></span><br /> <span class="token punctuation">%</span>Item<span class="token punctuation">{</span><span class="token attr-name">id:</span> i<span class="token punctuation">,</span> <span class="token attr-name">name:</span> <span class="token string">"Item <span class="token interpolation"><span class="token delimiter punctuation">#{</span>i<span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><span class="token punctuation">)</span><br /><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">items:</span> items<span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> handle_event<span class="token punctuation">(</span><span class="token string">"randomise"</span><span class="token punctuation">,</span> <span class="token punctuation">%</span><span class="token punctuation">{</span><span class="token string">"id"</span> <span class="token operator">=></span> id<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> id <span class="token operator">=</span> String<span class="token punctuation">.</span>to_integer<span class="token punctuation">(</span>id<span class="token punctuation">)</span><br /><br /> items <span class="token operator">=</span> Enum<span class="token punctuation">.</span>map<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>assigns<span class="token punctuation">.</span>items<span class="token punctuation">,</span> <span class="token keyword">fn</span> item <span class="token operator">-></span><br /> <span class="token keyword">if</span> item<span class="token punctuation">.</span>id <span class="token operator">==</span> id <span class="token keyword">do</span><br /> <span class="token punctuation">%</span><span class="token punctuation">{</span>item <span class="token operator">|</span> <span class="token attr-name">name:</span> <span class="token string">"Item <span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token atom symbol">:rand</span><span class="token punctuation">.</span>uniform<span class="token punctuation">(</span><span class="token number">999</span><span class="token punctuation">)</span><span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">}</span><br /> <span class="token keyword">else</span><br /> item<br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><span class="token punctuation">)</span><br /><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">items:</span> items<span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>The first-render message payload is 7431 bytes (7.4 KB). The bulk of that is the <code>id</code> attribute, name and <code>phx-value-id</code> of each item:</p>
<blockquote>
<p>["4","4","lv:phx-FiCy2Z_WdisCVxBD","phx_reply",{"response":{"rendered":{"0":{"d":[["1","Item 1","1"],<span class="truncated">…truncated…</span>,["300","Item 300","300"]],"s":["\n <p id="item-","">\n ","\n <button phx-click="randomise" phx-value-id="","">\n Randomise\n \n </p>\n"]},"s":["","\n"]}},"status":"ok"}]<p></p>
</blockquote>
<p>When I click "Randomise" on Item 50, the update message is 7274 bytes – so almost the same size as the initial message:</p>
<blockquote>
<p>["4","5","lv:phx-FiCy2Z_WdisCVxBD","phx_reply",{"response":{"diff":{"0":{"d":[["1","Item 1","1"],<span class="truncated">…truncated…</span>,["300","Item 300","300"]]}}},"status":"ok"}]</p>
</blockquote>
<p>It doesn't need to send the "statics" again (the non-dynamic parts that are the same for every item), but it re-renders and re-sends all the dynamic parts.</p>
<p>And of course this grows linearly – with 3000 items instead of 300, both payloads are about 10 times bigger.</p>
<h2>2. Temporary assigns</h2>
<p><a href="https://hexdocs.pm/phoenix_live_view/dom-patching.html">Temporary assigns</a> is a way of optimising both the amount of data transferred and the memory used in each LiveView process. (Every user gets their own process – one per LiveView on the page.)</p>
<p>With this approach, we'll send all 300 items on the first render, and then the LiveView process stops storing them.</p>
<p>When we update an item, we only re-render that single item on the backend, and only send that diff in the update message to the frontend.</p>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">defmodule</span> MyAppWeb<span class="token punctuation">.</span>TempLive <span class="token keyword">do</span><br /> <span class="token keyword">use</span> Phoenix<span class="token punctuation">.</span>LiveView<br /><br /> <span class="token keyword">defmodule</span> Item <span class="token keyword">do</span><br /> <span class="token keyword">defstruct</span> <span class="token punctuation">[</span><span class="token atom symbol">:id</span><span class="token punctuation">,</span> <span class="token atom symbol">:name</span><span class="token punctuation">]</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> render<span class="token punctuation">(</span>assigns<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> ~L<span class="token string">"""<br /> <div id="list" phx-update="append"><br /> <%= for item <- @items do %><br /> <p id="item-<%= item.id %>"><br /> <%= item.name %><br /> <button phx-click="randomise" phx-value-id="<%= item.id %>"><br /> Randomise<br /> </button><br /> </p><br /> <% end %><br /> </div><br /> """</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> mount<span class="token punctuation">(</span>_params<span class="token punctuation">,</span> _session<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> items <span class="token operator">=</span> Enum<span class="token punctuation">.</span>map<span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">..</span><span class="token number">300</span><span class="token punctuation">,</span> <span class="token keyword">fn</span> i <span class="token operator">-></span><br /> <span class="token punctuation">%</span>Item<span class="token punctuation">{</span><span class="token attr-name">id:</span> i<span class="token punctuation">,</span> <span class="token attr-name">name:</span> <span class="token string">"Item <span class="token interpolation"><span class="token delimiter punctuation">#{</span>i<span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><span class="token punctuation">)</span><br /><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">items:</span> items<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token attr-name">temporary_assigns:</span> <span class="token punctuation">[</span><span class="token attr-name">items:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> handle_event<span class="token punctuation">(</span><span class="token string">"randomise"</span><span class="token punctuation">,</span> <span class="token punctuation">%</span><span class="token punctuation">{</span><span class="token string">"id"</span> <span class="token operator">=></span> id<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> id <span class="token operator">=</span> String<span class="token punctuation">.</span>to_integer<span class="token punctuation">(</span>id<span class="token punctuation">)</span><br /><br /> item <span class="token operator">=</span> <span class="token punctuation">%</span>Item<span class="token punctuation">{</span><span class="token attr-name">id:</span> id<span class="token punctuation">,</span> <span class="token attr-name">name:</span> <span class="token string">"Item <span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token atom symbol">:rand</span><span class="token punctuation">.</span>uniform<span class="token punctuation">(</span><span class="token number">999</span><span class="token punctuation">)</span><span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">}</span><br /><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">items:</span> <span class="token punctuation">[</span>item<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>With temporary assigns, the initial payload is 7498 bytes (it was 7431 with the naive approach):</p>
<blockquote>
<p>["4","4","lv:phx-FiC0IVgqbVUCtxDG","phx_reply",{"response":{"rendered":{"0":{"d":[["1","Item 1","1"],<span class="truncated">…truncated…</span>,["300","Item 300","300"]],"s":["\n <p id="item-","">\n ","\n <button phx-click="randomise" phx-value-id="","">\n Randomise\n </button>\n </p>\n "]},"s":["<div id="list" phx-update="append">\n ","\n</div>\n"]}},"status":"ok"}]</p>
</blockquote>
<p>It's almost identical to the first render in the naive approach, just with some extra markup needed for updates to work with temporary assigns.</p>
<p>But now for the fun part – the update is a mere 120 bytes (it was 7274 bytes with the naive approach). Shown here in full:</p>
<blockquote>
<p>["4","8","lv:phx-FiC0IVgqbVUCtxDG","phx_reply",{"response":{"diff":{"0":{"d":[["50","Item 778","50"]]}}},"status":"ok"}]</p>
</blockquote>
<p>LiveView just sends the data for the single item we changed.</p>
<p>And again, temporary assigns also reduce the amount of memory each LiveView process uses. The archetypal example is a chat: with thousands of messages and thousands of users, storing the full list for every user could use significant memory (e.g. 100 bytes per message * 10 000 messages * 10 000 users = 10 GB).</p>
<p>But there is also a downside. Because we no longer have the full list in state, some things get more complicated.</p>
<p>If we want to show a count of chat messages, they're not always there to be counted. We'd need to run a database query, or keep a count as state and make sure to increase it every time a new message comes in.</p>
<p>And note how the naive approach was able to take the original item struct and modify it, whereas this solution can't. In this toy app, we can just build a new one with the same ID. In a real app, we might need to retrieve it from a database.</p>
<h2>3. Components</h2>
<p>Our final approach is identical to the naive approach, except that we extract each item to its own component.</p>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">defmodule</span> RemitWeb<span class="token punctuation">.</span>ComponentsLive <span class="token keyword">do</span><br /> <span class="token keyword">use</span> Phoenix<span class="token punctuation">.</span>LiveView<br /><br /> <span class="token keyword">defmodule</span> Item <span class="token keyword">do</span><br /> <span class="token keyword">defstruct</span> <span class="token punctuation">[</span><span class="token atom symbol">:id</span><span class="token punctuation">,</span> <span class="token atom symbol">:name</span><span class="token punctuation">]</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token keyword">defmodule</span> ItemComponent <span class="token keyword">do</span><br /> <span class="token keyword">use</span> Phoenix<span class="token punctuation">.</span>LiveComponent<br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> render<span class="token punctuation">(</span>assigns<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> ~L<span class="token string">"""<br /> <p id="item-<%= @id %>"><br /> <%= @item.name %><br /> <button phx-click="randomise" phx-value-id="<%= @id %>"><br /> Randomise<br /> </button><br /> </p><br /> """</span><br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> render<span class="token punctuation">(</span>assigns<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> ~L<span class="token string">"""<br /> <%= for item <- @items do %><br /> <%= live_component @socket, ItemComponent, id: item.id, item: item %><br /> <% end %><br /> """</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> mount<span class="token punctuation">(</span>_params<span class="token punctuation">,</span> _session<span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> items <span class="token operator">=</span> Enum<span class="token punctuation">.</span>map<span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">..</span><span class="token number">300</span><span class="token punctuation">,</span> <span class="token keyword">fn</span> i <span class="token operator">-></span><br /> <span class="token punctuation">%</span>Item<span class="token punctuation">{</span><span class="token attr-name">id:</span> i<span class="token punctuation">,</span> <span class="token attr-name">name:</span> <span class="token string">"Item <span class="token interpolation"><span class="token delimiter punctuation">#{</span>i<span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><span class="token punctuation">)</span><br /><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">items:</span> items<span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><br /> <span class="token attribute variable">@impl</span> <span class="token boolean">true</span><br /> <span class="token keyword">def</span> handle_event<span class="token punctuation">(</span><span class="token string">"randomise"</span><span class="token punctuation">,</span> <span class="token punctuation">%</span><span class="token punctuation">{</span><span class="token string">"id"</span> <span class="token operator">=></span> id<span class="token punctuation">}</span><span class="token punctuation">,</span> socket<span class="token punctuation">)</span> <span class="token keyword">do</span><br /> id <span class="token operator">=</span> String<span class="token punctuation">.</span>to_integer<span class="token punctuation">(</span>id<span class="token punctuation">)</span><br /><br /> items <span class="token operator">=</span> Enum<span class="token punctuation">.</span>map<span class="token punctuation">(</span>socket<span class="token punctuation">.</span>assigns<span class="token punctuation">.</span>items<span class="token punctuation">,</span> <span class="token keyword">fn</span> item <span class="token operator">-></span><br /> <span class="token keyword">if</span> item<span class="token punctuation">.</span>id <span class="token operator">==</span> id <span class="token keyword">do</span><br /> <span class="token punctuation">%</span><span class="token punctuation">{</span>item <span class="token operator">|</span> <span class="token attr-name">name:</span> <span class="token string">"Item <span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token atom symbol">:rand</span><span class="token punctuation">.</span>uniform<span class="token punctuation">(</span><span class="token number">999</span><span class="token punctuation">)</span><span class="token delimiter punctuation">}</span></span>"</span><span class="token punctuation">}</span><br /> <span class="token keyword">else</span><br /> item<br /> <span class="token keyword">end</span><br /> <span class="token keyword">end</span><span class="token punctuation">)</span><br /><br /> <span class="token punctuation">{</span><span class="token atom symbol">:noreply</span><span class="token punctuation">,</span> assign<span class="token punctuation">(</span>socket<span class="token punctuation">,</span> <span class="token attr-name">items:</span> items<span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span></code></pre>
<p>The first-render message for this one is 16 553 bytes. It was 7431 with the naive approach and about the same with temporary assigns.</p>
<p>(It used to be bigger still, but <a href="https://github.com/phoenixframework/phoenix_live_view/issues/912">preparing to write this post</a> led to some optimisations.)</p>
<p>Since the message is a bit more complex, I've prettified it:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">[</span><span class="token string">"4"</span><span class="token punctuation">,</span><span class="token string">"4"</span><span class="token punctuation">,</span><span class="token string">"lv:phx-FiDB5JJXb8yL8TpB"</span><span class="token punctuation">,</span><span class="token string">"phx_reply"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"rendered"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"0"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"d"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span>…truncated…<span class="token punctuation">,</span><span class="token punctuation">[</span><span class="token number">300</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"s"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"\n "</span><span class="token punctuation">,</span> <span class="token string">"\n"</span><span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"c"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"1"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"0"</span><span class="token operator">:</span> <span class="token string">"1"</span><span class="token punctuation">,</span><br /> <span class="token property">"1"</span><span class="token operator">:</span> <span class="token string">"Item 1"</span><span class="token punctuation">,</span><br /> <span class="token property">"2"</span><span class="token operator">:</span> <span class="token string">"1"</span><span class="token punctuation">,</span><br /> <span class="token property">"s"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"<p id=\"item-"</span><span class="token punctuation">,</span><br /> <span class="token string">"\">\n "</span><span class="token punctuation">,</span><br /> <span class="token string">"\n <button phx-click=\"randomise\" phx-value-id=\""</span><span class="token punctuation">,</span><br /> <span class="token string">"\">\n Randomise\n </button>\n</p>\n"</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"2"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"0"</span><span class="token operator">:</span> <span class="token string">"2"</span><span class="token punctuation">,</span><br /> <span class="token property">"1"</span><span class="token operator">:</span> <span class="token string">"Item 2"</span><span class="token punctuation">,</span><br /> <span class="token property">"2"</span><span class="token operator">:</span> <span class="token string">"2"</span><span class="token punctuation">,</span><br /> <span class="token property">"s"</span><span class="token operator">:</span> <span class="token number">1</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> …truncated…<span class="token punctuation">,</span><br /> <span class="token property">"300"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"0"</span><span class="token operator">:</span> <span class="token string">"300"</span><span class="token punctuation">,</span><br /> <span class="token property">"1"</span><span class="token operator">:</span> <span class="token string">"Item 300"</span><span class="token punctuation">,</span><br /> <span class="token property">"2"</span><span class="token operator">:</span> <span class="token string">"300"</span><span class="token punctuation">,</span><br /> <span class="token property">"s"</span><span class="token operator">:</span> <span class="token number">234</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"s"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">""</span><span class="token punctuation">,</span><br /> <span class="token string">"\n"</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"status"</span><span class="token operator">:</span> <span class="token string">"ok"</span><br /><span class="token punctuation">}</span><span class="token punctuation">]</span></code></pre>
<p>The reason this initial payload is bigger than the others is that components come with some additional bookkeeping.</p>
<p>I don't know the ins and outs of the format, but I think the <code>[1],…,[300]</code> list helps track components if they're reordered, moved and so on. And I assume <code>s: 1</code> means "use the same statics as in component 1". (But I have no idea why it's not <code>s: 1</code> throughout.)</p>
<p>The update clocks in at 1818 bytes. The naive approach had 7274, and temporary assigns had 120.</p>
<blockquote>
<p>["4","12","lv:phx-FiDB5JJXb8yL8TpB","phx_reply",{"response":{"diff":{"0":{"d":[[1],<span class="truncated">…truncated…</span>,[300]]},"c":{"50":{"1":"Item 450"}}}},"status":"ok"}]</p>
</blockquote>
<p>Most of the bulk is the <code>[1],…,[300]</code> list, which I again believe is there to track the order of components.</p>
<h2>Summary</h2>
<p>So which is the best option?</p>
<p>I can't recommend the naive approach. It <em>is</em> simplest, and perhaps good enough for some apps, but in most cases you want smaller update payloads. Otherwise every interaction will pay this tax, and the app may feel slow.</p>
<p>Also note that the naive approach actually <em>re-renders</em> all the items on every update, where the other approaches only re-render the part of the template needed for a single item.</p>
<p>Temporary assigns make for the smallest payloads (and memory use), but also give you more things to worry about, since the items don't remain in state.</p>
<p>The component approach comes with a bigger initial payload, but since that only happens once, I think it's usually acceptable. And it's worth noting that in a real app, there would likely be a lot more statics, and more data in each component, so the relative bulk of the <code>[1],…,[300]</code> list would be smaller.</p>
<p>The update payload is bigger than with temporary assigns, but again, in a real app with more data in each component, the difference to the naive approach would be greater, and the difference to temporary assigns would be smaller.</p>
<p>As you may suspect, I've gone with the component approach in <a href="https://github.com/barsoom/ex-remit">my app</a>, but your mileage may vary.</p>
<hr />
<div id="footnote"></div>
<h3>Footnote</h3>
<p>I believe other frameworks like <a href="https://laravel-livewire.com/">Laravel LiveWire</a> and <a href="https://stimulusreflex.com/">Stimulus Reflex</a> re-render the full page on the server and transfer the full page over the wire (Ajax or WebSocket), and then diff it on the client side.</p>
<p>LiveView's change tracking means it only re-renders the relevant parts of the template, and then (as discussed in this post) only transfers the parts that changed.</p>
<p>I don't know about LiveWire, but Stimulus Reflex lets you render just a template partial, and then target only a part of the page for updates – however, this is a more manual process than in LiveView.</p>
Connecting Ecto to a legacy database in a script2020-06-22T00:00:00Zhttps://thepugautomatic.com/2020/06/connecting-ecto-to-a-legacy-database-in-a-script/<p>I wanted to use Ecto in a script to read data from a legacy database, transform the data, and stick it in the current database.</p>
<p>The documentation focuses mostly on how to use Ecto with app-level configuration, in <code>config/config.exs</code> and friends.</p>
<p>But I didn't want to add app-level configuration for this one-off script. I just wanted to pass in the database URL.</p>
<p>After some experimentation, this works with Ecto 3.4:</p>
<div class="code-filename">priv/repo/migrate_data.exs</div>
<pre class="language-elixir"><code class="language-elixir">old_db_url <span class="token operator">=</span> System<span class="token punctuation">.</span>get_env<span class="token punctuation">(</span><span class="token string">"OLD_DB_URL"</span><span class="token punctuation">)</span> <span class="token operator">||</span> raise<span class="token punctuation">(</span><span class="token string">"Missing OLD_DB_URL!"</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">defmodule</span> OldRepo <span class="token keyword">do</span><br /> <span class="token keyword">use</span> Ecto<span class="token punctuation">.</span>Repo<span class="token punctuation">,</span><br /> <span class="token attr-name">otp_app:</span> <span class="token atom symbol">:my_app</span><span class="token punctuation">,</span><br /> <span class="token attr-name">adapter:</span> Ecto<span class="token punctuation">.</span>Adapters<span class="token punctuation">.</span>Postgres<span class="token punctuation">,</span><br /> <span class="token attr-name">read_only:</span> <span class="token boolean">true</span> <span class="token comment"># Let's be safe, if we don't need to write.</span><br /><span class="token keyword">end</span><br /><br /><span class="token comment"># This bit is completely optional.</span><br /><span class="token comment"># You can skip it and do schemaless queries.</span><br /><span class="token keyword">defmodule</span> OldItem <span class="token keyword">do</span><br /> <span class="token keyword">use</span> Ecto<span class="token punctuation">.</span>Schema<br /><br /> schema <span class="token string">"items"</span> <span class="token keyword">do</span><br /> field <span class="token atom symbol">:name</span><span class="token punctuation">,</span> <span class="token atom symbol">:string</span><br /> <span class="token comment"># and so on</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><br /><br />OldRepo<span class="token punctuation">.</span>start_link<span class="token punctuation">(</span><span class="token attr-name">url:</span> old_db_url<span class="token punctuation">,</span> <span class="token attr-name">ssl:</span> <span class="token boolean">true</span><span class="token punctuation">)</span><br /><br /><span class="token comment"># Verify it works, if you defined a schema.</span><br />IO<span class="token punctuation">.</span>inspect <span class="token attr-name">count:</span> OldRepo<span class="token punctuation">.</span>aggregate<span class="token punctuation">(</span>OldItem<span class="token punctuation">,</span> <span class="token atom symbol">:count</span><span class="token punctuation">)</span><br /><br /><span class="token comment"># Verify it works, if you're schemaless.</span><br />IO<span class="token punctuation">.</span>inspect <span class="token attr-name">count:</span> OldRepo<span class="token punctuation">.</span>aggregate<span class="token punctuation">(</span><span class="token string">"items"</span><span class="token punctuation">,</span> <span class="token atom symbol">:count</span><span class="token punctuation">)</span></code></pre>
<p>You run the script with</p>
<pre><code>OLD_DB_URL="postgres://secretsauce…" mix run priv/repo/migrate_data.exs
</code></pre>
<p>in a terminal.</p>
<p>In my case, I wanted to run the code from my local machine but against production databases. So I added a <code>new_db_url</code> and a <code>NewRepo</code> to the script as well, but used the schemas I already had in the app with that repo – e.g. <code>MyApp.Item</code>.</p>
<p>If I had been running the script in production, or wanted to copy to the development database, I could have used <code>MyApp.Repo</code> instead of creating a <code>NewRepo</code>.</p>
<p>Anyway, that's it! Not a lot of code, but took me a little while to piece together.</p>
The case for multiple anonymous function bodies2020-06-12T00:00:00Zhttps://thepugautomatic.com/2020/06/the-case-for-multiple-anonymous-function-bodies/<p>Here's a dollop of developer delight in Elixir.</p>
<p>Named functions can have multiple bodies, as you probably know:</p>
<pre class="language-elixir"><code class="language-elixir"><span class="token keyword">def</span> present_result<span class="token punctuation">(</span><span class="token atom symbol">:ok</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token attr-name">do:</span> <span class="token string">"Success!"</span><br /><span class="token keyword">def</span> present_result<span class="token punctuation">(</span><span class="token atom symbol">:error</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token attr-name">do:</span> <span class="token string">"Failure!"</span></code></pre>
<p>It's perhaps less well known that the same applies to anonymous functions. You can use this to simplify situations where you'd otherwise use <code>case</code>.</p>
<p>For example, instead of:</p>
<pre class="language-elixir"><code class="language-elixir">Enum<span class="token punctuation">.</span>each<span class="token punctuation">(</span>results<span class="token punctuation">,</span> <span class="token keyword">fn</span> <span class="token punctuation">(</span>result<span class="token punctuation">)</span> <span class="token operator">-></span><br /> <span class="token keyword">case</span> result <span class="token keyword">do</span><br /> <span class="token atom symbol">:ok</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"Success!"</span><br /> <span class="token atom symbol">:error</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"Failure!"</span><br /> <span class="token keyword">end</span><br /><span class="token keyword">end</span><span class="token punctuation">)</span></code></pre>
<p>You can do:</p>
<pre class="language-elixir"><code class="language-elixir">Enum<span class="token punctuation">.</span>each<span class="token punctuation">(</span>results<span class="token punctuation">,</span> <span class="token keyword">fn</span><br /> <span class="token atom symbol">:ok</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"Success!"</span><br /> <span class="token atom symbol">:error</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"Failure!"</span><br /><span class="token keyword">end</span><span class="token punctuation">)</span></code></pre>
<p>And you can pattern match just like in any function definition, of course. I did something like this recently:</p>
<pre class="language-elixir"><code class="language-elixir">results <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> <span class="token string">"It worked"</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> <span class="token string">"It also worked"</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:error</span><span class="token punctuation">,</span> <span class="token string">"It fell on its face"</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span><br /><br />counts <span class="token operator">=</span> Enum<span class="token punctuation">.</span>frequencies_by<span class="token punctuation">(</span>results<span class="token punctuation">,</span> <span class="token keyword">fn</span> <span class="token punctuation">{</span>status<span class="token punctuation">,</span> _count<span class="token punctuation">}</span> <span class="token operator">-></span> status <span class="token keyword">end</span><span class="token punctuation">)</span><br /><span class="token comment"># => [{:ok, 2}, {:error, 1}]</span><br /><br />Enum<span class="token punctuation">.</span>each<span class="token punctuation">(</span>counts<span class="token punctuation">,</span> <span class="token keyword">fn</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">}</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"1 success!"</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:ok</span><span class="token punctuation">,</span> count<span class="token punctuation">}</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"<span class="token interpolation"><span class="token delimiter punctuation">#{</span>count<span class="token delimiter punctuation">}</span></span> successes!"</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:error</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">}</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"1 failure!"</span><br /> <span class="token punctuation">{</span><span class="token atom symbol">:error</span><span class="token punctuation">,</span> count<span class="token punctuation">}</span> <span class="token operator">-></span> IO<span class="token punctuation">.</span>puts <span class="token string">"<span class="token interpolation"><span class="token delimiter punctuation">#{</span>count<span class="token delimiter punctuation">}</span></span> failures!"</span><br /><span class="token keyword">end</span><span class="token punctuation">)</span></code></pre>
<p>Pretty nice!</p>