Paul Hammant's blogCopyright (c) 2002 - 2015, Paul James Hammant2024-03-15T12:08:25+00:00https://paulhammant.comPaul Hammantpaul@hammant.orgThat Ruby and Groovy Language Feature2024-02-14T00:00:00+00:00https://paulhammant.com/2024/02/14/that-ruby-and-groovy-language-feature<p>“Groovy in builder-style DSL syntax that leverages closures” .. is where I was last month with GPT writing code in a particular style. And “Ruby in a builder-style DSL syntax that leverages blocks” is what it thought was the best way to describe the same for Ruby. Read on for the rabbit hole I went down with this.</p>
<p>Getting AI’s (GPT4 and Gemini) to describe this form of programming, was fun but won’t pay the bills. Here is a prexisting Ruby example using the library <code class="language-plaintext highlighter-rouge">green_shoes</code> for some context: <a href="https://raw.githubusercontent.com/Alexanderlol/GS-Calc/master/calc.rb">https://raw.githubusercontent.com/Alexanderlol/GS-Calc/master/calc.rb</a>. The code for that:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'green_shoes'</span>
<span class="no">Shoes</span><span class="p">.</span><span class="nf">app</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"My calculator"</span><span class="p">,</span> <span class="ss">width: </span><span class="mi">200</span><span class="p">,</span> <span class="ss">height: </span><span class="mi">240</span><span class="p">)</span> <span class="k">do</span>
<span class="n">number_field</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="vi">@number</span> <span class="o">=</span> <span class="mi">0</span>
<span class="vi">@op</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="vi">@previous</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">flow</span> <span class="ss">width: </span><span class="mi">200</span><span class="p">,</span> <span class="ss">height: </span><span class="mi">240</span> <span class="k">do</span>
<span class="n">flow</span> <span class="ss">width: </span><span class="mf">0.7</span><span class="p">,</span> <span class="ss">height: </span><span class="mf">0.2</span> <span class="k">do</span>
<span class="n">background</span> <span class="n">rgb</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">157</span><span class="p">,</span> <span class="mi">228</span><span class="p">)</span>
<span class="n">number_field</span> <span class="o">=</span> <span class="n">para</span> <span class="vi">@number</span><span class="p">,</span> <span class="ss">margin: </span><span class="mi">10</span>
<span class="k">end</span>
<span class="n">flow</span> <span class="ss">width: </span><span class="mf">0.3</span><span class="p">,</span> <span class="ss">height: </span><span class="mf">0.2</span> <span class="k">do</span>
<span class="n">button</span> <span class="s1">'Clr'</span><span class="p">,</span> <span class="ss">width: </span><span class="mf">1.0</span><span class="p">,</span> <span class="ss">height: </span><span class="mf">1.0</span> <span class="k">do</span>
<span class="vi">@number</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">number_field</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="vi">@number</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">flow</span> <span class="ss">width: </span><span class="mf">1.0</span><span class="p">,</span> <span class="ss">height: </span><span class="mf">0.8</span> <span class="k">do</span>
<span class="n">background</span> <span class="n">rgb</span><span class="p">(</span><span class="mi">139</span><span class="p">,</span> <span class="mi">206</span><span class="p">,</span> <span class="mi">236</span><span class="p">)</span>
<span class="sx">%w(7 8 9 + 4 5 6 - 1 2 3 / 0 . = *)</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">btn</span><span class="o">|</span>
<span class="n">button</span> <span class="n">btn</span><span class="p">,</span> <span class="ss">width: </span><span class="mi">50</span><span class="p">,</span> <span class="ss">height: </span><span class="mi">50</span> <span class="k">do</span>
<span class="k">case</span> <span class="n">btn</span>
<span class="k">when</span> <span class="sr">/[0-9]/</span>
<span class="vi">@number</span> <span class="o">=</span> <span class="vi">@number</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">+</span> <span class="n">btn</span><span class="p">.</span><span class="nf">to_i</span>
<span class="k">when</span> <span class="s1">'='</span>
<span class="vi">@number</span> <span class="o">=</span> <span class="vi">@previous</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="vi">@op</span><span class="p">,</span> <span class="vi">@number</span><span class="p">)</span>
<span class="k">else</span>
<span class="vi">@previous</span><span class="p">,</span> <span class="vi">@number</span> <span class="o">=</span> <span class="vi">@number</span><span class="p">,</span> <span class="kp">nil</span>
<span class="vi">@op</span> <span class="o">=</span> <span class="n">btn</span>
<span class="k">end</span>
<span class="n">number_field</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="vi">@number</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>It would work with { and } instead of do and end. <strong>Credits</strong>: Calculator code by <a href="https://github.com/Alexanderlol">Alex Afeltra</a>, <a href="https://github.com/ashbb/green_shoes">Green_shoes gem</a> by ashbb but unmaintained today. <a href="https://github.com/ashbb/shoes_tutorial_walkthrough">Shoes originally by _why</a>. Even if it was maintained, you could not reasonably deploy to Android or iOS.</p>
<p>GPT thinks that is “Ruby in a builder-style DSL syntax that leverages blocks” and that would work for the people in the Ruby community.” Groovy would call those closures, while Ruby ‘blocks’. So there’s no easy canonical way of describing it without a language prefix.</p>
<p>I’ve always said pseudo-declarative DSL, but that would itself need qualification. HTML is declarative. Gemini agrees:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reasons for classifying HTML as declarative:
1. Focus on describing desired state: HTML focuses on describing the content
and structure you want on a web page, not how to achieve it step-by-step. You
say what you want, and the browser handles the rendering details.
2. No control flow statements: Unlike languages with if/else or for loops,
HTML avoids control flow mechanisms. It simply defines the elements and their
relationships, allowing the browser to interpret and handle the presentation
and interactions dynamically.
2. Data-driven and reusable: HTML elements hold data and properties that define
their appearance and behavior. This creates reusability and modularity, allowing
the same element types to be used in different contexts with varying attributes.
While HTML is predominantly declarative, there are nuances to consider:
* Scripting languages like JavaScript can be embedded within HTML: This adds
imperative elements and control flow possibilities, though the overall
structure remains declarative.
* CSS stylesheets apply presentation rules: CSS, while separate from HTML,
defines how elements are presented. This introduces non-declarative aspects
as you specify colors, layouts, and animations.
Overall, calling HTML a "declarative DSL" is accurate, while acknowledging the potential
integration with imperative scripting for more complex interactions. Compared to the
"pseudo-declarative" style with mixed imperative elements, HTML leans far more towards
describing the desired state without explicitly controlling the steps to achieve it.
</code></pre></div></div>
<p>Gemini goes on after I asked for consideration of “HTML (and declarative languages) are parsed and loaded into navigable structures”</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTML and Declarative Languages:
* Parsing: As you mentioned, declarative languages like HTML are typically
parsed into a structured representation (e.g., DOM tree in HTML). This
structure captures the relationships and properties of elements within the
document.
* Data-driven: The parsed structure serves as the basis for further processing
and display. Styling and behavior are often applied based on this structure
and associated data.
* No execution: Declarative languages themselves don't execute code or define
algorithms. They provide a way to describe what elements and their properties
should be, leaving the interpretation and processing to other systems (like
browsers for HTML).
</code></pre></div></div>
<p>I’d wanted Gemini to say “the DOM” and it did. Gemini on the pseudo declarative DSL way I’m fascinated by:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Green Shoes and Similar Styles:
* Interpretation: Instead of parsing, this style relies on interpretation.
The code is executed line by line, and elements are dynamically created
and placed as the code runs.
* No explicit structure: While structures can emerge from the code
execution, there's no upfront parsing and creation of a separate data
structure representing the UI elements.
* Executable logic: The code includes both declarations of elements and
their properties, as well as logic for handling events and user
interactions.
</code></pre></div></div>
<p>So longer description of the scripting possible with this language feature could be: Pseudo-declarative DSL that leverage builder-style blocks or closures, and are interpreted (or compiled) as all 3GLs. And thus, are not merely parsed into DOM-alike for a secondary actioning (or rendering if the use is for UIs).</p>
<p>For UI usages, Kivy’s kv templates were nice, but not it. QML looks fairly close by the blocks/closures are not open to arbitrary bits of code. I’m told Nix’s expression language is attractive, but I can’t quite coerce GPT4 into showing me what Calculator would look like in a contrived nixGuiLib:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ lib, nixGuiLib }:
let
# Function to create number buttons
createNumberButton = number: nixGuiLib.button {
label = toString number;
width = 50;
height = 50;
onClick = ''self: { self.number = self.number * 10 + ${toString number}; }'';
};
# Function to create operator buttons
createOperatorButton = op: nixGuiLib.button {
label = op;
width = 50;
height = 50;
onClick = ''self: { ... }''; # Placeholder for operator functionality
};
# Generate number buttons
numberButtons = map createNumberButton [7 8 9 4 5 6 1 2 3 0];
# Generate operator buttons
operatorButtons = map createOperatorButton ["+", "-", "*", "/", "=", "."];
# Combine number and operator buttons
combinedButtons = lib.concatLists [numberButtons operatorButtons];
in
nixGuiLib.app {
title = "My calculator";
width = 200;
height = 240;
content = nixGuiLib.flow {
width = 200;
height = 240;
children = [
nixGuiLib.flow {
width = 0.7;
height = 0.2;
children = [
nixGuiLib.background {
color = "rgb(0, 157, 228)";
child = nixGuiLib.para {
text = "0"; # Initial number display
margin = 10;
};
}
];
}
nixGuiLib.flow {
width = 0.3;
height = 0.2;
children = [
nixGuiLib.button {
label = "Clr";
width = 1.0;
height = 1.0;
onClick = ''self: { self.number = 0; }''; # Reset the display
}
];
}
nixGuiLib.flow {
width = 1.0;
height = 0.8;
children = [
nixGuiLib.background {
color = "rgb(139, 206, 236)";
child = nixGuiLib.flow {
children = combinedButtons;
# Additional properties for layout if necessary
};
}
];
}
];
};
}
</code></pre></div></div>
<p>In that last, <code class="language-plaintext highlighter-rouge">concatLists</code> of two sets of buttons would give buttons incorrectly placed in a flowing four by four grid. The next generation was worse, and it was clear that the for-loop possible in the Ruby and Groovy scripts wasn’t possible. Even if that was possible, there is too much prefixing of <code class="language-plaintext highlighter-rouge">nixGuiLib.</code> on the functions that could infer context of what instance to operate on as Ruby/Groovy do.</p>
<p>Groovy + SwingBuilder port of the calculator (no good to iOS or Android deployments):</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">groovy.swing.SwingBuilder</span>
<span class="kn">import</span> <span class="nn">javax.swing.JFrame</span>
<span class="kn">import</span> <span class="nn">javax.swing.WindowConstants</span>
<span class="k">new</span> <span class="nf">SwingBuilder</span><span class="o">().</span><span class="na">edt</span> <span class="o">{</span>
<span class="n">JFrame</span> <span class="n">frame</span> <span class="o">=</span> <span class="n">frame</span><span class="o">(</span><span class="nl">title:</span> <span class="s1">'My calculator'</span><span class="o">,</span> <span class="nl">size:</span> <span class="o">[</span><span class="mi">200</span><span class="o">,</span> <span class="mi">240</span><span class="o">],</span> <span class="nl">defaultCloseOperation:</span> <span class="n">WindowConstants</span><span class="o">.</span><span class="na">EXIT_ON_CLOSE</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">numberField</span>
<span class="kt">def</span> <span class="n">number</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kt">def</span> <span class="n">op</span> <span class="o">=</span> <span class="kc">null</span>
<span class="kt">def</span> <span class="n">previous</span> <span class="o">=</span> <span class="mi">0</span>
<span class="c1">// Top number display</span>
<span class="n">panel</span><span class="o">(</span><span class="nl">layout:</span> <span class="k">new</span> <span class="n">BorderLayout</span><span class="o">(),</span> <span class="nl">preferredSize:</span> <span class="o">[</span><span class="mi">200</span><span class="o">,</span> <span class="mi">48</span><span class="o">])</span> <span class="o">{</span>
<span class="n">background</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Color</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">157</span><span class="o">,</span> <span class="mi">228</span><span class="o">)</span>
<span class="n">numberField</span> <span class="o">=</span> <span class="n">label</span><span class="o">(</span><span class="nl">text:</span> <span class="s2">"${number}"</span><span class="o">,</span> <span class="nl">horizontalAlignment:</span> <span class="n">SwingConstants</span><span class="o">.</span><span class="na">CENTER</span><span class="o">)</span>
<span class="o">}</span>
<span class="c1">// Clear button</span>
<span class="n">panel</span><span class="o">(</span><span class="nl">layout:</span> <span class="k">new</span> <span class="n">FlowLayout</span><span class="o">(),</span> <span class="nl">preferredSize:</span> <span class="o">[</span><span class="mi">200</span><span class="o">,</span> <span class="mi">48</span><span class="o">])</span> <span class="o">{</span>
<span class="n">button</span><span class="o">(</span><span class="s1">'Clr'</span><span class="o">,</span> <span class="nl">size:</span> <span class="o">[</span><span class="mi">94</span><span class="o">,</span> <span class="mi">48</span><span class="o">])</span> <span class="o">{</span>
<span class="n">action</span> <span class="o">=</span> <span class="o">{</span> <span class="n">evt</span> <span class="o">-></span>
<span class="n">number</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">numberField</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="s2">"${number}"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">// Number and operation buttons</span>
<span class="n">panel</span><span class="o">(</span><span class="nl">layout:</span> <span class="k">new</span> <span class="n">GridLayout</span><span class="o">(</span><span class="mi">4</span><span class="o">,</span> <span class="mi">4</span><span class="o">,</span> <span class="mi">5</span><span class="o">,</span> <span class="mi">5</span><span class="o">),</span> <span class="nl">preferredSize:</span> <span class="o">[</span><span class="mi">200</span><span class="o">,</span> <span class="mi">144</span><span class="o">])</span> <span class="o">{</span>
<span class="n">background</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Color</span><span class="o">(</span><span class="mi">139</span><span class="o">,</span> <span class="mi">206</span><span class="o">,</span> <span class="mi">236</span><span class="o">)</span>
<span class="o">[</span><span class="s1">'7'</span><span class="o">,</span> <span class="s1">'8'</span><span class="o">,</span> <span class="s1">'9'</span><span class="o">,</span> <span class="s1">'+'</span><span class="o">,</span> <span class="s1">'4'</span><span class="o">,</span> <span class="s1">'5'</span><span class="o">,</span> <span class="s1">'6'</span><span class="o">,</span> <span class="s1">'-'</span><span class="o">,</span> <span class="s1">'1'</span><span class="o">,</span> <span class="s1">'2'</span><span class="o">,</span> <span class="s1">'3'</span><span class="o">,</span> <span class="s1">'/'</span><span class="o">,</span> <span class="s1">'0'</span><span class="o">,</span> <span class="s1">'.'</span><span class="o">,</span> <span class="s1">'='</span><span class="o">,</span> <span class="s1">'*'</span><span class="o">].</span><span class="na">each</span> <span class="o">{</span> <span class="n">btn</span> <span class="o">-></span>
<span class="n">button</span><span class="o">(</span><span class="n">btn</span><span class="o">,</span> <span class="nl">size:</span> <span class="o">[</span><span class="mi">50</span><span class="o">,</span> <span class="mi">50</span><span class="o">])</span> <span class="o">{</span>
<span class="n">action</span> <span class="o">=</span> <span class="o">{</span> <span class="n">evt</span> <span class="o">-></span>
<span class="k">switch</span><span class="o">(</span><span class="n">btn</span><span class="o">)</span> <span class="o">{</span>
<span class="k">case</span> <span class="o">~</span><span class="s">/[0-9]/</span><span class="o">:</span>
<span class="n">number</span> <span class="o">=</span> <span class="n">number</span><span class="o">.</span><span class="na">toInteger</span><span class="o">()</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">+</span> <span class="n">btn</span><span class="o">.</span><span class="na">toInteger</span><span class="o">()</span>
<span class="k">break</span>
<span class="k">case</span> <span class="s1">'='</span><span class="o">:</span>
<span class="k">if</span> <span class="o">(</span><span class="n">op</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">previous</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">number</span> <span class="o">=</span> <span class="n">previous</span><span class="o">.</span><span class="s2">"$op"</span><span class="o">(</span><span class="n">number</span><span class="o">)</span>
<span class="n">previous</span> <span class="o">=</span> <span class="kc">null</span>
<span class="n">op</span> <span class="o">=</span> <span class="kc">null</span>
<span class="o">}</span>
<span class="k">break</span>
<span class="nl">default:</span>
<span class="n">previous</span> <span class="o">=</span> <span class="n">number</span>
<span class="n">number</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">op</span> <span class="o">=</span> <span class="n">btn</span>
<span class="k">break</span>
<span class="o">}</span>
<span class="n">numberField</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="s2">"${number}"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">frame</span><span class="o">.</span><span class="na">pack</span><span class="o">()</span>
<span class="n">frame</span><span class="o">.</span><span class="na">show</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Here’s the QML port by GPT4:</p>
<div class="language-qml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">QtQuick</span> <span class="mf">2.15</span>
<span class="k">import</span> <span class="nx">QtQuick</span><span class="p">.</span><span class="nx">Controls</span> <span class="mf">2.15</span>
<span class="k">import</span> <span class="nx">QtQuick</span><span class="p">.</span><span class="nx">Layouts</span> <span class="mf">1.15</span>
<span class="kt">ApplicationWindow</span> <span class="p">{</span>
<span class="nl">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">My Calculator</span><span class="dl">"</span>
<span class="nl">width</span><span class="p">:</span> <span class="mi">200</span>
<span class="nl">height</span><span class="p">:</span> <span class="mi">240</span>
<span class="nl">visible</span><span class="p">:</span> <span class="kc">true</span>
<span class="kd">property</span> <span class="kt">int</span> <span class="nl">number</span><span class="p">:</span> <span class="mi">0</span>
<span class="kd">property</span> <span class="kt">string</span> <span class="nl">op</span><span class="p">:</span> <span class="dl">""</span>
<span class="kd">property</span> <span class="kt">int</span> <span class="nl">previous</span><span class="p">:</span> <span class="mi">0</span>
<span class="kt">ColumnLayout</span> <span class="p">{</span>
<span class="nl">anchors.fill</span><span class="p">:</span> <span class="nx">parent</span>
<span class="kt">Rectangle</span> <span class="p">{</span>
<span class="nl">Layout.preferredWidth</span><span class="p">:</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">width</span>
<span class="nl">Layout.preferredHeight</span><span class="p">:</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">height</span> <span class="o">*</span> <span class="mf">0.2</span>
<span class="nl">color</span><span class="p">:</span> <span class="dl">"</span><span class="s2">#009de4</span><span class="dl">"</span>
<span class="kt">Text</span> <span class="p">{</span>
<span class="nl">id</span><span class="p">:</span> <span class="kd">numberField</span>
<span class="nl">text</span><span class="p">:</span> <span class="nx">number</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span>
<span class="nl">anchors.centerIn</span><span class="p">:</span> <span class="nx">parent</span>
<span class="nl">font.pixelSize</span><span class="p">:</span> <span class="mi">24</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">Rectangle</span> <span class="p">{</span>
<span class="nl">Layout.preferredWidth</span><span class="p">:</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">width</span> <span class="o">*</span> <span class="mf">0.3</span>
<span class="nl">Layout.preferredHeight</span><span class="p">:</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">height</span> <span class="o">*</span> <span class="mf">0.2</span>
<span class="kt">Button</span> <span class="p">{</span>
<span class="nl">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Clr</span><span class="dl">"</span>
<span class="nl">anchors.fill</span><span class="p">:</span> <span class="nx">parent</span>
<span class="nl">onClicked</span><span class="p">:</span> <span class="p">{</span>
<span class="nx">number</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="nx">numberField</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="nx">number</span><span class="p">.</span><span class="nx">toString</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">GridLayout</span> <span class="p">{</span>
<span class="nl">Layout.preferredHeight</span><span class="p">:</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">height</span> <span class="o">*</span> <span class="mf">0.8</span>
<span class="nl">columns</span><span class="p">:</span> <span class="mi">4</span>
<span class="nl">rowSpacing</span><span class="p">:</span> <span class="mi">10</span>
<span class="nl">columnSpacing</span><span class="p">:</span> <span class="mi">10</span>
<span class="kd">property</span> <span class="kt">var</span> <span class="nl">buttons</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">7</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">8</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">9</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">+</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">4</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">5</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">6</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">-</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">1</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">2</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">3</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">0</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">=</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">*</span><span class="dl">"</span><span class="p">]</span>
<span class="kt">Repeater</span> <span class="p">{</span>
<span class="nl">model</span><span class="p">:</span> <span class="nx">buttons</span>
<span class="kt">Button</span> <span class="p">{</span>
<span class="nl">text</span><span class="p">:</span> <span class="nx">modelData</span>
<span class="nl">width</span><span class="p">:</span> <span class="mi">50</span>
<span class="nl">height</span><span class="p">:</span> <span class="mi">50</span>
<span class="nl">onClicked</span><span class="p">:</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nx">text</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">0-9</span><span class="se">]</span><span class="sr">/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">btn</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">number</span> <span class="o">=</span> <span class="nx">number</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">+</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">btn</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">btn</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">=</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">op</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">number</span> <span class="o">=</span> <span class="nb">eval</span><span class="p">(</span><span class="nx">previous</span> <span class="o">+</span> <span class="nx">op</span> <span class="o">+</span> <span class="nx">number</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span> <span class="kt">else</span> <span class="p">{</span>
<span class="nx">previous</span> <span class="o">=</span> <span class="nx">number</span><span class="p">;</span>
<span class="nx">number</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="nx">op</span> <span class="o">=</span> <span class="nx">btn</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">numberField</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="nx">number</span><span class="p">.</span><span class="nx">toString</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Compatibility: iOS & Android (via <a href="https://felgo.com/">felgo.com</a> tech), and macOS, Linux and Windows for QML</p>
<p>Note that I’ve previously <a href="https://paulhammant.com/2016/11/15/qmls-squandered-opportunity/">been critical of QML</a>. There is a theoretical deployment possibility to iOS & Android (via <a href="https://felgo.com/">felgo.com</a> tech), and macOS, Linux and Windows for QML.</p>
<p>Here is the GPT4 port to Flutter with inline Dart:</p>
<pre><code class="language-flutter">import 'package:flutter/material.dart';
void main() => runApp(MyCalculator());
class MyCalculator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My calculator',
home: Calculator(),
);
}
}
class Calculator extends StatefulWidget {
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
String _display = '0';
double _number = 0;
String _op = '';
double _previous = 0;
void _onPressed(String value) {
setState(() {
switch (value) {
case 'Clr':
_display = '0';
_number = 0;
_op = '';
_previous = 0;
break;
case '=':
if (_op != null && _op.isNotEmpty) {
switch (_op) {
case '+':
_number = _previous + _number;
break;
case '-':
_number = _previous - _number;
break;
case '*':
_number = _previous * _number;
break;
case '/':
_number = _previous / _number;
break;
}
_previous = 0;
_op = '';
_display = _number.toString();
_number = 0;
}
break;
case '+':
case '-':
case '*':
case '/':
_previous = _number;
_number = 0;
_op = value;
break;
default: // Number buttons
_number = _number * 10 + double.parse(value);
_display = _number.toString();
break;
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My calculator')),
body: Column(
children: <Widget>[
Expanded(
child: Container(
color: Color.fromRGBO(0, 157, 228, 1),
child: Center(
child: Text(_display, style: TextStyle(fontSize: 24, color: Colors.white)),
),
),
),
Expanded(
child: Container(
color: Colors.blueGrey,
child: FlatButton(
child: Text('Clr', style: TextStyle(fontSize: 20)),
onPressed: () => _onPressed('Clr'),
),
),
),
Expanded(
flex: 4,
child: Container(
color: Color.fromRGBO(139, 206, 236, 1),
child: GridView.count(
crossAxisCount: 4,
children: <String>['7', '8', '9', '+', '4', '5', '6', '-', '1', '2', '3', '/', '0', '.', '=', '*']
.map((String value) {
return GridTile(
child: FlatButton(
child: Text(value, style: TextStyle(fontSize: 24)),
onPressed: () => _onPressed(value),
),
);
}).toList(),
),
),
),
],
),
);
}
}
</code></pre>
<p>Flutter has a iOS, Android, macOS, Linux and Windows deployment possibilities.</p>
<p>GPT4-made Kotlin with TornadoFX (JVM language family, but now abandoned):</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">tornadofx.*</span>
<span class="k">import</span> <span class="nn">javafx.geometry.Pos</span>
<span class="k">import</span> <span class="nn">javafx.scene.paint.Color</span>
<span class="kd">class</span> <span class="nc">MyCalculatorApp</span> <span class="p">:</span> <span class="nc">App</span><span class="p">(</span><span class="nc">MyCalculatorView</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="kd">class</span> <span class="nc">MyCalculatorView</span> <span class="p">:</span> <span class="nc">View</span><span class="p">(</span><span class="s">"My calculator"</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="py">numberField</span><span class="p">:</span> <span class="nc">Text</span> <span class="k">by</span> <span class="nf">singleAssign</span><span class="p">()</span>
<span class="kd">var</span> <span class="py">number</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
<span class="kd">var</span> <span class="py">op</span><span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
<span class="kd">var</span> <span class="py">previous</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
<span class="k">override</span> <span class="kd">val</span> <span class="py">root</span> <span class="p">=</span> <span class="nf">vbox</span> <span class="p">{</span>
<span class="n">minWidth</span> <span class="p">=</span> <span class="mf">200.0</span>
<span class="n">minHeight</span> <span class="p">=</span> <span class="mf">240.0</span>
<span class="n">alignment</span> <span class="p">=</span> <span class="nc">Pos</span><span class="p">.</span><span class="nc">TOP_CENTER</span>
<span class="nf">hbox</span> <span class="p">{</span>
<span class="n">minWidth</span> <span class="p">=</span> <span class="mf">200.0</span>
<span class="n">minHeight</span> <span class="p">=</span> <span class="mf">48.0</span> <span class="c1">// 20% of 240</span>
<span class="nf">rectangle</span> <span class="p">{</span>
<span class="n">fill</span> <span class="p">=</span> <span class="nc">Color</span><span class="p">.</span><span class="nf">rgb</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">157</span><span class="p">,</span> <span class="mi">228</span><span class="p">)</span>
<span class="n">width</span> <span class="p">=</span> <span class="mf">140.0</span> <span class="c1">// 70% of 200</span>
<span class="n">height</span> <span class="p">=</span> <span class="mf">48.0</span> <span class="c1">// 100% height of this hbox</span>
<span class="p">}</span>
<span class="n">numberField</span> <span class="p">=</span> <span class="nf">text</span><span class="p">(</span><span class="n">number</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span> <span class="p">{</span>
<span class="nf">style</span> <span class="p">{</span>
<span class="n">fontSize</span> <span class="p">=</span> <span class="mi">20</span><span class="p">.</span><span class="n">px</span>
<span class="n">fill</span> <span class="p">=</span> <span class="nc">Color</span><span class="p">.</span><span class="nc">WHITE</span>
<span class="p">}</span>
<span class="n">paddingAll</span> <span class="p">=</span> <span class="mf">10.0</span>
<span class="p">}</span>
<span class="nf">button</span><span class="p">(</span><span class="s">"Clr"</span><span class="p">)</span> <span class="p">{</span>
<span class="n">minWidth</span> <span class="p">=</span> <span class="mf">60.0</span> <span class="c1">// 30% of 200</span>
<span class="n">minHeight</span> <span class="p">=</span> <span class="mf">48.0</span> <span class="c1">// 100% height of this hbox</span>
<span class="nf">action</span> <span class="p">{</span>
<span class="n">number</span> <span class="p">=</span> <span class="mi">0</span>
<span class="n">numberField</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">number</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">gridpane</span> <span class="p">{</span>
<span class="n">minWidth</span> <span class="p">=</span> <span class="mf">200.0</span>
<span class="n">minHeight</span> <span class="p">=</span> <span class="mf">192.0</span> <span class="c1">// 80% of 240</span>
<span class="n">hgap</span> <span class="p">=</span> <span class="mf">5.0</span>
<span class="n">vgap</span> <span class="p">=</span> <span class="mf">5.0</span>
<span class="n">alignment</span> <span class="p">=</span> <span class="nc">Pos</span><span class="p">.</span><span class="nc">CENTER</span>
<span class="nf">rectangle</span> <span class="p">{</span>
<span class="n">fill</span> <span class="p">=</span> <span class="nc">Color</span><span class="p">.</span><span class="nf">rgb</span><span class="p">(</span><span class="mi">139</span><span class="p">,</span> <span class="mi">206</span><span class="p">,</span> <span class="mi">236</span><span class="p">)</span>
<span class="n">width</span> <span class="p">=</span> <span class="mf">200.0</span>
<span class="n">height</span> <span class="p">=</span> <span class="mf">192.0</span>
<span class="p">}</span>
<span class="nf">listOf</span><span class="p">(</span><span class="s">"7"</span><span class="p">,</span> <span class="s">"8"</span><span class="p">,</span> <span class="s">"9"</span><span class="p">,</span> <span class="s">"+"</span><span class="p">,</span> <span class="s">"4"</span><span class="p">,</span> <span class="s">"5"</span><span class="p">,</span> <span class="s">"6"</span><span class="p">,</span> <span class="s">"-"</span><span class="p">,</span> <span class="s">"1"</span><span class="p">,</span> <span class="s">"2"</span><span class="p">,</span> <span class="s">"3"</span><span class="p">,</span> <span class="s">"/"</span><span class="p">,</span> <span class="s">"0"</span><span class="p">,</span> <span class="s">"."</span><span class="p">,</span> <span class="s">"="</span><span class="p">,</span> <span class="s">"*"</span><span class="p">).</span><span class="nf">forEachIndexed</span> <span class="p">{</span> <span class="n">index</span><span class="p">,</span> <span class="n">btn</span> <span class="p">-></span>
<span class="kd">val</span> <span class="p">(</span><span class="py">x</span><span class="p">,</span> <span class="py">y</span><span class="p">)</span> <span class="p">=</span> <span class="n">index</span> <span class="p">%</span> <span class="mi">4</span> <span class="n">to</span> <span class="n">index</span> <span class="p">/</span> <span class="mi">4</span>
<span class="nf">button</span><span class="p">(</span><span class="n">btn</span><span class="p">)</span> <span class="p">{</span>
<span class="n">minWidth</span> <span class="p">=</span> <span class="mf">45.0</span>
<span class="n">minHeight</span> <span class="p">=</span> <span class="mf">45.0</span>
<span class="nf">gridpaneConstraints</span> <span class="p">{</span>
<span class="n">columnIndex</span> <span class="p">=</span> <span class="n">x</span>
<span class="n">rowIndex</span> <span class="p">=</span> <span class="n">y</span>
<span class="p">}</span>
<span class="nf">action</span> <span class="p">{</span>
<span class="k">when</span> <span class="p">{</span>
<span class="n">btn</span><span class="p">.</span><span class="nf">matches</span><span class="p">(</span><span class="nc">Regex</span><span class="p">(</span><span class="s">"[0-9]"</span><span class="p">))</span> <span class="p">-></span> <span class="p">{</span>
<span class="n">number</span> <span class="p">=</span> <span class="n">number</span> <span class="p">*</span> <span class="mi">10</span> <span class="p">+</span> <span class="n">btn</span><span class="p">.</span><span class="nf">toInt</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">btn</span> <span class="p">==</span> <span class="s">"="</span> <span class="p">-></span> <span class="p">{</span>
<span class="n">number</span> <span class="p">=</span> <span class="k">when</span> <span class="p">(</span><span class="n">op</span><span class="p">)</span> <span class="p">{</span>
<span class="s">"+"</span> <span class="p">-></span> <span class="n">previous</span> <span class="p">+</span> <span class="n">number</span>
<span class="s">"-"</span> <span class="p">-></span> <span class="n">previous</span> <span class="p">-</span> <span class="n">number</span>
<span class="s">"*"</span> <span class="p">-></span> <span class="n">previous</span> <span class="p">*</span> <span class="n">number</span>
<span class="s">"/"</span> <span class="p">-></span> <span class="n">previous</span> <span class="p">/</span> <span class="n">number</span>
<span class="k">else</span> <span class="p">-></span> <span class="n">number</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">-></span> <span class="p">{</span>
<span class="n">previous</span> <span class="p">=</span> <span class="n">number</span>
<span class="n">number</span> <span class="p">=</span> <span class="mi">0</span>
<span class="n">op</span> <span class="p">=</span> <span class="n">btn</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">numberField</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">number</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="n">launch</span><span class="p"><</span><span class="nc">MyCalculatorApp</span><span class="p">>()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>GPT4 notes that the JavaFX without Kotlin is more verbose. I’m not doing to show it here because it does not have that language feature we’re looking for it is just a bunch of <code class="language-plaintext highlighter-rouge">instance.method(arg)</code> statements that superficially look nested.</p>
<p>Apple’s SwiftUI made by GPT4 (macOS and iOS only for now)</p>
<pre><code class="language-swiftui">import SwiftUI
struct ContentView: View {
@State private var number: Double = 0
@State private var previous: Double = 0
@State private var op: String? = nil
let buttons: [[String]] = [
["7", "8", "9", "+"],
["4", "5", "6", "-"],
["1", "2", "3", "/"],
["0", ".", "=", "*"]
]
var body: some View {
VStack(spacing: 0) {
Text("\(number)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.foregroundColor(.white)
.font(.largeTitle)
Button("Clr") {
self.number = 0
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
.foregroundColor(.white)
ForEach(buttons, id: \.self) { row in
HStack(spacing: 0) {
ForEach(row, id: \.self) { button in
Button(button) {
self.buttonPressed(button)
}
.frame(width: 50, height: 50)
.background(Color.cyan)
.foregroundColor(.black)
}
}
}
}
.frame(width: 200, height: 240)
}
private func buttonPressed(_ button: String) {
switch button {
case "0"..."9":
number = number * 10 + Double(button)!
case "=":
if let op = op {
switch op {
case "+": number = previous + number
case "-": number = previous - number
case "*": number = previous * number
case "/": number = previous / number
default: break
}
}
default:
previous = number
number = 0
self.op = button
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
</code></pre>
<p>And GPT4 Common Lisp with CLIM (for Mac, Windows and Linux deployments), even though that’s a different language family than the one I’m talking about in this blog entry - it’s the epitome of terse and elegant.</p>
<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">defpackage</span> <span class="ss">:my-calculator</span>
<span class="p">(</span><span class="ss">:use</span> <span class="ss">:common-lisp</span> <span class="ss">:clim-lisp</span> <span class="ss">:clim</span><span class="p">))</span>
<span class="p">(</span><span class="nb">in-package</span> <span class="ss">:my-calculator</span><span class="p">)</span>
<span class="p">(</span><span class="nb">defun</span> <span class="nv">start-calculator</span> <span class="p">()</span>
<span class="p">(</span><span class="k">let</span> <span class="p">((</span><span class="nc">number</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">(</span><span class="nv">op</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">(</span><span class="nv">previous</span> <span class="mi">0</span><span class="p">))</span>
<span class="p">(</span><span class="nv">define-application-frame</span> <span class="nv">my-calculator</span> <span class="p">()</span>
<span class="p">()</span>
<span class="p">(</span><span class="ss">:panes</span>
<span class="p">(</span><span class="nv">display</span> <span class="ss">:application</span>
<span class="ss">:display-function</span> <span class="p">(</span><span class="k">lambda</span> <span class="p">(</span><span class="nv">frame</span> <span class="nv">pane</span><span class="p">)</span>
<span class="p">(</span><span class="k">declare</span> <span class="p">(</span><span class="k">ignore</span> <span class="nv">frame</span><span class="p">))</span>
<span class="p">(</span><span class="nb">format</span> <span class="nv">pane</span> <span class="s">"~A"</span> <span class="nc">number</span><span class="p">))</span>
<span class="ss">:scroll-bars</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">(</span><span class="nv">buttons</span> <span class="ss">:push-button-panel</span>
<span class="ss">:items</span> <span class="o">'</span><span class="p">((</span><span class="s">"Clr"</span> <span class="p">(</span><span class="nb">setf</span> <span class="nc">number</span> <span class="mi">0</span><span class="p">))</span>
<span class="s">"7"</span> <span class="s">"8"</span> <span class="s">"9"</span> <span class="s">"+"</span> <span class="s">"4"</span> <span class="s">"5"</span> <span class="s">"6"</span> <span class="s">"-"</span> <span class="s">"1"</span> <span class="s">"2"</span> <span class="s">"3"</span> <span class="s">"/"</span> <span class="s">"0"</span> <span class="s">"."</span> <span class="s">"="</span> <span class="s">"*"</span><span class="p">)</span>
<span class="ss">:activate-callbacks</span> <span class="o">'</span><span class="p">((</span><span class="ss">:button-press</span> <span class="p">(</span><span class="nv">button</span><span class="p">)</span>
<span class="p">(</span><span class="nb">case</span> <span class="p">(</span><span class="nb">string</span> <span class="nv">button</span><span class="p">)</span>
<span class="p">((</span><span class="s">"Clr"</span><span class="p">)</span> <span class="p">(</span><span class="nb">setf</span> <span class="nc">number</span> <span class="mi">0</span><span class="p">))</span>
<span class="p">((</span><span class="nv">digit</span> <span class="nb">when</span> <span class="p">(</span><span class="nb">digit-char-p</span> <span class="p">(</span><span class="nb">aref</span> <span class="nv">digit</span> <span class="mi">0</span><span class="p">)))</span>
<span class="p">(</span><span class="nb">setf</span> <span class="nc">number</span> <span class="p">(</span><span class="nb">+</span> <span class="p">(</span><span class="nb">*</span> <span class="nc">number</span> <span class="mi">10</span><span class="p">)</span> <span class="p">(</span><span class="nb">parse-integer</span> <span class="nv">digit</span><span class="p">))))</span>
<span class="p">((</span><span class="s">"="</span><span class="p">)</span> <span class="p">(</span><span class="nb">setf</span> <span class="nc">number</span> <span class="p">(</span><span class="nb">funcall</span> <span class="p">(</span><span class="nb">case</span> <span class="nv">op</span>
<span class="p">(</span><span class="s">"+"</span> <span class="nf">#'</span><span class="nb">+</span><span class="p">)</span>
<span class="p">(</span><span class="s">"-"</span> <span class="nf">#'</span><span class="nb">-</span><span class="p">)</span>
<span class="p">(</span><span class="s">"/"</span> <span class="nf">#'</span><span class="nb">/</span><span class="p">)</span>
<span class="p">(</span><span class="s">"*"</span> <span class="nf">#'</span><span class="nb">*</span><span class="p">)</span>
<span class="p">(</span><span class="nv">otherwise</span> <span class="nf">#'</span><span class="nb">identity</span><span class="p">))</span>
<span class="nv">previous</span> <span class="nc">number</span><span class="p">)))</span>
<span class="p">(</span><span class="nv">otherwise</span>
<span class="p">(</span><span class="nb">setf</span> <span class="nv">previous</span> <span class="nc">number</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="nc">number</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">(</span><span class="nb">setf</span> <span class="nv">op</span> <span class="nv">button</span><span class="p">)))</span>
<span class="p">(</span><span class="nv">redisplay-frame-pane</span> <span class="vg">*application-frame*</span> <span class="ss">'display</span><span class="p">)))))</span>
<span class="p">(</span><span class="ss">:layouts</span>
<span class="p">(</span><span class="nv">default</span> <span class="p">(</span><span class="nv">vertically</span> <span class="p">()</span>
<span class="p">(</span><span class="nv">scrolling</span> <span class="p">(</span><span class="ss">:height</span> <span class="mi">120</span><span class="p">)</span> <span class="nv">display</span><span class="p">)</span>
<span class="p">(</span><span class="nv">fixed-size</span> <span class="p">(</span><span class="ss">:height</span> <span class="mi">120</span><span class="p">)</span> <span class="nv">buttons</span><span class="p">)))))</span>
<span class="p">(</span><span class="nv">run-frame-top-level</span> <span class="p">(</span><span class="nv">make-application-frame</span> <span class="ss">'my-calculator</span><span class="p">))))</span>
<span class="p">(</span><span class="nv">start-calculator</span><span class="p">)</span>
</code></pre></div></div>
<p>Back to Groovy - there’s an exciting JVM build tech called “Bld”that uses Java and fluent-interfaces
as its way of configuring and describing targets: I made a Groovy skin over that. In execution it’s about
one second worse than the pure Java one for me. Someone with Groovy skills could reduce that, I guess (bld.groovy):</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Grapes</span><span class="o">(</span>
<span class="nd">@Grab</span><span class="o">(</span><span class="n">group</span><span class="o">=</span><span class="s1">'com.uwyn.rife2'</span><span class="o">,</span> <span class="n">module</span><span class="o">=</span><span class="s1">'bld'</span><span class="o">,</span> <span class="n">version</span><span class="o">=</span><span class="s1">'1.9.0'</span><span class="o">)</span>
<span class="o">)</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">dependencies</span><span class="o">.</span><span class="na">Repository</span><span class="o">.*</span>
<span class="kn">import</span> <span class="nn">rife.bld.dependencies.Scope</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">operations</span><span class="o">.</span><span class="na">TemplateType</span><span class="o">.</span><span class="na">HTML</span>
<span class="kt">def</span> <span class="n">proj</span> <span class="o">=</span> <span class="n">BuildConfig</span><span class="o">.</span><span class="na">webProject</span><span class="o">()</span> <span class="o">{</span>
<span class="n">pkg</span> <span class="o">=</span> <span class="s2">"com.example"</span>
<span class="n">name</span> <span class="o">=</span> <span class="s1">'Mywebapp'</span>
<span class="n">mainClass</span> <span class="o">=</span> <span class="s2">"com.example.MywebappSite"</span>
<span class="n">uberJarMainClass</span> <span class="o">=</span> <span class="s2">"com.example.MywebappSiteUber"</span>
<span class="n">version</span> <span class="o">=</span> <span class="n">version</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">)</span>
<span class="n">downloadSources</span> <span class="o">=</span> <span class="kc">true</span>
<span class="n">repositories</span> <span class="o">=</span> <span class="n">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">MAVEN_CENTRAL</span><span class="o">,</span> <span class="n">RIFE2_RELEASES</span><span class="o">)</span>
<span class="n">compile</span><span class="o">()</span> <span class="o">{</span>
<span class="n">include</span><span class="o">()</span> <span class="o">{</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"com.uwyn.rife2"</span><span class="o">,</span> <span class="s2">"rife2"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span><span class="mi">7</span><span class="o">,</span><span class="mi">3</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">test</span><span class="o">()</span> <span class="o">{</span>
<span class="n">include</span><span class="o">()</span> <span class="o">{</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"org.jsoup"</span><span class="o">,</span> <span class="s2">"jsoup"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span><span class="mi">17</span><span class="o">,</span><span class="mi">2</span><span class="o">))</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"org.junit.jupiter"</span><span class="o">,</span> <span class="s2">"junit-jupiter"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">5</span><span class="o">,</span><span class="mi">10</span><span class="o">,</span><span class="mi">2</span><span class="o">))</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"org.junit.platform"</span><span class="o">,</span> <span class="s2">"junit-platform-console-standalone"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span><span class="mi">10</span><span class="o">,</span><span class="mi">2</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">standalone</span><span class="o">()</span> <span class="o">{</span>
<span class="n">include</span><span class="o">()</span> <span class="o">{</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"org.eclipse.jetty.ee10"</span><span class="o">,</span> <span class="s2">"jetty-ee10"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">12</span><span class="o">,</span><span class="mi">0</span><span class="o">,</span><span class="mi">6</span><span class="o">))</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"org.eclipse.jetty.ee10"</span><span class="o">,</span> <span class="s2">"jetty-ee10-servlet"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">12</span><span class="o">,</span><span class="mi">0</span><span class="o">,</span><span class="mi">6</span><span class="o">))</span>
<span class="n">dependency</span><span class="o">(</span><span class="s2">"org.slf4j"</span><span class="o">,</span> <span class="s2">"slf4j-simple"</span><span class="o">,</span> <span class="n">version</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span><span class="mi">0</span><span class="o">,</span><span class="mi">11</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">precompileOperation</span><span class="o">()</span> <span class="o">{</span>
<span class="n">templateTypes</span><span class="o">(</span><span class="n">HTML</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">proj</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="n">args</span><span class="o">)</span>
<span class="c1">// -------------------------------------------------------------------------------------------</span>
<span class="c1">// All below here would be in a library no the bld.groovy file for ultimate end-user realities</span>
<span class="kd">class</span> <span class="nc">BuildConfig</span> <span class="o">{</span>
<span class="kt">def</span> <span class="kd">static</span> <span class="nf">webProject</span><span class="o">(</span><span class="n">Closure</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">proj</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GldWebProject</span><span class="o">()</span>
<span class="n">config</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">proj</span>
<span class="n">config</span><span class="o">.</span><span class="na">resolveStrategy</span> <span class="o">=</span> <span class="n">Closure</span><span class="o">.</span><span class="na">DELEGATE_FIRST</span>
<span class="n">config</span><span class="o">()</span>
<span class="k">return</span> <span class="n">proj</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">class</span> <span class="nc">GldWebProject</span> <span class="kd">extends</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">WebProject</span> <span class="o">{</span>
<span class="kt">def</span> <span class="nf">compile</span><span class="o">(</span><span class="n">Closure</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">gldScope</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GldScope</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">scope</span><span class="o">(</span><span class="n">Scope</span><span class="o">.</span><span class="na">compile</span><span class="o">))</span>
<span class="n">config</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">gldScope</span>
<span class="n">config</span><span class="o">.</span><span class="na">resolveStrategy</span> <span class="o">=</span> <span class="n">Closure</span><span class="o">.</span><span class="na">DELEGATE_FIRST</span>
<span class="n">config</span><span class="o">()</span>
<span class="o">}</span>
<span class="kt">def</span> <span class="nf">test</span><span class="o">(</span><span class="n">Closure</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">gldScope</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GldScope</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">scope</span><span class="o">(</span><span class="n">Scope</span><span class="o">.</span><span class="na">test</span><span class="o">))</span>
<span class="n">config</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">gldScope</span>
<span class="n">config</span><span class="o">.</span><span class="na">resolveStrategy</span> <span class="o">=</span> <span class="n">Closure</span><span class="o">.</span><span class="na">DELEGATE_FIRST</span>
<span class="n">config</span><span class="o">()</span>
<span class="o">}</span>
<span class="kt">def</span> <span class="nf">standalone</span><span class="o">(</span><span class="n">Closure</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">gldScope</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GldScope</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">scope</span><span class="o">(</span><span class="n">Scope</span><span class="o">.</span><span class="na">standalone</span><span class="o">))</span>
<span class="n">config</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">gldScope</span>
<span class="n">config</span><span class="o">.</span><span class="na">resolveStrategy</span> <span class="o">=</span> <span class="n">Closure</span><span class="o">.</span><span class="na">DELEGATE_FIRST</span>
<span class="n">config</span><span class="o">()</span>
<span class="o">}</span>
<span class="kt">def</span> <span class="nf">precompileOperation</span><span class="o">(</span><span class="n">Closure</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">gldpo</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GldPrecompileOperation</span><span class="o">(</span><span class="k">this</span><span class="o">)</span>
<span class="n">config</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">gldpo</span>
<span class="n">config</span><span class="o">.</span><span class="na">resolveStrategy</span> <span class="o">=</span> <span class="n">Closure</span><span class="o">.</span><span class="na">DELEGATE_FIRST</span>
<span class="n">config</span><span class="o">()</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">class</span> <span class="nc">GldScope</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">proj</span>
<span class="kt">def</span> <span class="n">sc</span>
<span class="kt">def</span> <span class="nf">GldScope</span><span class="o">(</span><span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">WebProject</span> <span class="n">proj</span><span class="o">,</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">dependencies</span><span class="o">.</span><span class="na">DependencySet</span> <span class="n">scope</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">sc</span> <span class="o">=</span> <span class="n">scope</span>
<span class="k">this</span><span class="o">.</span><span class="na">proj</span> <span class="o">=</span> <span class="n">proj</span>
<span class="o">}</span>
<span class="kt">def</span> <span class="nf">include</span><span class="o">(</span><span class="n">Closure</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">gldinclude</span> <span class="o">=</span> <span class="k">new</span> <span class="n">GldInclude</span><span class="o">(</span><span class="n">proj</span><span class="o">,</span> <span class="n">sc</span><span class="o">)</span>
<span class="n">config</span><span class="o">.</span><span class="na">delegate</span> <span class="o">=</span> <span class="n">gldinclude</span>
<span class="n">config</span><span class="o">.</span><span class="na">resolveStrategy</span> <span class="o">=</span> <span class="n">Closure</span><span class="o">.</span><span class="na">DELEGATE_FIRST</span>
<span class="n">config</span><span class="o">()</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">class</span> <span class="nc">GldInclude</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">proj</span>
<span class="kt">def</span> <span class="n">ds</span>
<span class="kt">def</span> <span class="nf">GldInclude</span><span class="o">(</span><span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">WebProject</span> <span class="n">proj</span><span class="o">,</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">dependencies</span><span class="o">.</span><span class="na">DependencySet</span> <span class="n">scope</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">proj</span> <span class="o">=</span> <span class="n">proj</span>
<span class="k">this</span><span class="o">.</span><span class="na">ds</span> <span class="o">=</span> <span class="n">scope</span>
<span class="o">}</span>
<span class="kt">def</span> <span class="nf">dependency</span><span class="o">(</span><span class="n">g</span><span class="o">,</span> <span class="n">a</span><span class="o">,</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span>
<span class="n">ds</span><span class="o">.</span><span class="na">include</span><span class="o">(</span><span class="n">proj</span><span class="o">.</span><span class="na">dependency</span><span class="o">(</span><span class="n">g</span><span class="o">,</span> <span class="n">a</span><span class="o">,</span> <span class="n">v</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">class</span> <span class="nc">GldPrecompileOperation</span> <span class="o">{</span>
<span class="kt">def</span> <span class="n">proj</span>
<span class="kt">def</span> <span class="n">ttype</span>
<span class="kt">def</span> <span class="nf">GldPrecompileOperation</span><span class="o">(</span><span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">WebProject</span> <span class="n">proj</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">proj</span> <span class="o">=</span> <span class="n">proj</span>
<span class="o">}</span>
<span class="kt">def</span> <span class="nf">templateTypes</span><span class="o">(</span><span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">operations</span><span class="o">.</span><span class="na">TemplateType</span> <span class="n">ttype</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">ttype</span> <span class="o">=</span> <span class="n">ttype</span>
<span class="n">proj</span><span class="o">.</span><span class="na">precompileOperation</span><span class="o">().</span><span class="na">templateTypes</span><span class="o">(</span><span class="n">ttype</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>That’s not just a speculative grammar, you can try it here: <a href="https://raw.githubusercontent.com/paul-hammant/groovy-bld-prototype">https://raw.githubusercontent.com/paul-hammant/groovy-bld-prototype</a>. I do have a prior article on Groovy infra-as-code <a href="/2024/01/24/infra-as-code-dsl">here</a></p>
<p><strong>Back to the quest implicit in this post</strong></p>
<p>Anyway, this is a needs a short name, in my opinion. Like a “Monads” class of short name. I asked Ruby’s creator
Yukihiro “Matz” Matsumoto and he said “DSL with scope” is what he’s always called it.</p>
<p>Updates:</p>
<ul>
<li>Feb 24th, 2024: add GPT4 ported calculator code: Nix example with a <strong>contrived</strong> UI lib, Swing example with its real SwingBuilder, QML example, Lisp example</li>
<li>Feb 25th, 2024: add Kotlin with TornadoFX and SwiftUI port of that Ruby Calculator.</li>
<li>Mar 1st, 2024: Add Groovy grammar for ‘Bld’ build language</li>
<li>Mar 7th, 2024: Add Matz feedback</li>
</ul>
Redirect to Ruby & Groovy entry ^2024-02-14T00:00:00+00:00https://paulhammant.com/2024/02/14/that-groovy-language-featureFlask and Static Files2024-02-08T00:00:00+00:00https://paulhammant.com/2024/02/08/flask-static-files<p>Cos I tied myself in knots reading about static file handling in Python’s Flask, I thought I’d take a note of what worked for now (GPT’s help appreciated). Attempts to look at StackOverflow for the same advice was confusing as defaults had changed over the years. Attempts would work for my localhost deployment, and not for the corporate app-server, or vice versa, but not both.</p>
<p>Prompt: “Make a Python/Flask app that has a static JavaScript resource that says <code class="language-plaintext highlighter-rouge">document.write("Hello");</code> a page web page template that says “world” after the JavaScript has inserted its word into the page, but also a context (top-level directory of the webapp) “greeting” that’s passed in as an argument to the Python and not hard-coded anywhere”. Well, close enough as it took three goes.</p>
<h2 id="python-apppy-script">Python ‘app.py’ script</h2>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">argparse</span>
<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span><span class="p">,</span> <span class="n">send_from_directory</span>
<span class="c1"># Argument parsing
</span><span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Run a Flask app with a given URL context.'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'context'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Context for the app URL'</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="n">context</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">context</span><span class="p">.</span><span class="n">strip</span><span class="p">(</span><span class="s">'/'</span><span class="p">)</span>
<span class="c1"># Static file serving route
</span><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="sa">f</span><span class="s">'/</span><span class="si">{</span><span class="n">context</span><span class="si">}</span><span class="s">/static/<path:filename>'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">custom_static</span><span class="p">(</span><span class="n">filename</span><span class="p">):</span>
<span class="k">return</span> <span class="n">send_from_directory</span><span class="p">(</span><span class="s">'static'</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="sa">f</span><span class="s">'/</span><span class="si">{</span><span class="n">context</span><span class="si">}</span><span class="s">/'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">index</span><span class="p">():</span>
<span class="n">greeting</span> <span class="o">=</span> <span class="s">"world"</span>
<span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">'index.html'</span><span class="p">,</span> <span class="n">greeting</span><span class="o">=</span><span class="n">greeting</span><span class="p">)</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="scriptjs-inside-static-folder">script.js inside static/ folder</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>document.write("Hello");
</code></pre></div></div>
<h2 id="indexhtml-in-template-folder">index.html in template/ folder</h2>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">></span>
<span class="nt"><title></span>Flask App<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><script </span><span class="na">src=</span><span class="s">"./static/script.js"</span><span class="nt">></script></span>
<span class="nt"><span</span> <span class="na">id=</span><span class="s">"js-greeting"</span><span class="nt">></span></span> <span class="nt"><span></span>, how are you?<span class="nt"></span></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<h2 id="launching">Launching</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python app.py
</code></pre></div></div>
<p>There is <code class="language-plaintext highlighter-rouge">url_for(..)</code> template function for Flask, but it didn’t help at all. Relative via “./” was the best/correct
approach in the html for loading static resources without having to know the context name. To be honest that was true
in 1995 and hasn’t changed. Of course, web-scale deployments should at least use the likes of Nginx for static resources.</p>
Infrastructure as Code DSLs2024-01-24T00:00:00+00:00https://paulhammant.com/2024/01/24/infra-as-code-dsl<p>I had liked one feature of Groovy for a long time that the dev team originally called builders, but that’s been re-used to mean different things since. Maybe even before. The language feature is a copy of Ruby’s <code class="language-plaintext highlighter-rouge">method_missing</code> that superficially looks like it is allowing additional keywords to the language, but it’s really a trick to allow method invocations on an implied context. With curly-braces and a bunch of classes and methods to make this look terse and elegant, we can end up with Groovy scripts that look like they are fully declarative. They’re not really, you can insert regular statements, including conditionals and loops. You might prefer for the <strong>pseudo-declarative</strong> form to not do that, but it’s always there for you if you want.</p>
<p>I thought I would ask ChatGPT what the language feature is called and <strong>Groovy in builder-style DSL syntax that leverages closures</strong> seems to coerce GPT into writing the right code based on examples, if you prefix with that. Someone needs a one-word name for this that doesn’t clash with other names, and could end up in other languages. Say “Zig now has first class cl*n?ds style like Groovy, Ruby and Python”.</p>
<h2 id="elegant-iac">Elegant IaC</h2>
<pre><code class="language-Groovy">fredAndWilma = new IaCscript('QA').containers("fredAndWilma") {
allowIngressFrom("192.168.0.*")
subnet("192.168.1.0/24")
container("fred") {
from("debian12-cloudinit.iso")
listeningPort(80)
allowIngressFrom("192.168.0.*")
allowEgressTo("wilma")
}
container("wilma") {
from("debian12-cloudinit.iso")
listeningPort(8080)
allowIngressFrom("fred")
persistentStorage("wilmaStorage/")
}
startAll()
}
fredAndWilma.print() // should be an enumeration of fred and wilma with start/stop status
fredAndWilma.on("wilma").transfer("fancyAppWC.tgz").runScript("fancyAppWebCache.sh")
fredAndWilma.on("fred").transfer("fancyAppBL.tgz").runScript("fancyAppBizLogic.sh")
</code></pre>
<p>Get your prompt right with “Groovy in builder-style DSL syntax that leverages closures” and it will write all the implementation code. There’s ten of so picky “don’t be lazy” clauses to add, but it does get there.</p>
<p>You’d run the script, and it would create local containers (LXC or Docker - not specified). It would also start them so they could be navigated to for further work (those last two lines). Indeed while you retain the right to rerun the script dropping previous provisioned pieces (ephemeral infra), you may also choose to just do the <code class="language-plaintext highlighter-rouge">trasnfer().runScript()</code> for CI:CD into say ‘shared_dev’ env.</p>
<p>So last night I was at EdinburghJS and saw Daniel Grant demonstrate the fantastic <a href="https://notation.dev/">Notation</a>. Notation has chosen TypeScript (of course) as the language to provision bits and pieces, and for now it focuses on AWS cloud-app setup. The demo of last night, as of the videos in the README, are for the registration of a Lambda function. Again that’s registration not the implementation of the lambda function itself. Well that can be inline too, but you’d probably separate. Registration means AWS web-api invocation ahead of actual use. Notation also has <a href="https://github.com/notation-dev/notation/blob/main/examples">examples</a> that are worth checking out. Now, TypeScript and JavaScript don’t have the “builder-style DSL syntax that leverages closures” language feature so the examples don’t look even half declarative.</p>
<p>Guaranteed to be wrong, but giving a feel for the Groovy pseudo-declarative way for aws/lambda:</p>
<pre><code class="language-Groovy">IaCscript.provisionEnv('shared_dev') {
aws() {
router() {
lambda(timeout: 5, memory: 64) {
function('/' MyApplication.greeting)
function('/foo' MyApplication.fooHandler)
}
}
}
}
</code></pre>
<p>Of course, I am not going to take this any further. My code is 0.0000001% complete versus all the vendor cloud things, and the on-prem stuff like Proxmox, VmWare, etc.</p>
<p>See also my <a href="https://paulhammant.com/2016/12/14/principles-of-containment">Principles of Containment blog entry</a>.</p>
More Syntactic Sugar For Java DSLs2023-05-14T00:00:00+00:00https://paulhammant.com/2023/05/14/more-syntactic-sugar-for-java-dsls<p>I once took a job that <a href="https://www.linkedin.com/in/gbevin/">Geert Bevin</a> had left a week or so before. I was blown away at the code quality and advanced topics of the code he’d left behind. That was late 1999 and my introduction to Inversion of Control and Perforce. Since then, he has remained one of the people that cyclically reminds me I’ve more things to learn in software development. <a href="https://github.com/rife2/rife2">Rife</a> is his advanced web-framework and within it, he’s made a simple Java-based build technology <a href="https://github.com/rife2/bld">Bld</a> that you could use instead of Maven, Gradle and others. Like all good reusable things, he’s eeked it out of a single use: it is Rife2’s build system.</p>
<h2 id="blds-example-script">Bld’s example script</h2>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">rife.bld.Project</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.List</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">dependencies</span><span class="o">.</span><span class="na">Repository</span><span class="o">.*;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">rife</span><span class="o">.</span><span class="na">bld</span><span class="o">.</span><span class="na">dependencies</span><span class="o">.</span><span class="na">Scope</span><span class="o">.*;</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyappBuild</span> <span class="kd">extends</span> <span class="nc">Project</span> <span class="o">{</span>
<span class="kd">public</span> <span class="nf">MyappBuild</span><span class="o">()</span> <span class="o">{</span>
<span class="n">pkg</span> <span class="o">=</span> <span class="s">"com.example"</span><span class="o">;</span>
<span class="n">name</span> <span class="o">=</span> <span class="s">"Myapp"</span><span class="o">;</span>
<span class="n">mainClass</span> <span class="o">=</span> <span class="s">"com.example.MyappMain"</span><span class="o">;</span>
<span class="n">version</span> <span class="o">=</span> <span class="n">version</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span><span class="mi">1</span><span class="o">,</span><span class="mi">0</span><span class="o">);</span>
<span class="n">downloadSources</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="n">repositories</span> <span class="o">=</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="no">MAVEN_CENTRAL</span><span class="o">,</span> <span class="no">RIFE2_RELEASES</span><span class="o">);</span>
<span class="n">scope</span><span class="o">(</span><span class="n">test</span><span class="o">)</span>
<span class="o">.</span><span class="na">include</span><span class="o">(</span><span class="n">dependency</span><span class="o">(</span><span class="s">"org.junit.jupiter"</span><span class="o">,</span>
<span class="s">"junit-jupiter"</span><span class="o">,</span>
<span class="n">version</span><span class="o">(</span><span class="mi">5</span><span class="o">,</span><span class="mi">9</span><span class="o">,</span><span class="mi">2</span><span class="o">)))</span>
<span class="o">.</span><span class="na">include</span><span class="o">(</span><span class="n">dependency</span><span class="o">(</span><span class="s">"org.junit.platform"</span><span class="o">,</span>
<span class="s">"junit-platform-console-standalone"</span><span class="o">,</span>
<span class="n">version</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span><span class="mi">9</span><span class="o">,</span><span class="mi">2</span><span class="o">)));</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="k">new</span> <span class="nf">MyappBuild</span><span class="o">().</span><span class="na">start</span><span class="o">(</span><span class="n">args</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>I like pseudo-declarative DSLs. To make a few that I have specifically liked:</p>
<ul>
<li>Groovy’s one (originally called Builder I think)</li>
<li>QML - QT’s markup language that coulda been a huge thing (has JavaScript as the woven-in non-declarative language)</li>
<li>Kotlin’s one (you can see it used well in <a href="https://tornadofx.io/">TornadoFX</a></li>
<li>Ruby has one too - I forget if it has a name</li>
<li>Flutter (has Dart as the woven-in non-declarative language)</li>
</ul>
<p>Pseudo-declarative means it looks superficially that scripts are declarative, but there is optionally support for if conditions, for loops and regular statements … even if you’d attempt to not use them.</p>
<p>Plain Java does not. You can get close with initializer tricks though - see <a href="http://jmock.org/getting-started.html">JMock2</a> and test frameworks <a href="http://cuppa.forgerock.org/">Cuppa</a> and <a href="https://github.com/mscharhag/oleaster">Oleaster</a>. Hacky but not totally ugly.</p>
<h2 id="a-dsl-version-of-that-bld-example">A DSL version of that Bld example</h2>
<p>Anyway, I thought I would have a quick go at turning Geert’s Bld script into something that approximated the Pseudo declarative grammar I am drawn to:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.List</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="k">new</span> <span class="nf">Project</span><span class="o">()</span>
<span class="o">);</span>
<span class="o">}}.</span><span class="na">start</span><span class="o">(</span><span class="n">args</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Version</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">i</span><span class="o">,</span> <span class="n">i1</span><span class="o">,</span> <span class="n">i2</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">Version</span><span class="o">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">,</span> <span class="kt">int</span> <span class="n">i1</span><span class="o">,</span> <span class="kt">int</span> <span class="n">i2</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">i</span> <span class="o">=</span> <span class="n">i</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">i1</span> <span class="o">=</span> <span class="n">i1</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">i2</span> <span class="o">=</span> <span class="n">i2</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Scope</span> <span class="o">{</span>
<span class="kd">public</span> <span class="nf">Scope</span><span class="o">(</span><span class="nc">String</span> <span class="n">scopeName</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// todo</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">include</span><span class="o">(</span><span class="nc">Dependency</span> <span class="n">dependency</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// todo</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Dependency</span> <span class="o">{</span>
<span class="kd">public</span> <span class="nf">Dependency</span><span class="o">(</span><span class="nc">String</span> <span class="n">scopeName</span><span class="o">,</span> <span class="nc">String</span> <span class="n">s1</span><span class="o">,</span> <span class="nc">Version</span> <span class="n">version</span><span class="o">)</span> <span class="o">{</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Project</span> <span class="o">{</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">MAVEN_CENTRAL</span> <span class="o">=</span> <span class="s">"MAVEN_CENTRAL"</span><span class="o">;</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">RIFE2_RELEASES</span> <span class="o">=</span> <span class="s">"RIFE2_RELEASES"</span><span class="o">;</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">test</span> <span class="o">=</span> <span class="s">"scope_test"</span><span class="o">;</span>
<span class="kd">protected</span> <span class="nc">String</span> <span class="n">pkg</span><span class="o">,</span> <span class="n">name</span><span class="o">,</span> <span class="n">mainClass</span><span class="o">;</span>
<span class="kd">protected</span> <span class="nc">Version</span> <span class="n">version</span><span class="o">;</span>
<span class="kd">protected</span> <span class="kt">boolean</span> <span class="n">downloadSources</span><span class="o">;</span>
<span class="kd">protected</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">repositories</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">start</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// todo</span>
<span class="o">}</span>
<span class="kd">protected</span> <span class="nc">Version</span> <span class="nf">version</span><span class="o">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">,</span> <span class="kt">int</span> <span class="n">i1</span><span class="o">,</span> <span class="kt">int</span> <span class="n">i2</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">Version</span><span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="n">i1</span><span class="o">,</span> <span class="n">i2</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">scope</span><span class="o">(</span><span class="nc">Scope</span> <span class="n">scope</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// todo: register that scope with the project somehow</span>
<span class="o">}</span>
<span class="kd">protected</span> <span class="nc">Dependency</span> <span class="nf">dependency</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">,</span> <span class="nc">String</span> <span class="n">s1</span><span class="o">,</span> <span class="nc">Version</span> <span class="n">version</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">Dependency</span><span class="o">(</span><span class="n">s</span><span class="o">,</span> <span class="n">s1</span><span class="o">,</span> <span class="n">version</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>It doesn’t do anything as <code class="language-plaintext highlighter-rouge">bld.jar</code> was not used for the experiment, and I’ve probably broken compatibility with it. The bit in the main() method is what you should focus on.</p>
<h2 id="more-syntactic-sugar-for-java">More syntactic sugar for Java</h2>
<p>This got me thinking that more sugar could be applied to this section:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scope</span><span class="o">(</span>
<span class="k">new</span> <span class="nf">Scope</span><span class="o">(</span><span class="n">test</span><span class="o">)</span>
<span class="o">);</span>
</code></pre></div></div>
<p>Yup it’s an accident here that it is Bld’s <code class="language-plaintext highlighter-rouge">Scope</code> <strong>class</strong> that’s in the example, and that programmatic scope is focus of conversation.</p>
<p>Does this make it clearer?</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">skope</span><span class="o">(</span>
<span class="k">new</span> <span class="nf">Skope</span><span class="o">(</span><span class="n">test</span><span class="o">)</span>
<span class="o">);</span>
</code></pre></div></div>
<p>What if a future JEP could allow for:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">skope</span><span class="o">(</span><span class="s">"test"</span><span class="o">)</span> <span class="o">;</span>
</code></pre></div></div>
<p>It would need some factory-method-ish clue in the method declaration:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// in Project class</span>
<span class="nd">@FactoryMethodIsh</span>
<span class="kd">protected</span> <span class="nc">Skope</span> <span class="nf">skope</span><span class="o">(</span><span class="nc">String</span> <span class="n">skopeText</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Skope</span> <span class="n">skope</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Skope</span><span class="o">(</span><span class="n">skopeText</span><span class="o">);</span>
<span class="n">project</span><span class="o">.</span><span class="na">addSkope</span><span class="o">(</span><span class="n">skope</span><span class="o">);</span> <span class="c1">// say there was an addSkope member (not shown in code snippets previously)</span>
<span class="k">return</span> <span class="n">skope</span><span class="o">;</span> <span class="c1">// AND allow for ongoing initializer trick.</span>
<span class="o">}</span>
</code></pre></div></div>
<p>The compiler would make the <strong>inline</strong> bytecode equivalent of this:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// as many of these as there were uses of skope(..)</span>
<span class="o">{</span>
<span class="nc">Skope</span> <span class="n">skope123</span> <span class="o">=</span> <span class="k">new</span> <span class="k">new</span> <span class="nc">Skope</span><span class="o">(</span><span class="n">skopeText</span><span class="o">)</span> <span class="o">;</span>
<span class="n">project</span><span class="o">.</span><span class="na">addSkope</span><span class="o">(</span><span class="n">skope123</span><span class="o">);</span>
<span class="c1">// skope123 would be available to following statements IF there were an assignment for the skope(..) use. </span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">skope123</code>, is unique to scope and contrived by the compiler. Could as easily be skope127, etc.</p>
<p>Talking of JEPs one for <a href="https://openjdk.org/jeps/445">main-method syntactic sugar</a> is years late versus C# getting it.</p>
Collecting and Charting Multi-Module Nx Jest Coverage2023-02-15T00:00:00+00:00https://paulhammant.com/2023/02/15/collecting-and-charting-multi-module-nx-jest-coverage<p>Directed graph build technology <a href="https://github.com/nrwl/nx">Nx</a> can run Jest test suites for multiple modules in one go. If you add <code class="language-plaintext highlighter-rouge">--skip-nx-cache --all</code> it will run all the Jest suites in the repo. You might have tacked <code class="language-plaintext highlighter-rouge">--codeCoverage --coverageReporters="json-summary"</code> onto the command, in which case coverage data will have been collected at the same time as test results <strong>to JSON files</strong>. Multiple JSON files for multiple modules leaves you wanting them combined for further use. One such use could be a chart that has lines for each module showing coverage changes up or down over time. Such a chart would allow management to work out where additional tech talent could be added to the team to get coverage up. Up to say 80% and not higher if we’re trying to be reasonable.</p>
<h2 id="some-prior-work">Some prior work</h2>
<p>Nearly ten years back I specified then helped make an AngularJS app that showed commits on a timeline in a way that would allow leadership to work out who was writing too little or too much tests. At least in companies where devs were supposed to write test and production logic at the same time. Test-Driven-Development being a perfectionist way of coding solution. <a href="https://paulhammant.com/2014/10/30/angular-commit-bubbles/">Angular Commit Bubbles</a> was the result. I picked AngularJS and SVG for the solution and paired with Partha Sarkar towards completion:</p>
<p><img src="https://paulhammant.com/images/angular-commit-bubbles.jpg" alt="" /></p>
<p>Now, at the same time colleague <a href="https://twitter.com/pbadenski">Pawel Badenski</a> made a D3 solution that was <strong>much more impressive</strong>. Less lines of code and more elegant counts as ‘more impressive’ to many. Pawel’s version:</p>
<p><a href="https://pbadenski.github.io/d3-commit-bubbles/commits.html"><img src="https://paulhammant.com/images/pawelsbubbles.png" alt="" /></a>
(click to go through to the live app)</p>
<h2 id="back-to-now">Back to now</h2>
<p>So the new idea was to use D3 to chart multiple modules of Jest coverage over time. Then with <a href="https://twitter.com/charltonaustin">Charlton Austin</a> specifying and funding an UpWork contract, we thought we would see if we could get it delivered for $150. Here’s the repo with the solution <a href="https://github.com/paul-hammant/jest-coverage-chart">jest-coverage-chart</a>. The finished product is much more code than Pawel’s solution from way back. It is also missing some “select all” and “select none” that would be needed. It probably needs a rewrite to be more canonical D3. As it happens, Charlton was at the same client site in Wilmingon, DE, back when the “commit bubbles” work happened.</p>
<p>Here it the result for the Nx team’s <a href="https://github.com/nrwl/nx-examples">nx-examples</a> demo repo. Nearly all of 2022 is represented with intervals of a month between sampled commits (give or take):</p>
<p><a href="https://paul-hammant.github.io/jest-coverage-chart"><img src="https://paulhammant.com/images/coverage_plot.png" alt="" /></a>
(Click to go through to the live app)</p>
<p>One more example for <a href="https://github.com/stefanoslig/angular-ngrx-nx-realworld-example-app">stefanoslig/angular-ngrx-nx-realworld-example-app</a>:</p>
<p><img src="https://paulhammant.com/images/stefanoslig-angular-ngrx-nx-realworld-example-app.png" alt="" />
(no live app for this)</p>
<p>The tech to collect coverage over time is just some bash (in the repo README). Were the Nx team to reimplement this in TypeScript (they should), then it could be closer to utility a further from experimental than it is now. That bash is in the README for the repo, and would need to be tweaked for each repo it would collect data for (from/to dates, interval, branch name).</p>
<p>Ir you set this up for real it would not need to do this every commit. Maybe just daily. You could also down-sample to weekly datapoints and represent min/max/average as error bars.</p>
<p>A midnight cloud job would collect new data into bucket storage, then recreate the main JSON for the chart at the end. That would be from the collection of JSON docs already in the bucket plus the new datapoint. You could alternatively store the JSON back in the repo if you were happy for cloud jobs to do pushes to repos. Future audit standards might wish for coverage data to be stored in history-retaining merkle trees (Git) in order to prove correlation to code. <a href="https://paulhammant.com/2018/05/30/better-practices-for-audits">As I blogged before</a>.</p>
Merkleized Public Attestations2022-08-14T00:00:00+00:00https://paulhammant.com/2022/08/14/merkleized-public-attestations<p>A single case - Applicant Tracking systems used in recruitment</p>
<p>Get far enough into your career, and you will have used a fair range of recruitment application. Many people will hate all they’ve used, but one less so. I’ve not used one that I’ve really liked, though I have had demonstrations of a couple that looked good. What I really pine for is an <strong>Applicant Tracking System</strong> (ATS) that’s as good for candidates as it is for prospective employers, while also making it easy and fair for recruitment agents (commission earners). That would mean features and experience that each of teh groups would need. What actually happens is that mid-sized and large employers select applications (Web SaaS these days) that matches their own needs. That’s the recruitment subgroup of HR primarily, and maybe hiring managers secondarily. Recruitment agents who are on the preferred supplier list (PSL) most likely just have to grin their way through the lack of features for them and get quietly busy with lots copy/paste between the employer’s ATS and their own. What they really need is a standard and some interoperation that synchronizes updates between the two. That would be quite doable, if you could get the parties to agree.</p>
<p>Candidates though looking for status updates, are quite often only interoperating with agents or prospective via telephone and email. Well at least after initial upload of CV/Resume during registration of an interest in an open position. Indeed, if there is a recruitment agent. then the candidate deals with them for status updates. The cracks in that sidewalk are around wish for all parties to be kept informed soon after state changes. One party can often be chasing another. There’s a need to be trusting what they say re updates. As a candidate, one of your fears is the recruitment agent isn’t on the PSL for a prospective employer, but said they were to you. The agent would be using a candidates great CV to open the door to an employer and make them a client. That may not work, but the agent doesn’t tell the candidate. Instead, they tell them the client wasn’t interested but that they have other roles/clients. Sometimes the employer gets a CV and doesn’t respond to the agent with a yes or no. Or doesn’t do so quickly. The agent has to assure tha candidate that they’re chasing for that answer without throwing their client under the bus. Those and many other cracks in a three-party system to land a job.</p>
<p>What would be nice would be if all the state changes were provably shared three ways. <strong>That’s the attestation of the title</strong>. The claim, the parties and the date all visible online <strong>without authentication</strong>. Using one-way hashes of claims-made + parties + date, we could see a system where the prospective employer publishes a hash of that without the un-hashed version of the same. That publication would be over HTTPS, obviously. It could as easily be static in lieu of the ATS getting the dynamic features needed.</p>
<p>I’ve prototyped something: <a href="https://paul-hammant.github.io/KickAssRecruitment">GitHub Pages static site</a> - the source for that <a href="https://github.com/paul-hammant/KickAssRecruitment">GitHub Project</a>. Shown is a fictionalized application and consideration of me for Tesla as CTO. <strong>Not shown is any other feature of an ATS</strong>: candidate review, advancing/rejecting candidates, form-responses, workflows, or anything. I link to Tesla’s site as if they were participating in this demo - they would be witnessing the claims/statements (attestations) made about my candidacy. The three records shown in this ATS would have continued to conclusion, but you hopefully get the gist at three entries. It’s ugly so far, sorry:</p>
<p><img src="https://paulhammant.com/images/kick-ass.png" alt="" /></p>
<p>Each record/row is an update to my fake Tesla candidacy. A new claim is made and with the previous SHA1 hash, a new hash is created from that. Going forward in time it is a merkle tree. The body of each hosted hash in the ATS also contains a link to the next entry in the chain, but not the text that was used to create the hash. Those “next entry” hashes added later if they happen at all (follow-up interviews, rejections, test results, offers, acceptances). Of course, this demo is a little blockchain like. There’s no distributed byzantine consensus though as it was not needed. Distributed Byzantine consensus is a feature of regular blockchains. It is just a vanilla merkle tree with some JavaScript for in-browser verification. Try changing the <code class="language-plaintext highlighter-rouge">/#</code> part of URL <strong>a little</strong> (just a word or two) and reloading the page. You will get some indication of unverified claims that way. Removing the <code class="language-plaintext highlighter-rouge">/#</code> part of a URL completely and reloading will show you what happens when there’s no claim at all that could be verified/attested.</p>
<p>The ATS might nuke all entries for a candidacy after six months, or as soon as the application has concluded. That seems like a configuration choice for the employer.</p>
<p>Tesla would host each of the SHA1s for each update. In a backend system they <strong>know</strong> the plain-text that created the SHA1, but they never share that in any way outside their HR team. They just share the SHA1 which is linked to by the agent’s ATS (which is an evolution of the app shown above). The agent’s ATS, subject to permissions for the logged-in user, would show the text too, and allow you to verify that they one-way hashing is correct, then confirm that the prospective client sees that too.</p>
<p>If the employer was in the EU/UK and GDPR allowed for a candidate to discover what was stored about them, the test and the hashes would be made available, but the candidate would have <strong>already had them</strong>. Of course, HR teams keep interview notes/feedback that doesn’t get sent by default to the candidate. “Paul knows nothing about cars” is something Tesla staff could have written internally in the ATS, but not passed back to me with the “Thanks for your candidacy, but at this stage we are going with other candidates”. In the EU/UK could still ask for the full commentary, so interviews are asked to “keep it legal”.</p>
<p>So in the end the prototype app isn’t so much an ATS syncing standard that allows agent and client ATSs to stay in step, it is more of system of safely keeping track of top-level events in public. I had wondered whether this could be a startup, and I don’t think it is. It would be a neat idea for existing ATSs to adopt though, and it is not a lot of code as it is quite a straightforward un-blockchain implementation.</p>
<p>At some point transactions systems including card purchases & receipts, corporate expenses, all get merkleized presences online. Monthly statements too. Tax collection starts to use it, and more. All hosted by the key participants and not shoved into a public blockchain as that’s not needed.</p>
Principles of Containment in the DOM2022-08-12T00:00:00+00:00https://paulhammant.com/2022/08/12/principles-of-containment-in-the-dom<p>I guess this follows on from my <a href="https://paulhammant.com/2016/12/14/principles-of-containment">principles of containment</a> (2016) blog entry.</p>
<p>There is an aspect of iFrames that is interesting architecturally: a ‘sandbox’ feature. You can embed an iFrame in a page
and have it isolated from the rest of the page, even if all the pieces of the page are served from the “same origin”.</p>
<h1 id="prototype-diagramming-tool-that-uses-that-sandbox-for-tools">Prototype diagramming tool that uses that sandbox for “tools”</h1>
<p><a href="https://paul-hammant.github.io/cs_diagramming/">Here statically hosted in GitHub-Pages</a>. Here’s the <a href="https://github.com/paul-hammant/cs_diagramming">source for the same</a>. Here’s what it looks like in operation:</p>
<p><img src="https://paulhammant.com/images/csd1.png" alt="" /></p>
<p><img src="https://paulhammant.com/images/csd2.png" alt="" /></p>
<p><img src="https://paulhammant.com/images/csd3.png" alt="" /></p>
<p><img src="https://paulhammant.com/images/csd4.png" alt="" /></p>
<p>In this prototype there are two toolbar buttons that perform changes on the SVG canvas in the main frame. One of those is shown being actioned above. The JavaScript logic for the two buttons are in two different iFrame overlays that are isolated from the main frame that shows the canvas (and the buttons that would launch the toolbar overlays). Acronyms API, WebAPI, and API has some <a href="https://martinfowler.com/bliki/SemanticDiffusion.html">semantic diffusion</a> these days, but in this context there’s an ABI that allows for the sandboxed iframe controls for the toolbar functions to invoke queries and changes on the SVG canvas. It still needs some small security work (see source code for note). Where there are a bunch of open-source RPC technologies GitHub that work via <code class="language-plaintext highlighter-rouge">window.postMessage(..)</code> and through the sandbox, I’m not using them for a prototype. Instead, I’m just using JQuery expressions that can be passed to <code class="language-plaintext highlighter-rouge">eval(..)</code> in the outer window. The inner sandboxed iframes do not themselves have JQuery in their loaded set of JavaScript libs, but they are able to send strings over <code class="language-plaintext highlighter-rouge">window.postMessage()</code> just fine.</p>
<p>Back to the two buttons…</p>
<p>The color picker just changes the color of all the shapes in the canvas. Not a selected subset as yet.<br />
The wire operation is:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>window.parent.postMessage("$('#Canvas_Layer_1 [fill]').attr('fill', '" + hex + "');", "*");
</code></pre></div></div>
<p><a href="https://github.com/paul-hammant/cs_diagramming/blob/master/jscolor-picker/jscolor-a2cafbbfc1.html#L19">source</a></p>
<p>The other tool makes all the stroke widths (line widths) 1 point bigger or smaller. That’s two wire operations. First:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>window.parent.postMessage("? $.uniqueSort($('#Canvas_Layer_1 [stroke-width]').map(function(x) {
return this.getAttribute('stroke-width'); }).get().sort()).join();", "*");
</code></pre></div></div>
<p><a href="https://github.com/paul-hammant/cs_diagramming/blob/master/stroke-width-more-less/index.html#L19">source</a></p>
<p>Then some for-loop javascript in the sandbox, then:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>window.parent.postMessage("$(\"#Canvas_Layer_1 [stroke-width*='" + w + "']\")
.attr('stroke-width', " + (w + diff) + ");", "*");
</code></pre></div></div>
<p><a href="https://github.com/paul-hammant/cs_diagramming/blob/master/stroke-width-more-less/index.html#L7">source</a></p>
<p>Variable <code class="language-plaintext highlighter-rouge">w</code> the ‘from’ width in question. Initially that’s 4 then 1 for the line-widths returned by the query operation when making the lines <strong>bigger</strong>, or 1 then 4 when making the lines <strong>smaller</strong>. Variable <code class="language-plaintext highlighter-rouge">diff</code> is either +1 or -1 for the ‘more’ or ‘less’ functions respectively.</p>
<p>As I say, the JQuery via eval() needs some security work. Maybe some consultation with the JQuery team about suitability via <code class="language-plaintext highlighter-rouge">window.postMessage()</code> because there is a possibility it is totally flawed.</p>
<h1 id="overall-experience">Overall experience</h1>
<p>A system that delivered an embeddable SVG canvas into a larger editing experience like GoogleDocs would be able to allow the addition of plugins to do discrete operations on the SVG without the elaborate integration work we see today. No SAML, SSO, Consent/Auth, Next->next->next work. That’s all error-prone and frankly confusing even to software developers who make the same technologies. See “rationale” below.</p>
<p>What you could have here is a simple per-document workflow like “Add Corel Shadow Effects (TM) tools to this GoogleDoc diagram”. If those were <strong>not</strong> free, have Google handle the micropayments on each actual use. Then, remove the same tools another day, but still have the SVG shapes <strong>as changed</strong> by the same tools had you used them. You’d gain comforts from knowing the resulting SVG source was your guarantee of long-term viability of the diagram. You would gain more comforts knowing that only editors of the SVG diagrams would need the plugins - readers could see the finished result just fine. Heck, document editors who were focussed on the written prose and not the fine-grained diagram editing could still make less fine-grained changes to the SVG diagram and not wreck the changes made by someone else prior to that moment with the tools/plugins installed for them.</p>
<h2 id="glass-ceilings">Glass Ceilings</h2>
<p>Inside the DOM of the browser many straight operations on the SVG representation of a diagram just work. It’s not a perfect world though. If you wanted to make an “align top edges” toolbar item, you would have trouble. As some SVG shapes are made from Bézier curves, it is not clear what the bounding top-left or bottom-right coordinates are until it is actually rendered. You simply can’t pick those details from the implicit <code class="language-plaintext highlighter-rouge"><shape/></code> element like I have done for the two tools already. You could try to run your own engine to render the same shape in an off-screen way, but that seems like folly. Better to query the browser’s own actual rendered view of the shape. That might feel like it is browser-specific though even if there are hints in the browser API documentation: <a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMRect">DOMRect</a> vs <a href="https://developer.mozilla.org/en-US/docs/Web/API/SVGRect">SVGRect</a>. I tried a little and gave up as a blog entry is my goal not seed funding for a startup that Google would acquire.</p>
<h1 id="history-of-interprocess-communication---arexx-in-particular">History of interprocess communication - ARexx in particular.</h1>
<p>JQuery is a very expressive thing over this <code class="language-plaintext highlighter-rouge">window.postMessage()</code> ABI/API.</p>
<p>Back in 1987 there was home computer called the Amiga from Commodore that had a language that was installable called ARexx and good for interprocess communication. See <a href="https://wiki.amigaos.net/wiki/AmigaOS_Manual:_Python_Interprocess_communication_through_ARexx_ports">AmigaOS Manual: Python Interprocess communication through ARexx ports</a> - even though that’s recent (the Amiga still has fans) and talks of Python too, it’ll give you an understanding of the wire commands that were available for this machine/OS before the world wide web. By version 2 of AmigaOS, it was bundled. ARexx on the Amiga for interprocess-control was an amazing experience. Use of JQuery over this <code class="language-plaintext highlighter-rouge">window.postMessage()</code> <em>wire</em> reminds me of that. Sure, ‘eval’ situations for <strong>all</strong> languages have had their security challenges and ARexx would have had dozens had the Amiga been a contender beyond the 80s. The other problem with ARexx “port” way of connection two process is that the principle of containment didn’t apply - it was wide open - any process could chat to and launch any other.</p>
<h1 id="rationale">Rationale</h1>
<p>For in-document diagramming, all of Mural, Miro, Diagrams.net, Lucid-Charts (and their GoogleDocs integrations), and are terrible experiences. Microsoft Sharepoint and MsWord don’t have an obvious high-capability diagramming tech, either. On that last, I have access to a version of Visio from 2009 in a corporate setting, but not anything more recent because it is incredibly expensive (and Windows only). Those techs are mostly terrible because of the “integration” setup, but also because of the lock in to the technology after you have committed to it. As you’re going through those integrations steps, your mind wonders what else from your cloud-stored files can the vendor now see or change without you knowing. I mean those doubts that build up and make you wonder if you shouldn’t just read a book made from trees. The lock-in aspects of these tools are also terrible. At least they are from a long term knowledge-management point of view.</p>
<p>We have a standard in SVG and supported in all modern browsers. It is the best interoperable standard for drawing that we have in 2022. Not just for representation in a browser, but for canonical save-state, and rendering into PDF or actual print. You can have a SVG canvas in a page, have that canvas saved/loaded by the underlying platform itself (say GoogleDocs). Then you could have multiple plugins provide editing services for it in-situ in the page. It need not be an either-or choice of which plugin it, could be many of them. They could be free or paid. Either way, an emerging standard that’s needed.</p>
<p>More backstory: <a href="https://twitter.com/paul_hammant/status/1555453984567066624">Me whining about Miro on Twitter</a> a week ago. <a href="https://twitter.com/paul_hammant/status/1549874906338410501">About GoogleDocs drawings</a> a few weeks ago, and <a href="https://twitter.com/paul_hammant/status/1547891550461407232">Chromebook, pens and drawing apps</a> that has videos from a month ago.</p>
More on Depth-first recursive vs DAG build technologies2022-04-13T00:00:00+00:00https://paulhammant.com/2022/04/13/more-on-depth-first-recursive-vs-dag-build-techs<p>Well it turns out I was wrong in the blog entry a few days ago - Bazel doesn’t do a single compiler invocation for the set of impacted BUILD modules for a target. At least for Java. It invokes <code class="language-plaintext highlighter-rouge">javac</code> once per BUILD module in most-depended-on oder towards the intention of the build target. On the way it makes mini libs (jars). I could be wrong about that too, if my Bazel skills are so bad that I missed built-in ways of working.</p>
<p>Remember PicoContainer from 16 years back? Well JetBrains do - it’s wedged inside their java-based IDEs. I’ve pulled it out here: <a href="https://github.com/picocontainer/PicoContainer1.9">github.com/picocontainer/PicoContainer1.9</a>. JetBrains did a bunch of work based on what they pulled in from their forking of the PicoContainer 1.1. Generics work and more. They also change package structure to suit them. This isn’t so important. They kept the original author tags: Aslak Hellesøy, Jörg Schaible, Jon Tirsén, Mauro Talevi, Paul Hammant, Thomas Heller. A few more contributed, but we didn’t put them in author tags.</p>
<p>Back in the day, PicoContainer 1.1 had Maven as a build tech (depth-first-recursive). With this extraction, it now has three alternate build techs:</p>
<ol>
<li>Maven (depth-first recursive)</li>
<li>Bazel (directed acyclic graph)</li>
<li>Bash scripts executing vanilla Java JDK commands with variations (neither depth-first recursive nor DAG)</li>
</ol>
<p>The source tree ALSO contains a source file that doesn’t compile. This is just for the purposes of this blog entry:</p>
<p><img src="https://user-images.githubusercontent.com/82182/163056846-899bcdcc-61aa-408c-8a7c-a38e310d3190.png" alt="" /></p>
<p>The Maven build masks out that source file via compiler-plugin config:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <excludes>
<exclude>**/RedHerring.java</exclude>
</excludes>
</code></pre></div></div>
<p>It is important to note that this maven build isn’t multi-module, so depth-first recursive doesn’t apply.</p>
<p>Bazel build notes nothing depends (a DAG remember) on the BUILD module that contains that source file so never builds it unless it is explicitly mentioned. Lots of trial and error to get the multiple Bazel BUILD files right. In this effort, I found <a href="https://github.com/bazelbuild/buildtools/blob/master/unused_deps/">unused_deps</a> to be very helpful.</p>
<p>Targets:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bazel build //src/java/com/intellij/util/pico:PicoContainer_deploy.jar
# or
bazel test //src/test/org/picocontainer/tests/integration:tests
</code></pre></div></div>
<p>I also have three shell scrips invoking javac, java and jar in order.</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">naive_classic_build.sh</code></li>
<li><code class="language-plaintext highlighter-rouge">naive_masked_build.sh</code></li>
<li><code class="language-plaintext highlighter-rouge">wise_classic_build.sh</code></li>
</ol>
<p>The first one chokes on the bad source file, by design. The remaining two builds (and the maven & bazel ones) make a jar of PicoContainer’s classes after running a single test. Two different strategies for picking source files. The “wise” one uses a build in feature of <code class="language-plaintext highlighter-rouge">javac</code> that can work out what else needs to compiled in that invocation. The “masked” one uses “sed” to eliminate <code class="language-plaintext highlighter-rouge">RedHerring.java</code> just before invoking the compiler.</p>
<h1 id="mucking-around-with-gits-sparse-checkout">Mucking around with Git’s Sparse Checkout</h1>
<p>One of the features of Google’s Blaze that has not made it into Bazel yet, is the build-in subsetting of the source tree (monorepo nirvana).</p>
<p>Git’s “Sparse Checkout” feature came with Git 2.25 (Jan 2020). I had listed it as something that was needed in <a href="https://paulhammant.com/2019/06/14/merkle-trees-and-source-control/">June 2019</a> but who knows whether the git leads read my blog entry. See also my <a href="https://paulhammant.com/2020/01/19/vcs-nirvana/">VCS-nirvana update to the same list</a> in Jan 2020.</p>
<p>We can play with sparse checkout to mimic Google monorepo life:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git sparse-checkout init --cone
git sparse-checkout set third_party src/java/org/picocontainer/defaults "src/java/org/picocontainer/*.java" "src/java/org/picocontainer/BUILD" src/java/org/jetbrains src/java/com src/test README.md pom.xml "*.sh" WORKSPACE ".all*"
</code></pre></div></div>
<p>This elaborate modification to the checkout allows the <code class="language-plaintext highlighter-rouge">RedHerring.java</code> to be ignored in <em>all</em> situations. Specifically <code class="language-plaintext highlighter-rouge">./naive_classic_build.sh</code> build <strong>passes instead of fails</strong>. Whereas <code class="language-plaintext highlighter-rouge">naive_masked_build.sh</code> asked out the reg herring via sed trick, this new way doesn’t need to exclude the dir/file as it is no longer there in the file system. It is still in the ./git/ backing store, but not in the “checkout”.</p>
<p>Git sparse checkout has ‘set’ for a big list as above. This smushes prior settings each time you use it. It also has ‘add’ which adds new patterns to the list it had before, instead of smushing. I think a ‘remove’ sub-command is needed. My case above could be just:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git sparse-checkout init --cone
git sparse-checkout remove src/java/org/picocontainer/redherring
</code></pre></div></div>
<p>Mucking around this way, gives you a glimpse of what Google’s Blaze would readily do for committers in their monorepo subsetting down from many hundreds of different team’s permutation of directories for meaningful buildable deployables.</p>
<p>Note that the Maven build still works for the sparse checkout, as does <code class="language-plaintext highlighter-rouge">wise_classic_build.sh</code> and <code class="language-plaintext highlighter-rouge">naive_masked_build.sh</code>. Nothing changed from their point of view.</p>
<p>When you’ve finished mucking around, this resets you back to non-sparse.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git sparse-checkout disable
</code></pre></div></div>
<p>Also, in closing, I still think a nirvana monorepo build system would do a single invocation of the compiler where the target/intention supporting that.</p>
Depth-first recursive vs DAG build technologies2022-04-08T00:00:00+00:00https://paulhammant.com/2022/04/08/depth-first-recursive-vs-dag-build-techs<p>For many years I’ve been contrasting depth-first recursive build technologies with directed graph types.</p>
<p>In this blog entry I outline then discuss pros and cons</p>
<h2 id="classic-depth-first-recursive">Classic depth-first recursive</h2>
<p><img src="https://paulhammant.com/images/dfr.png" alt="" /></p>
<p>Stylized directory representation</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>build-file
fooapp/
build-file
src/
prod/
fooapp/ (many source files)
test/
fooapp/ (many source files)
a/
build-file
src/
prod/
fooapp/a/ (many source files)
test/
fooapp/a/ (many source files)
a1/
build-file
src/
prod/
fooapp/a/a1/ (many source files)
test/
fooapp/a/a1/ (many source files)
a2/
build-file
src/
prod/
fooapp/a/a2/ (many source files)
test/
fooapp/a/a2/ (many source files)
b/
build-file
src/
prod/
fooapp/b/ (many source files)
test/
fooapp/b/ (many source files)
</code></pre></div></div>
<p>If you built a regular ‘compile’ intention from root, the order is: <code class="language-plaintext highlighter-rouge">a1</code> prod source, <code class="language-plaintext highlighter-rouge">a2</code> prod source, <code class="language-plaintext highlighter-rouge">a</code> prod source, <code class="language-plaintext highlighter-rouge">b</code> prod source, then the root module prod source.</p>
<p>If you built a ‘test’ target/intention from root, the order is:</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">a1</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">a1</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">a1</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">a</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">a</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">a</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">b</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">b</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">b</code> test execution</li>
<li>root module compile prod source</li>
<li>root module compile test source</li>
<li>root module test execution</li>
</ol>
<p>Any of those stages failing and if stops right there with output telling you want went wrong.</p>
<p>If you built a ‘binary/package’ target/intention from root, the order is:</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">a1</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">a1</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">a1</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">a1</code> binary creation</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">a2</code> binary creation</li>
<li><code class="language-plaintext highlighter-rouge">a</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">a</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">a</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">a</code> binary creation</li>
<li><code class="language-plaintext highlighter-rouge">b</code> compile prod source</li>
<li><code class="language-plaintext highlighter-rouge">b</code> compile test source</li>
<li><code class="language-plaintext highlighter-rouge">b</code> test execution</li>
<li><code class="language-plaintext highlighter-rouge">b</code> binary creation</li>
<li>root module compile prod source</li>
<li>root module compile test source</li>
<li>root module test execution</li>
<li>root module binary creation</li>
</ol>
<p>As before, any of those stages failing and if stops right there with output telling you want went wrong.</p>
<p>Depth-first recursive build technologies validate the entire tree to some degree. Module level deps is the onus for that. As the build kicks off and (say) <code class="language-plaintext highlighter-rouge">a1</code> compilation happens, other classes of dependency may be noted as missing by the language’s compiler at that stage.</p>
<h2 id="directed-graph-google-style-monorepo">Directed graph (Google-style monorepo)</h2>
<p><img src="https://paulhammant.com/images/dg.png" alt="" /></p>
<p>Stylized directory representation</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>build-file.sh
prod/
fooapp/
build-meta-info
(many source files)
a/
build-meta-info
(many source files)
a1/
build-meta-info
(many source files)
a2/
build-meta-info
(many source files)
b/
build-meta-info
(many source files)
(others)
test/
fooapp/
build-meta-info
(many source files)
a/
build-meta-info
(many source files)
a1/
build-meta-info
(many source files)
a2/
build-meta-info
(many source files)
b/
build-meta-info
(many source files)
(others)
</code></pre></div></div>
<p>If you built a regular ‘compile’ intention from root, the best case is: prod source for a1, a2, a, and b all compiled together.</p>
<p>If you built a ‘test’ target/intention from root, the best case order is:</p>
<ol>
<li>prod source for <code class="language-plaintext highlighter-rouge">a1</code>, <code class="language-plaintext highlighter-rouge">a2</code>, <code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">b</code> and the root module all compiled together</li>
<li>test source for <code class="language-plaintext highlighter-rouge">a1</code>, <code class="language-plaintext highlighter-rouge">a2</code>, <code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">b</code> and the root module all compiled together</li>
<li>test execution of <code class="language-plaintext highlighter-rouge">a1</code>, <code class="language-plaintext highlighter-rouge">a2</code>, <code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">b</code> and the root module all together</li>
</ol>
<p>If you built a ‘binary/package’ target/intention from root, the order is those three above and:</p>
<ol>
<li>binary created for shippable/deployable root module <code class="language-plaintext highlighter-rouge">fooapp</code> and it’s deps (<code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">a1</code>, <code class="language-plaintext highlighter-rouge">a2</code> & <code class="language-plaintext highlighter-rouge">b</code> … also third party deps, but we’re not going into that today)</li>
</ol>
<p>Directed graph build systems can validate the graph of dependencies without compilation. For example, a source file may have an import/using/include of <code class="language-plaintext highlighter-rouge">fooapp/z/Zed.java</code> and nowhere in the current set of declared dependencies is that found. The invalidity can be flagged to the developer sooner: the build-meta-info should be updated to include the dep (source of binary). Likewise, a dependency in the build-meta-info might not be needed, and it is possible to get the build technology to report too: you’d take that out, maybe.</p>
<h2 id="compared">Compared</h2>
<p>Note: “build” to me doesn’t just mean compile when I use it without qualifier. It means compile AND all impacted tests AND package steps.</p>
<p>The depth-first recursive build technologies can have optional parameters for larger traversals from root.</p>
<ul>
<li>skip all tests, but still make the binary/binaries</li>
<li>only run tests that match a specific pattern - a subset of all tests, or maybe just a single test</li>
</ul>
<p>The depth-first recursive build technologies allow you to do an initial full build of everything, then cd into a sub dir for a more focussed set of activities: <code class="language-plaintext highlighter-rouge">cd a/a2; #build commands now affect that that module</code></p>
<p>Directed graph build technologies can ALSO work with classic modular source tree layout (more common with depth-first recursive). That would leverage a depth-first traversal that is more in line with that class of build tech. Purists would say this is second class.</p>
<p>Directed graph build systems classically work from the root of the checkout: <code class="language-plaintext highlighter-rouge">build-command tests a/a2/*</code> would compile <code class="language-plaintext highlighter-rouge">a1</code> and <code class="language-plaintext highlighter-rouge">a2</code>, then execute the tests in <code class="language-plaintext highlighter-rouge">a2</code> (and nothing else). It is a key feature of these technologies that they skip source directories that are not pertinent to the target. In Google ‘adsense’ and ‘adwords’ were adjacent Java packages in both the prod source tree and the test source tree. Your checkout could have one or both - you blaze target would ignore source present on your filesystem in directories (packages for Java) if the target didn’t need it. By contrast, if a directed graph build system steps into a module compile is going to happen for all present (subject to cache) without exclusions of packages within.</p>
<p>Both systems could leverage a build cache and not repeat compile steps for modules that have already been compiles for the sources in question. Not all build technologies have nailed the repeatability needed to implement this.</p>
<h3 id="circular-references">Circular references</h3>
<p>Do either of the two allow for circular references?</p>
<p>No, not at the module level where a thing is buildable on its own or has build meta info. Both classes of build technology fail early in an attempt to do compile, test, or package.</p>
<p>Some languages do allow circular references. e.g. <code class="language-plaintext highlighter-rouge">Laurel.java</code> can depend on <code class="language-plaintext highlighter-rouge">Hardy.java</code> which can depend on <code class="language-plaintext highlighter-rouge">Laurel.java</code> but that’s a different thing and can only happen in one <code class="language-plaintext highlighter-rouge">javac</code> invocation (one module)</p>
<p>Note: <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">Directed acyclic graph</a> (DAG) applies in this context to what I am titling “directed graph build techs”.</p>
<h3 id="running-impacted-tests-only">Running impacted tests only</h3>
<p>I wrote for Martin Fowler’s ‘bliki’: <a href="https://martinfowler.com/articles/rise-test-impact-analysis.html">The Rise of Test Impact Analysis</a> (2017). This is about shorter build times that are still provably sufficient.</p>
<p>Directed graph build systems can target impacted tests far more easily using build-meta-info that’s already in the source tree. Sure if that meta info is a multi-year mess, then impacted tests only might be a mirage.</p>
<p>For depth-first recursive build technologies it’s harder to implement this capability. Indeed, it may require training to calculate the maps. Training would be non-CI repeated runs of builds, and the resulting meta info requires storage in order to be used by the team. I would say inside VCS and co-located with the modules it pertains to, but then I’m obsession with source-control as a backing store.</p>
<h3 id="subsetting-the-checkout-expandcontract">Subsetting the checkout (expand/contract)</h3>
<p>Blog entries:</p>
<ul>
<li><a href="https://paulhammant.com/2014/01/06/googlers-subset-their-trunk/">Googlers Subset their Trunk</a> (2014)</li>
<li><a href="https://paulhammant.com/2015/05/20/turning-bazel-back-into-blaze-for-monorepo-nirvana/">Turning Bazel back into Blaze for monorepo nirvana</a> (2015)</li>
</ul>
<p>Directed graph build systems can casually skip modules present in the file system, but not implicated in the build target. Google’s in-house Blaze can remove the unneeded directories or add them with simple commands. The build target execution time doesn’t change but the number of source files can be reduced speeding up your chosen IDE.</p>
<p>I attempted to do something similar for Java’s Maven build-tech and Git. Blog entry <a href="https://paulhammant.com/2017/02/08/further-experiments-with-expanding-contracting-monorepos/">Further Experiments With Expanding/Contracting Monorepos</a> (2017).</p>
<h1 id="directed-graph-build-tech-choices">Directed graph build-tech choices</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Tech</th>
<th>Years</th>
<th>Open source</th>
<th>expand/contract</th>
<th>Build-in 3GL #</th>
<th>Community 3GL #</th>
</tr>
</thead>
<tbody>
<tr>
<td>Blaze</td>
<td>2004 -</td>
<td>No, inside Google</td>
<td>yes</td>
<td>5+</td>
<td> </td>
</tr>
<tr>
<td>Bazel</td>
<td>2010 -</td>
<td><a href="https://bazel.build">Yes</a></td>
<td> </td>
<td>14</td>
<td>8</td>
</tr>
<tr>
<td>Nx</td>
<td>2018 -</td>
<td><a href="https://nx.dev/">Yes</a></td>
<td> </td>
<td>2</td>
<td>2</td>
</tr>
<tr>
<td>Pants 1</td>
<td>2012 - 2021</td>
<td><a href="https://v1.pantsbuild.org/">Yes</a></td>
<td> </td>
<td>4</td>
<td> </td>
</tr>
<tr>
<td>Pants 2</td>
<td>2020</td>
<td><a href="https://pantsbuild.org/">Yes</a></td>
<td> </td>
<td>4</td>
<td> </td>
</tr>
<tr>
<td>Please</td>
<td>2016</td>
<td><a href="https://please.build/">Yes</a></td>
<td> </td>
<td>4</td>
<td> </td>
</tr>
<tr>
<td>Lage</td>
<td> </td>
<td><a href="https://microsoft.github.io/lage">Yes</a></td>
<td> </td>
<td>?</td>
<td> </td>
</tr>
<tr>
<td>Rush</td>
<td> </td>
<td><a href="https://rushjs.io/">Yes</a></td>
<td> </td>
<td>2 (JS & TS)</td>
<td> </td>
</tr>
<tr>
<td>Turborepo</td>
<td>2021 -</td>
<td><a href="https://turborepo.org/">Yes</a></td>
<td> </td>
<td>2 (JS & TS)</td>
<td> </td>
</tr>
<tr>
<td>Buck</td>
<td>2013 -</td>
<td><a href="https://buck.build/">Yes</a></td>
<td> </td>
<td>15</td>
<td> </td>
</tr>
</tbody>
</table>
<p>TODO: help me fill these in folks ==> paul@hammant.org</p>