<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jqno.nl/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jqno.nl/" rel="alternate" type="text/html" /><updated>2026-03-27T18:39:32+00:00</updated><id>https://jqno.nl/feed.xml</id><title type="html">tr ouwens</title><subtitle>Jan Ouwens ▷ tr ouwens ▷ by the way, things I want to say</subtitle><author><name>Jan Ouwens</name></author><entry><title type="html">Old man yells at cloud db schema</title><link href="https://jqno.nl/post/2026/02/17/old-man-yells-at-cloud-db-schema/" rel="alternate" type="text/html" title="Old man yells at cloud db schema" /><published>2026-02-17T00:00:00+00:00</published><updated>2026-02-17T00:00:00+00:00</updated><id>https://jqno.nl/post/2026/02/17/old-man-yells-at-cloud-db-schema</id><content type="html" xml:base="https://jqno.nl/post/2026/02/17/old-man-yells-at-cloud-db-schema/"><![CDATA[<p>I have four rules that I follow when I define a relational database schema:</p>

<ol>
  <li>Implement <a href="https://github.com/flyway/flyway">Flyway</a> from the start to avoid future regret</li>
  <li>Slap a separate primary key field on every table</li>
  <li>Yes, this includes join tables</li>
  <li>Be consistent</li>
</ol>

<p>These rules have served me well over the years. Yes, there are always exceptions to any rule, but I believe it’s good to follow these unless you have a good reason not to.</p>

<p><img src="/images/2026-02-17-old-man-yells-at-cloud-db-schema/old-man-yells-at-cloud.webp" alt="Meme - old man yells at cloud" /></p>

<p>Coincidentally, these rules have come up surprisingly often for me in recent years in conversations with teammates who tend to be smart and very experienced with tools like Kafka or Redis or Cassandra, but haven’t worked with relational database because I’m old and they’re not. Let’s call these teammates Chad, because addressing a group of people with a single name is a very reasonable thing to do.</p>

<p>Of course Chad can totally pull up a PostgreSQL in <code class="language-plaintext highlighter-rouge">$preferred_cloud_provider</code> and make it go brr, but they don’t have the painful experiences I’ve had. Thankfully, I very much enjoy nitpicking their PRs and my rules come up every time I do. I’ve discussed them with three different Chads in the past year alone.</p>

<p>My rules are laughably easy to implement in a fresh database, but extremely difficult and expensive when the database has been running in prod for some time. So let’s go over each one.</p>

<h2 id="1-implement-flyway-from-the-start-to-avoid-future-regret">1. Implement Flyway from the start to avoid future regret</h2>

<p><a href="https://github.com/flyway/flyway">Flyway</a> is a tool that manages schema migrations. Migrations are when you want to make a change to an existing schema, like add a field or a table. There are alternatives to Flyway, like <a href="https://www.liquibase.com">Liquibase</a>, which are all fine.</p>

<p>One time Chad argued that their microservice was very small and wouldn’t ever change, but they were wrong. It’s software, therefore it will change, and they need to prepare for that. Flyway is that preparation.</p>

<p>Without Flyway, when Chad has to make a change, they’ll have to do it manually in test, accept, prod, and oh yeah, the entire team’s local machines. Good luck keeping everything in sync Chad.</p>

<p>If Chad decides to add Flyway later, they’re in for a lot of pain too, because Flyway will not be able to see any pre-existing schemas. I once had to add Flyway to an existing database. It took me <em>weeks</em>. I yelled at many clouds that winter.</p>

<h2 id="2-slap-a-separate-primary-key-field-on-every-table">2. Slap a separate primary key field on every table</h2>

<p>Another time, Chad chose a phone number field to be the primary key. Phone numbers are unique, right? We can do that! (This is called a <em>natural id</em>.)</p>

<p>I told them to change it and add a separate field, not linked to the data itself, whose only function is to be a primary key. (This is called a <em>surrogate id</em>).</p>

<p>I know, <em>natural id</em> sounds wholesome and cuddly and good, while <em>surrogate id</em> sounds distant and cold and, well, unnatural. Still, surrogate ids are better.</p>

<p>Why? Because it’s not just software that changes; the world changes too. A phone number seems unique enough, but what if the owner switches providers and the number is assigned to someone else? Or worse: I remember when the GDPR was introduced. We had to find a way to delete the phone number but not the rest of the data. Think about all the foreign keys that point to this phone number field!</p>

<p>If—no, <em>when</em> such a change occurs, Chad suddenly needs to pick a new primary key, change all the foreign keys that point to it, and update all the database code related to that table. That’s gonna be painful.</p>

<p>Better to just have an auto-incremented number (don’t forget to give it its own dedicated sequence!) or a generated UUID, right?</p>

<h2 id="3-yes-this-includes-join-tables">3. Yes, this includes join tables</h2>

<p>This also applies to join tables (with a caveat).</p>

<p>Join tables? Why join tables?</p>

<p>People may start using your software in unexpected ways, and what once was a humble join table could become an entity in its own right. If it does, you’ll be glad if it already has its own surrogate id.</p>

<p>For example, a join table that links between Employees and Departments might become its own entity in version 2.0: a Project or Assignment, because it’s the gig economy where jobs are temporary and people work several different jobs at once. The only possible natural id for a classic join table is the combined primary key of the two fields <code class="language-plaintext highlighter-rouge">employee_id</code> and <code class="language-plaintext highlighter-rouge">department_id</code>, and that just <em>sucks</em> to work with. If you have to add a real primary key later, you’ll have to update a bunch of client code and, depending on how much data is already in the table, the migration might run for a long time, resulting in down time. Better to have a surrogate id field ready to go!</p>

<p>Is this premature optimization? Maybe. But it’s extremely cheap to add a primary key field that you don’t really need, and it’s extremely expensive to add one later.</p>

<p>When I was but a young Chad, I once had to promote such a join table to an entity in its own right. Fortunately, the company where I worked had a rule that every join table have a surrogate id field. You see, I didn’t make this one up! I learn from experienced old geezers around me, and you should too.</p>

<p>The caveat I mentioned, is that JPA doesn’t handle link tables with an explicit id field very well: the join table must become an <code class="language-plaintext highlighter-rouge">@Entity</code> and you can’t use <code class="language-plaintext highlighter-rouge">@ManyToMany</code>. This is a consequence of ORM being a leaky abstraction, but that’s a topic for another rant called “Old Man Yells at ORMs”. Take a look at <a href="https://www.jooq.org/">jOOQ</a> or <a href="https://jdbi.org/">Jdbi</a>, or if it’s a small service, just rawdog the JDBC, it’s not that hard!</p>

<p>So, if you <em>must</em> use JPA, you’re excused. You can skip this rule.</p>

<h2 id="4-be-consistent">4. Be consistent</h2>

<p>Sometimes when you have multiple tables, it can be useful to write some generic code that works across all tables. But you can only do that if all tables work the same way, even if that way doesn’t follow the previous rules.</p>

<p>For example, if you need to set up an audit trail, it’s really annoying if all your tables have a <code class="language-plaintext highlighter-rouge">created_at</code> field except the Account table where it’s called <code class="language-plaintext highlighter-rouge">create_time</code> because it was written by Chad, and Chad didn’t pay attention to the other tables that already existed in the database, and now you’re stuck with it.</p>

<p>I recently inherited a microservice from a Chad when they left the company, where every table had a string natural id except for one which had an auto-incrementing integer surrogate id. I had to special-case all my shared integration test logic for that one entity and it made me sad and angry.</p>

<h2 id="conclusion">Conclusion</h2>

<p>There you have it: my personal rules for defining a schema for a relational database. If Chad is you, I hope this rant can help you make good decisions. If Chad is your teammate, I hope it can help you better explain your comments when you need to review their code. And if you disagree with some (or all) of these rules, that’s fine. I hope my rant can still help you put your own arguments into words when inevitably you find yourself in a debate with an old cloud-yeller like me.</p>

<p><img src="/images/2026-02-17-old-man-yells-at-cloud-db-schema/old-jan.webp" alt="Me, an old cloud-yeller" /></p>]]></content><author><name>Jan Ouwens</name></author><category term="database" /><summary type="html"><![CDATA[In which I explain my personal rules for defining a schema in relational databases]]></summary></entry><entry><title type="html">Reading blogs on my Kobo e-reader via Wallabag</title><link href="https://jqno.nl/post/2025/06/04/reading-blogs-on-my-kobo-ereader-via-wallabag/" rel="alternate" type="text/html" title="Reading blogs on my Kobo e-reader via Wallabag" /><published>2025-06-04T00:00:00+00:00</published><updated>2025-06-04T00:00:00+00:00</updated><id>https://jqno.nl/post/2025/06/04/reading-blogs-on-my-kobo-ereader-via-wallabag</id><content type="html" xml:base="https://jqno.nl/post/2025/06/04/reading-blogs-on-my-kobo-ereader-via-wallabag/"><![CDATA[<p>I was a regular user of Pocket, a service that you can send links to and that downloads them so you can read them later, even offline. I also used their Kobo integration a lot, so I could read articles I found online on my e-reader. So when Mozilla <a href="https://support.mozilla.org/en-US/kb/future-of-pocket">announced they’re shutting down Pocket</a>, I had to look for a replacement, which I found in <a href="https://wallabag.org/">Wallabag</a>.</p>

<p>Wallabag is a FOSS tool that you can self-host, or you can use their instance for a small fee. I chose the latter, because I’m lazy, I like supporting small FOSS developers, and everything is hosted safely in Europe.</p>

<h2 id="kobo">Kobo</h2>

<p>Wallabag also has a Kobo integration. It’s not as seamless as Kobo’s native Pocket integration (<a href="https://piaille.fr/@nicosomb/114591105271561144">yet</a> 😉), but it works. In this post I want to explain how I set it up.</p>

<p>I found two ways to do it: <a href="https://gitlab.com/anarcat/wallabako">Wallabako</a> and <a href="https://koreader.rocks">KOReader</a>. They both have downsides, some of which overlap. But I’ve tried both on my 5-year-old Kobo Clara HD, and can confirm that they both work.</p>

<p><strong>Warning</strong>: for both options, you’ll have to use a file manager and a text editor and edit some configuration files.</p>

<p><strong>Another warning</strong>: for both options, you’ll have to put your password in an unencrypted file on your e-reader, so make sure you don’t use the same password for other services or websites.</p>

<p>Before you start, you need to set up an API client in your Wallabag instance. You can find it in the “API clients management” section of the hamburger menu. Copy and paste the Client ID and Client secret into your favorite text editor so you can find them later. Also copy the location of your Wallabag instance while you’re there.</p>

<h2 id="wallabako">Wallabako</h2>

<p><a href="https://gitlab.com/anarcat/wallabako">Wallabako</a> is the more light-weight option of the two. You can install it by following the instructions on their GitLab page. Make sure you replace all the items in the example configuration file with your own values. Once you’re done, restart the e-reader.</p>

<p>Wallabako integrates with Kobo’s own UI. It can synchronize automatically and it creates a new e-book for every unread article in your Wallabag. When you finish reading an article, it will be marked as read in your Wallabag.</p>

<p>This is all very nice but there are two big downsides. First, your articles get mixed up with your “regular” e-books, and there’s no way to get a list of just your Wallabag articles or just your e-books.</p>

<p>Second, the synchronization is clunky. When Wallabako wants to download new articles, you see the “Computer detected” screen that you also get when you connect your Kobo to a computer using usb. You have to press “Connect” manually or else the sync won’t happen. Also, when you finish an article, the e-book remains on your reader. If you’re a digital neat-freak like me, you’ll have to delete it manually. But make sure you have synchronized first, because when the e-book is gone, Wallabako can’t update the read status to your Wallabag.</p>

<p><img src="/images/2025-06-04-reading-blogs-on-my-kobo-ereader-via-wallabag/computer-detected.webp" alt="Kobo's &quot;Computer detected&quot; screen" /></p>

<p>It’s not always clear when synchronization happens, but you can add a launcher to your Kobo menu to force the sync. Look at <a href="https://gitlab.com/anarcat/wallabako/-/blob/main/DESIGN.md#launchers">the Launchers section of Wallabako’s wiki</a> to find out how to set that up. Note that it requires installing another tool on your device.</p>

<p>If you prefer a smoother synchronization process and don’t mind tweaking configuration options, you might prefer KOReader.</p>

<h2 id="koreader">KOReader</h2>

<p>KOReader is the heavier option, but depending on how you use your e-reader, it could also be the nicer one. KOReader is a replacement for your Kobo’s entire interface, and it includes a Wallabag plugin by default that has none of the sync clunkiness of Wallabako.</p>

<p>KOReader is very configurable. You can tweak almost anything about its appearance, but you also kind of have to, because the defaults are not that great. There are many, <em>many</em> settings, and specific ones can be hard to find if you’re looking for them, so be prepared to spend some time exploring. KOReader does have a dark mode though, which is a nice bonus if you like that sort of thing.</p>

<p><img src="/images/2025-06-04-reading-blogs-on-my-kobo-ereader-via-wallabag/koreader.webp" alt="KOReader's articles list on my Kobo" /></p>

<p>The installation process is hard to find on KOReader’s website, and it’s pretty complicated. You can find it <a href="https://github.com/koreader/koreader/wiki/Installation-on-Kobo-devices">here, on their wiki</a>. They describe a semi-automatic process that involves downloading binaries and a script from a forum post. That feels both dangerous and unprofessional, but it <em>is</em> the easiest way, unfortunately.</p>

<p>Once installed, you can run it from a menu. When it’s started, you can find Wallabag somewhere in the menus. There’s an option to configure the Wallabag server. If you press it, it opens a dialog box where you can type in your connection details. You don’t want to type that stuff with Kobo’s clumsy on-screen keyboard. Fortunately, if you keep the fields empty and press OK in this dialog, it creates an empty config file on your Kobo. So, connect it to a laptop via USB, open the file in <code class="language-plaintext highlighter-rouge">.adds/koreader/settings/wallabag.lua</code>, and fill in the details there (note the <code class="language-plaintext highlighter-rouge">.</code> before <code class="language-plaintext highlighter-rouge">adds</code>!). More information on configuring Wallabag can be found <a href="https://github.com/koreader/koreader/wiki/Wallabag">on the KOReader wiki</a>.</p>

<p>Now, when you synchronize with Wallabag, you will get a separate epub file for each article. They will be in a separate folder (<code class="language-plaintext highlighter-rouge">wallabag</code> by default), so it’s easy to separate them from your regular e-books.</p>

<p>Like Wallabako, KOReader has some serious downsides. First is the fact that configuring it takes a lot of time and effort. Though for some, this might actually be enjoyable.</p>

<p>Second is the fact that KOReader can’t read DRM-protected e-books. So if you have your Kobo linked to an online vendor (like I have with bol.com in the Netherlands), you won’t be able to use it to read the books you buy there, and you’ll have to return to Kobo’s regular UI for them.</p>

<h2 id="separating-wallabag-from-your-e-books-with-koreader">Separating Wallabag from your e-books with KOReader</h2>

<p>Because KOReader doesn’t show DRM-protected books, I have decided to use Kobo’s regular UI to read e-books, and KOReader to read Wallabag articles. In order to make this seamless, you have to configure some things.</p>

<p>By default, in Kobo’s regular UI, Wallabag article epubs once again mix with regular e-books, just like with Wallabako. To avoid this, I once again connected my Kobo to my laptop with USB and created a <code class="language-plaintext highlighter-rouge">.wallabag</code> folder. Note the <code class="language-plaintext highlighter-rouge">.</code> in front of the name! This makes sure it’s a hidden folder. Now, in KOReader’s “🗄 → Settings”, enable “Show hidden files”. The new <code class="language-plaintext highlighter-rouge">.wallabag</code> folder should now appear. In KOReader’s Wallabag settings, you can now select this as your “Download folder”. Wallabag articles now get downloaded into this folder, and Kobo’s regular UI won’t see them.</p>

<p>Now, go to KOReader’s “🗄 → Settings → Home folder settings”, and select <code class="language-plaintext highlighter-rouge">.wallabag</code> as your home folder. Now, every time you open KOReader, you’ll automatically be directed to your Wallabag articles. When you exit KOReader, you’re back in Kobo’s regular UI and you’ll see only your e-books again.</p>

<p>If you want, you can now disable “Show hidden files” again. If you had downloaded articles in a different directory than the <code class="language-plaintext highlighter-rouge">.wallabag</code> directory we just created, now is also a good time to connect the USB again and remove the old directory and the articles downloaded there.</p>

<p>I have also enabled the “Remote mark-as-read settings → Auto-upload article statuses when downloading” setting. With this setting enabled, it will update your read status when you download new articles. Otherwise, this would be two separate synchronization operations, which is extra work and I’m lazy.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I’m happy I got this working, and I can now read articles on my Kobo again. However, neither of the proposed solutions is without problems. Kobo’s native Pocket integration was much, much easier to use. Hopefully, Kobo will realize this and replace their (now useless) Pocket app with a Wallabag client.</p>

<h2 id="bonus-round-fonts">Bonus round: fonts!</h2>

<p><img src="/images/2025-06-04-reading-blogs-on-my-kobo-ereader-via-wallabag/tilburgsans.webp" alt="An article on my Kobo set in TilburgsAns" /></p>

<p>I’ve <a href="/post/2024/01/10/about-fonts/">talked about fonts on this blog</a> before, and I thought it might be nice to share how I installed my preferred fonts on my Kobo device. It doesn’t have anything to do with Wallabag, but it makes reading on my Kobo so much nicer 😄. And if you’ve successfully installed Wallabag (through either method) on your e-reader, this is now simple.</p>

<p>Make sure you have your favorite font stored on your hard disk somewhere. You’ll need to have it in either <code class="language-plaintext highlighter-rouge">.ttf</code> or <code class="language-plaintext highlighter-rouge">.otf</code> format. A <code class="language-plaintext highlighter-rouge">.woff</code> file is not going to work.</p>

<p>Connect your Kobo with your laptop via USB. Create a folder called <code class="language-plaintext highlighter-rouge">fonts</code>, and place your font file(s) in there. This makes them available to the regular Kobo UI. Open a book, find the font settings, and your fonts should be available in the Font Face box.</p>

<p>If you went with the KOReader option, find the <code class="language-plaintext highlighter-rouge">.adds/koreader/fonts</code> directory, and copy the files into that folder too. Now they’re available in KOReader. Open a Wallabag article, open the menu, and find the “Font” setting. Note that changing this setting only applies the font to the current epub file. If you want it to become the default font (and I don’t see why you wouldn’t want that), you have to long-press on the font, and then a menu will pop up that will allow you to make it the default for all epub files.</p>]]></content><author><name>Jan Ouwens</name></author><category term="kobo" /><category term="wallabag" /><summary type="html"><![CDATA[In which I compare two options for reading blog posts on my e-reader, using the Wallabag read-it-later service.]]></summary></entry><entry><title type="html">Reviewing large PRs</title><link href="https://jqno.nl/post/2024/12/28/reviewing-large-prs/" rel="alternate" type="text/html" title="Reviewing large PRs" /><published>2024-12-28T00:00:00+00:00</published><updated>2024-12-28T00:00:00+00:00</updated><id>https://jqno.nl/post/2024/12/28/reviewing-large-prs</id><content type="html" xml:base="https://jqno.nl/post/2024/12/28/reviewing-large-prs/"><![CDATA[<p>Do you have to do code reviews sometimes? I do. At my current project, we use GitHub PRs for that. Some PRs are small, and can easily be comprehended just by looking at them on the PR page, but some PRs are too large for this. The linear view that GitHub’s PR page provides provides little help to fully understand the changes made in the PR. So what do you do?</p>

<p>You could power through and read it from the PR page anyway, which I don’t recommend: it’s very hard to keep the big picture in your mind and you’ll end up pointing out only small things like typos. Or you can checkout its corresponding branch in your IDE and look at it there. This is better, because you can leverage your IDE to navigate through the code to find the big picture. However, you can’t see the diff in the IDE, so it’s hard to keep track of what code belongs to the PR and what doesn’t. You might miss things, or look at things that aren’t part of the PR.</p>

<p>I was in this situation often enough that I decided to write a script to help me deal with it. You can run it like this:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git review 1981
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">1981</code> is the PR-number for the PR that you want to review. Let’s say that its branch name is <code class="language-plaintext highlighter-rouge">feature/add-the-big-new-killer-feature</code>. When you run the script, it will create a <em>new</em> branch called <code class="language-plaintext highlighter-rouge">review/feature/add-the-big-new-killer-feature</code>. This new branch will contain a bunch of new and/or changed files. These changes are <em>precisely</em> the changes in the PR you are reviewing. So in essence you’re seeing the branch in the same way that the PR author saw it, just before creating the commit (as if they had collapsed all changes into a single commit). You can now use your familiar IDE to navigate the PR, simply by navigating the changes in the branch that you’re working on. If you want to suggest a change, you can even try it out and see if it actually works, before making the suggestion.</p>

<p>Why doesn’t this script just open the branch <code class="language-plaintext highlighter-rouge">feature/add-the-big-new-killer-feature</code> and “uncommit” whatever is in there, you might ask? Well, this new branch gives you a clean playground which prevents you from accidentally committing and pushing something to the actual PR.</p>

<p>Also note that your working directory has to be clean for this script to run; otherwise it will exit early with an error message.</p>

<p>When you’re done reviewing the PR, you can run <code class="language-plaintext highlighter-rouge">git review done</code> and the script will clean up any changes you made, delete the review branch, and return you to the branch you were in before you started the review.</p>

<p>Pretty nifty, right?</p>

<p>You can find the code for the script below this post. I’ve added comments so you can follow along with the logic; there’s some pretty funky git-fu going on in there.</p>

<p>You can put the code in a file named <code class="language-plaintext highlighter-rouge">git-review</code>. The <code class="language-plaintext highlighter-rouge">-</code> is important: that way, <code class="language-plaintext highlighter-rouge">git</code> can pick it up and pretend that it’s an actual <code class="language-plaintext highlighter-rouge">git</code> subcommand. Of course you can also give it another name if you prefer. Next, <a href="https://apple.stackexchange.com/q/275343/175504">put it in a directory that’s on your path</a>.</p>

<p>Note that you need to have the <a href="https://cli.github.com/">GitHub CLI</a> installed: the script uses it to determine the name of the branch for a PR. If you don’t want to install it, you can modify the script to checkout branches instead of PRs. In that case, remove the block that checks if the GitHub CLI is available, and replace the line that says <code class="language-plaintext highlighter-rouge">pr_branch=$(gh ...)</code> with <code class="language-plaintext highlighter-rouge">pr_branch=$1</code>. That will allow you to run the script using <code class="language-plaintext highlighter-rouge">git review feature/add-the-big-new-killer-feature</code>.</p>

<p>And yes, I’ve been told that IntelliJ IDEA comes with very similar functionality out of the box through its GitHub plugin: you’ll have to authorize IntelliJ with GitHub, and then you can interact with your PRs directly from the IDE’s Pull Request tab. You can see the PR’s changes in context, but the view is read-only so you won’t be able to make changes to try things out. So try both and see what you prefer!</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Check if we're in a Git repo</span>
<span class="k">if</span> <span class="o">!</span> git rev-parse <span class="nt">--is-inside-work-tree</span> &amp;&gt;/dev/null<span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"❌ Not in a git repository"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Check if the user provided a command-line argument</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Usage: git review [&lt;pr-number&gt; | done]"</span>
    <span class="nb">exit </span>1
<span class="k">fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"done"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># We were reviewing; now we're done and we want to go back to where we were</span>

    <span class="c"># Determine name of current branch</span>
    <span class="nv">current_branch</span><span class="o">=</span><span class="si">$(</span>git rev-parse <span class="nt">--abbrev-ref</span> HEAD<span class="si">)</span>

    <span class="c"># Check if we're on a review branch</span>
    <span class="k">if</span> <span class="o">!</span> <span class="o">[[</span> <span class="nv">$current_branch</span> <span class="o">=</span>~ ^review/ <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"❌ This is not a review branch"</span>
        <span class="nb">exit </span>1
    <span class="k">fi</span>

    <span class="c"># Clean up the branch</span>
    git reset <span class="nt">--hard</span> <span class="o">&gt;</span> /dev/null 2&gt;&amp;1
    git clean <span class="nt">-fd</span> <span class="o">&gt;</span> /dev/null 2&gt;&amp;1

    <span class="c"># Go back to the branch that was active before the review started</span>
    git checkout - <span class="o">&gt;</span> /dev/null 2&gt;&amp;1

    <span class="c"># Remove review branch</span>
    git branch <span class="nt">-D</span> <span class="s2">"</span><span class="nv">$current_branch</span><span class="s2">"</span> <span class="o">&gt;</span> /dev/null 2&gt;&amp;1

    <span class="nb">echo</span> <span class="s2">"✅ Done reviewing </span><span class="nv">$current_branch</span><span class="s2">"</span>

<span class="k">else</span>
    <span class="c"># We want to start a review session</span>

    <span class="nv">pr_number</span><span class="o">=</span><span class="nv">$1</span>

    <span class="c"># Check if there's changes</span>
    <span class="k">if </span>git status <span class="nt">--porcelain</span> | <span class="nb">grep</span> <span class="nt">-q</span> <span class="s1">'^'</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"❌ There are changes or untracked files in the repository"</span>
        <span class="nb">exit </span>1
    <span class="k">fi</span>

    <span class="c"># Check if the GitHub CLI command is available</span>
    <span class="k">if</span> <span class="o">!</span> <span class="nb">command</span> <span class="nt">-v</span> gh <span class="o">&gt;</span> /dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"❌ GitHub CLI not available"</span>
        <span class="nb">exit </span>1
    <span class="k">fi</span>

    <span class="c"># Make sure we have everything</span>
    git fetch

    <span class="c"># Determine the name of the PR's branch (using GitHub CLI)</span>
    <span class="nv">pr_branch</span><span class="o">=</span><span class="si">$(</span>gh <span class="nb">pr </span>view <span class="s2">"</span><span class="nv">$pr_number</span><span class="s2">"</span> <span class="nt">--json</span> headRefName <span class="nt">--jq</span> <span class="s1">'.headRefName'</span><span class="si">)</span>

    <span class="c"># Remove review branch if it exists already</span>
    git branch <span class="nt">-D</span> <span class="s2">"review/</span><span class="nv">$pr_branch</span><span class="s2">"</span> <span class="o">&gt;</span> /dev/null 2&gt;&amp;1

    <span class="c"># Checkout the PR's branch into a new branch which mirror's the PR's branch, but with 'review/' in front</span>
    git checkout <span class="nt">-b</span> <span class="s2">"review/</span><span class="nv">$pr_branch</span><span class="s2">"</span> <span class="s2">"origin/</span><span class="nv">$pr_branch</span><span class="s2">"</span> <span class="o">&gt;</span> /dev/null 2&gt;&amp;1

    <span class="c"># Fail if the branch doesn't exist</span>
    <span class="k">if</span> <span class="o">[[</span> <span class="nv">$?</span> <span class="o">!=</span> 0 <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"❌ Branch doesn't exist (was the PR already merged?)"</span>
        <span class="nb">exit </span>1
    <span class="k">fi</span>

    <span class="c"># Sever the link with the remote branch, so we don't accidentally push something to it</span>
    git branch <span class="nt">--unset-upstream</span> <span class="s2">"review/</span><span class="nv">$pr_branch</span><span class="s2">"</span>

    <span class="c"># Determine name of main/master branch</span>
    <span class="nv">main_branch</span><span class="o">=</span><span class="si">$(</span>git rev-parse <span class="nt">--abbrev-ref</span> origin/HEAD<span class="si">)</span>

    <span class="c"># Find the point where the PR split off from main</span>
    <span class="nv">mergebase</span><span class="o">=</span><span class="si">$(</span>git merge-base <span class="s2">"</span><span class="nv">$main_branch</span><span class="s2">"</span> <span class="s2">"review/</span><span class="nv">$pr_branch</span><span class="s2">"</span><span class="si">)</span>

    <span class="c"># Check if this all worked correctly</span>
    <span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$mergebase</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"❌ Could not determine a mergebase for branch </span><span class="nv">$pr_branch</span><span class="s2">"</span>
        <span class="nb">exit </span>1
    <span class="k">fi</span>

    <span class="c"># Reset the changes made in the PR so we can look at them in our editor</span>
    git reset <span class="nt">--mixed</span> <span class="s2">"</span><span class="nv">$mergebase</span><span class="s2">"</span> <span class="o">&gt;</span> /dev/null

    <span class="nb">echo</span> <span class="s2">"🚀 Ready to review PR </span><span class="nv">$pr_number</span><span class="s2"> from branch </span><span class="nv">$pr_branch</span><span class="s2">"</span>
<span class="k">fi</span>
</code></pre></div></div>

<blockquote>
  <p>Cross-posted with permission. The original post is at <a href="https://yoink.nl/posts/2024/12/24/reviewing-large-prs.html">the Yoink blog</a>.</p>
</blockquote>]]></content><author><name>Jan Ouwens</name></author><category term="git" /><category term="code-review" /><category term="script" /><summary type="html"><![CDATA[In which I over-engineer a shell script.]]></summary></entry><entry><title type="html">Un-refactoring</title><link href="https://jqno.nl/post/2024/12/06/unrefactoring/" rel="alternate" type="text/html" title="Un-refactoring" /><published>2024-12-06T00:00:00+00:00</published><updated>2024-12-06T00:00:00+00:00</updated><id>https://jqno.nl/post/2024/12/06/unrefactoring</id><content type="html" xml:base="https://jqno.nl/post/2024/12/06/unrefactoring/"><![CDATA[<p>Yesterday was Sinterklaas, the Dutch holiday of gift-giving. Sinterklaas was very generous again this year for me and my family, but I also gave myself a gift: the gift of … (checks notes) … reverting two month’s worth of work on EqualsVerifier!? That’s, like, three whole releases!</p>

<p>So what happened?</p>

<p>In my <a href="/post/2024/06/01/looking-back-on-15-years-of-equalsverifier/">15 years retrospective</a>, I mentioned that if I could do it all over again, I would have taken a more immutable approach to the instantiation logic underpinning EqualsVerifier. That got me thinking: why don’t I? I have more than a thousand unit tests on this sucker. I could <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">strangler fig</a> the heck out of that!</p>

<p>So I started. And it went well, until it didn’t. Edge case after edge case popped up, causing exactly the kind of gnarly debugging sessions that I wanted to avoid by doing this. So I threw it away and started again, in a different way. And it went well, until it didn’t. Different edge cases this time. And I got frustrated, because other cool projects that I also want to do got dismissed because of this.</p>

<p>I’d also been releasing various stages of this refactoring, in version 3.17 to 3.17.4, and issues started coming in for things that used to work well but now don’t anymore. Corner cases that none of those 1000+ tests covered.</p>

<p>So, the other day I was doing a late-night debugging session prompted by a GitHub issue, tired, and starting to get migraine aura. Did you ever have migraine aura? It’s like seeing a <a href="https://www.youtube.com/watch?v=-AC0ucyisd8">Pink Floyd laser show</a> in your head, without the music and the ability to shut it off. Fortunately mine often come without the migraine headaches.</p>

<p>And that’s when I decided: no more. The code works as it is. It has for the past 15 years. And generally without issues, too. Why would I pull the rug on that? I must be crazy. So yesterday, I pulled the trigger and reverted everything I made after the release of 3.17.1, because the first part of the refactoring didn’t touch the nasty bits, didn’t cause any of the issues, and was actually still valuable to me for my plans going forward.</p>

<p>I also went through all the commits that I reverted, and cherry-picked the things that were still good: mostly unit tests, but also fixes to a feature I’d introduced in 3.17 that didn’t relate to the refactoring.</p>

<p>As we say in Dutch, ‘beter ten halve gekeerd dan ten hele gedwaald’: better to turn back halfway, than to go astray completely.</p>

<p>It feels painful in a way, but it’s also liberating. Maybe <a href="https://arstechnica.com/gadgets/2024/09/it-was-the-wrong-decision-employees-discuss-sonos-rushed-app-debacle/">software companies</a> should be brave enough to consider this option more often.</p>]]></content><author><name>Jan Ouwens</name></author><category term="equalsverifier" /><category term="software-engineering" /><summary type="html"><![CDATA[In which I throw away code that I'd already released.]]></summary></entry><entry><title type="html">Why are there no decent code formatters for Java?</title><link href="https://jqno.nl/post/2024/08/24/why-are-there-no-decent-code-formatters-for-java/" rel="alternate" type="text/html" title="Why are there no decent code formatters for Java?" /><published>2024-08-24T00:00:00+00:00</published><updated>2024-08-24T00:00:00+00:00</updated><id>https://jqno.nl/post/2024/08/24/why-are-there-no-decent-code-formatters-for-java</id><content type="html" xml:base="https://jqno.nl/post/2024/08/24/why-are-there-no-decent-code-formatters-for-java/"><![CDATA[<p>I haven’t found a single Java code formatter that I like. And believe me, I’ve <em>looked</em>.</p>

<p><img src="/images/2024-08-24-why-are-there-no-decent-code-formatters-for-java/meme.jpg" alt="Meme - all java code formatters suck, change my mind.jpg" /></p>

<p>Let’s discuss, because I really want my mind to change.</p>

<h2 id="my-history-with-code-formatters">My history with code formatters</h2>

<p>First, some background. Feel free to skip this part if you want to get to the good part.</p>

<p>In my teens, I was working on a hobby project with a friend. We didn’t know about version control yet, so every day at school we would exchange diskettes with source code which I would then integrate by hand when I got home. Part of this integration was fixing spacing, indentation and capitalization. My friend was a competent programmer but his code often looked sloppy to my perfectionist eyes. Making the corrections gave me a false sense of productivity, and it was probably at this time that a toxic thought settled in my mind: that a competent programmer is precise enough and cares enough about their craft that they don’t need an automated formatter.</p>

<p>Years later, I had to do a code review with an inexperienced coworker. I was unable to read his code because it was so badly formatted. I’m not exaggerating, it was truly unreadable. I thought my toxic thought: this is clearly someone who doesn’t care about their craft. I sent him back to fix the formatting, and then we resumed the review.</p>

<p>More years later, the toxic thought settled even deeper when I started working for a well-known Dutch company in a team where a code formatter was mandated. The formatter of choice was “badly-configured Eclipse”, and instead of being checked into version control, the configuration was manually copied to every workstation. I asked a coworker what he thought of this formatter. Him: “I like how it’s consistently readable.” Me: “You mean consistently unreadable.” Him: “Well…yeah.” Sometimes I would try to make a bit of code more readable by making a manual adjustment, but whenever someone made an edit to the same file, Eclipse would happily reformat everything again. So I made an attempt to change the configuration. Everybody thought it was a good idea, but nobody was willing to reconfigure their Eclipse, so that’s where that ended.</p>

<p>This is how I remember what it looked like (exaggerated for dramatic effect):</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span>
    <span class="o">.</span><span class="na">when</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
    <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
    <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">()).</span><span class="na">verify</span><span class="o">()).</span><span class="na">assertFailure</span><span class="o">()</span>
    <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<p>The change, for me, came a few years later still, when I was working on a Scala project and was introduced to <a href="https://scalameta.org/scalafmt/">Scalafmt</a>. A colleague insisted we use it and I grudgingly agreed. It was the first time I encountered a formatter that actually worked, and worked <em>well</em>. The configuration could be checked into git along with the project itself, and it integrated well with all of the tools. What’s more, I found it actually caught inconsistencies that I had introduced myself. I guess I didn’t care enough about my craft, after all! I noticed it improved my colleagues’s code as well. And it would have solved that code review problem with the mere press of a button.</p>

<p>That was the end of the toxic thoughts about formatting: my mind was changed.</p>

<p>I decided to adopt a formatter in EqualsVerifier as well. I tried a few, started using one (<a href="https://github.com/google/google-java-format">google-java-format</a>), switched to another (<a href="https://github.com/jhipster/prettier-java">Prettier Java</a>), and have come to the bold conclusion that all code formatting tools for Java are inadequate.</p>

<h2 id="so-what-do-i-want-from-a-formatter">So what do I want from a formatter?</h2>

<p>Basically, I want what formatters from other language ecosystems have, such as Scala, Rust and Go. Which is:</p>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: Integration with Maven. Anything that’s not enforced by CI is merely a suggestion, and may as well not exist. I want CI to check formatting and fail the build if it’s not good, and I want Maven to be able to reformat code too, so contributors don’t have to install anything. As a corollary, if a binary must be installed, Maven must handle that too.</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: It must be fast (or: there must be a fast way to format a file). That way, I can invoke it from <a href="https://jqno.nl/post/2020/09/09/my-vim-setup">Neovim</a>. A command-line tool is ideal, but Maven is too slow. Even <a href="https://github.com/apache/maven-mvnd">mvnd</a> is not fast enough for this. In lieu of a command-line tool, I’ll accept some kind of <a href="https://langserver.org/">Language Server Protocol</a> integration. I realise that this may not be a concern for many people, but it is for me and it seems to me like a reasonable thing to want: it’s standard practice for all other language ecosystems.</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: It must format well. It may seem obvious, but I want the formatted code to look <em>nice</em>. See the code sample in the previous paragraph for an example of a formatter not doing a good job.</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: It must get out of my way. I don’t want to get bogged down with version incompatibilities that I have to debug or jump through weird hoops in order to configure the thing.</li>
</ul>

<p>If a formatter fails on one of these criteria, it fails as a whole. Some criteria may be subjective; in those cases I will be the judge and executioner. Again, all of the formatters for all of the languages have all of these features, so it shouldn’t be strange to want the same things from a Java formatter, right? It’s not like Java is a niche language with historically bad tooling, after all.</p>

<p>There are other factors to consider that, for me at least, don’t weigh on the final verdict, but are interesting nonetheless:</p>

<ul>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: A plugin for IntelliJ would be nice. I don’t use IntelliJ myself, but many potential contributors do, and I want things to be easy for them.</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: Some formatters have many configuration options while others take inspiration from Go and provide none. I’m fine either way (as long as the results are good enough), but it’s interesting to keep track of.</li>
</ul>

<h2 id="review-of-formatters">Review of formatters</h2>

<p>So, let’s discuss all the formatters (that I know of) one by one.</p>

<h3 id="intellijs-built-in-formatter">IntelliJ’s built-in formatter</h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: fail</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: fail</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: good</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: good</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: excellent</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: many options</li>
</ul>

<p>It’s a decent formatter, but it suffers from Kotlin-syndrome: there’s no way you can use it from anything that isn’t IntelliJ. You can’t call it from Maven, and you <em>certainly</em> can’t use it from the command-line.</p>

<p>Also, if left unconfigured, it won’t touch newlines. That can be a good thing as you have a lot of control over how your code is formatted, but has some edge cases. The results are not 100% predictable, and these are all possible results:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">simple</span><span class="o">().</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">verify</span><span class="o">();</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">EqualsVerifier</span>
        <span class="o">.</span><span class="na">simple</span><span class="o">()</span>
        <span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
        <span class="o">.</span><span class="na">verify</span><span class="o">();</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">simple</span><span class="o">()</span>
        <span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">verify</span><span class="o">();</span>
</code></pre></div></div>

<p>For me, that’s not a deal-breaker. The lack of proper non-telliJ tooling, however, is.</p>

<p>Final verdict: <strong>fail</strong></p>

<p>Example, with inconsistent line breaking:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span><span class="o">.</span><span class="na">when</span><span class="o">(</span>
                <span class="o">()</span> <span class="o">-&gt;</span>
                        <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
                                <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                                        <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                                        <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span>
                                                <span class="o">.</span><span class="na">toList</span><span class="o">(),</span>
                                        <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">()</span>
                                                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">())</span>
                                <span class="o">.</span><span class="na">verify</span><span class="o">())</span>
        <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
        <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="google-java-format"><a href="https://github.com/google/google-java-format">google-java-format</a></h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: good</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: excellent</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: fail</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: excellent</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: good</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: no options</li>
</ul>

<p>This should have been the obvious choice. Integration with tooling is excellent, and the quality of the formatting would have been good if not for one <em>extremely weird</em> design decision.</p>

<p>I guess Google decided that the tabs-vs-spaces discussion wasn’t heated enough, and wanted to throw the <em>number</em> of spaces into the mix. Java has had the convention of 4-space indents <a href="https://www.oracle.com/java/technologies/cc-java-programming-language.html">since at least 1999</a>, if not longer, so of course, Google decides they want to do 2 spaces instead.</p>

<p>Sure, other languages use 2 spaces. Who cares. If you want to participate in an ecosystem, you need to follow the established conventions for that ecosystem.</p>

<p>Thankfully, the <em>one</em> configuration option that they have, allows you to use 4 spaces instead. This option is called AOSP for Android Open Source Project because when Google acquired it, Android had been using 4 spaces like any sane Java shop. Google doesn’t document this option on their website, though.</p>

<p>Anyway, the AOSP option really accentuates google-java-format’s only other weakness: it <em>loves</em> to double-indent, and sometimes even quadruple-indent, especially with nested lambdas and expressions. This really pushes a lot of code to the right hand side of your editor, making the code look quite bad, as you can see in the examples below.</p>

<p>Final verdict: <strong>fail</strong></p>

<p>Example, with default configuration - note that there actually aren’t any 2-space indentations in this example; they only happen after <code class="language-plaintext highlighter-rouge">{</code>:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span><span class="o">.</span><span class="na">when</span><span class="o">(</span>
        <span class="o">()</span> <span class="o">-&gt;</span>
            <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
                <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                    <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                    <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
                    <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">())</span>
                <span class="o">.</span><span class="na">verify</span><span class="o">())</span>
    <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
    <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<p>Example, with AOSP - note how everything gets pushed to the right:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span><span class="o">.</span><span class="na">when</span><span class="o">(</span>
                <span class="o">()</span> <span class="o">-&gt;</span>
                        <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                                <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
                                <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                                        <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                                        <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">()</span>
                                                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span>
                                                <span class="o">.</span><span class="na">toList</span><span class="o">(),</span>
                                        <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">()</span>
                                                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">)</span>
                                                <span class="o">.</span><span class="na">toList</span><span class="o">())</span>
                                <span class="o">.</span><span class="na">verify</span><span class="o">())</span>
        <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
        <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="prettier-java"><a href="https://www.jhipster.tech/prettier-java/">Prettier Java</a></h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: good</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: good</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: excellent</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: fail</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: fail</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: some options</li>
</ul>

<p>Prettier Java, on the surface, is also a very nice formatter. It has sane defaults and produces beautiful code out of the box. It’s the one I’m currently using for EqualsVerifier.</p>

<p>It has a lot of peripheral problems though:</p>

<ul>
  <li>It’s an extension for <a href="https://prettier.io/">Prettier</a>, the JavaScript formatter. This means that it requires a full NodeJS runtime to work, which is … unfortunate.</li>
  <li>It can be kind of annoying to install, because it needs to be installed as a plugin to Prettier. If you want to use <a href="https://github.com/fsouza/prettierd">prettierd</a>, to speed things up, it becomes downright <em>hard</em> to set up.</li>
  <li>The formatting isn’t stable between versions of Prettier Java, so you might have to re-format your code base every time you upgrade.</li>
  <li>Because of this, you might want to pin your Prettier Java to a specific version, which makes managing your prettierd installation <em>even harder</em>, because now you can’t blindly update your system anymore. Of course, if you allow the Prettier Java in your build script and the one in your editor to diverge, they will start working against each other, which is no fun.</li>
  <li>There’s no IntelliJ plugin.</li>
</ul>

<p>Final verdict: <strong>fail</strong></p>

<p>Example with Prettier Java 2.4.0:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span>
    <span class="o">.</span><span class="na">when</span><span class="o">(()</span> <span class="o">-&gt;</span>
        <span class="nc">EqualsVerifier</span>
            <span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
            <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">verify</span><span class="o">()</span>
    <span class="o">)</span>
    <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
    <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<p>Example with Prettier Java 2.6.0 - note how the <code class="language-plaintext highlighter-rouge">() -&gt;</code> gets its own line now, how <code class="language-plaintext highlighter-rouge">.when</code> and <code class="language-plaintext highlighter-rouge">.forClass</code> don’t, and what the heck is going on with that single <code class="language-plaintext highlighter-rouge">)</code>!?</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span><span class="o">.</span><span class="na">when</span><span class="o">(</span>
    <span class="o">()</span> <span class="o">-&gt;</span>
        <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
            <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">verify</span><span class="o">()</span>
<span class="o">)</span>
    <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
    <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="eclipse-jdt-formatter">Eclipse JDT formatter</h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: good</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: meh</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: good</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: fail</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: good</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: many options</li>
</ul>

<p>The formatter that turned me off on formatters so long ago. But if configured properly, it’s actually very decent.</p>

<p>And that’s immediately where the problem lies. Even though this formatter can be used as a more-or-less stand-alone tool, unlike IntelliJ’s formatter, it’s still very integrated with the Eclipse IDE, and you’ll need to run it to configure it. And you <em>need</em> to configure it. You can export your settings to an xml file of the kind that you’re not going to want to edit by hand. In other words, you can’t adopt this formatter without installing Eclipse, which is … <em>sigh</em></p>

<p>While researching this post, I found that IntelliJ is able to import and export Eclipse formatter configurations. However, I don’t know how well this works.</p>

<p>Also, despite the fact that there are <em>two</em> different Maven plugins to run this formatter, there’s no proper command-line tool. The loop-hole here is that the LSP I use for Java is based on Eclipse and has the formatter built-in, so I can still run it from Vim. But if I ever want to try out Oracle’s shiny new LSP, I’m out of a formatter again.</p>

<p>Since the Eclipse JDT formatter is basically a Java library that can be used from various tools, it shouldn’t be too hard to make a GraalVM command-line tool to invoke it. But that adds quite a lot to the burden of adopting it, which was already high due to the need to configure it from Eclipse.</p>

<p>Final verdict: <strong>fail</strong></p>

<p>Example with no configuration:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span>
    <span class="o">.</span><span class="na">when</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
        <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
            <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">())</span>
        <span class="o">.</span><span class="na">verify</span><span class="o">())</span>
    <span class="o">.</span><span class="na">assertFailure</span><span class="o">().</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<p>Example with configuration:</p>
<ul>
  <li>Wrapping settings → Function calls → Arguments → “Wrap all elements, except first element if not necessary”</li>
  <li>Wrapping settings → Function calls → Qualified invocations → “Wrap all elements, every element on a new line”
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span>
  <span class="o">.</span><span class="na">when</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="nc">EqualsVerifier</span>
      <span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
      <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
      <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
          <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
          <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">())</span>
      <span class="o">.</span><span class="na">verify</span><span class="o">())</span>
  <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
  <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="palantir-java-format"><a href="https://github.com/palantir/palantir-java-format">Palantir Java Format</a></h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: good</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: fail</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: excellent</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: good</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: good</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: no options</li>
</ul>

<p>Palantir Java Format is based on google-java-format, and somehow they manage to make Google’s bad parts good, and Google’s good parts bad, at the same time.</p>

<p>On the one hand, they make google-java-format’s crappy formatting much, <em>much</em> better. But at the same time, they don’t offer a command-line tool. Yes, there’s Maven integration, and yes, there’s an IntelliJ plugin. But no official command-line tool. Weird.</p>

<p>There’s a way to work around that. If you use Arch, BTW, you can download <a href="https://aur.archlinux.org/cgit/aur.git/tree/palantir-java-format?h=palantir-java-format">a script from the AUR</a> that you can run from the command-line, and of course you can easily borrow and adapt that script if you use a more sensible OS. You’ll have to gather and package the dependencies somehow, though. If you want to go down that path, here’s the updated ratings:</p>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: good</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: good</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: excellent</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: fail</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: good</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: no options</li>
</ul>

<p>Palantir fails <code class="language-plaintext highlighter-rouge">ergonomics</code> in this case because you have to figure out how to share the script and its dependencies with other people, and how you want to update them.</p>

<p>This script does expose that Palantir is probably a wrapper around google-java-format, rather than a fork of it. I’m not sure what that implies for the long-term stability of the project, and I don’t know if I should let myself be bothered by that.</p>

<p>Also, there’s the business activities of Palantir, the company behind this formatter. I don’t know if I should let myself be bothered by that, either. But I might. I think I will.</p>

<p>Either way, the messy business around the command-line tool is a deal-breaker for me.</p>

<p>Final verdict: <strong>fail</strong></p>

<p>Example:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span><span class="o">.</span><span class="na">when</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
                <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                        <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                        <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
                        <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">())</span>
                <span class="o">.</span><span class="na">verify</span><span class="o">())</span>
        <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
        <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="spring-java-format"><a href="https://github.com/spring-io/spring-javaformat">Spring Java Format</a></h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: good</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: fail</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: good</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: fail</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: good</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: no options</li>
</ul>

<p>This seems to be a wrapper around the Eclipse JDT formatter (and also Checkstyle) with a hard-coded configuration. The formatting looks nice enough, and it provides plugins for all the build tools and IDEs … but no command-line tool.</p>

<p>For some reason, it needs a modification in your <code class="language-plaintext highlighter-rouge">.m2/settings.xml</code> file, which is weird.</p>

<p>Final verdict: <strong>fail</strong></p>

<p>Example:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpectedException</span>
    <span class="o">.</span><span class="na">when</span><span class="o">(()</span> <span class="o">-&gt;</span>
        <span class="nc">EqualsVerifier</span>
            <span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
            <span class="o">.</span><span class="na">suppress</span><span class="o">(</span><span class="nc">Warning</span><span class="o">.</span><span class="na">NONFINAL_FIELDS</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withPrefabValues</span><span class="o">(</span>
                <span class="nc">List</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="o">).</span><span class="na">toList</span><span class="o">(),</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="n">i</span> <span class="o">-&gt;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">2</span><span class="o">).</span><span class="na">toList</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">verify</span><span class="o">()</span>
    <span class="o">)</span>
    <span class="o">.</span><span class="na">assertFailure</span><span class="o">()</span>
    <span class="o">.</span><span class="na">assertMessageContains</span><span class="o">(</span><span class="s">"something"</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="cross-language-code-formatters">Cross-language code formatters</h3>

<ul>
  <li>🏗 <code class="language-plaintext highlighter-rouge">maven</code>: fail</li>
  <li>🏃‍♂️ <code class="language-plaintext highlighter-rouge">speed</code>: excellent</li>
  <li>✨ <code class="language-plaintext highlighter-rouge">beauty</code>: varies</li>
  <li>🚀 <code class="language-plaintext highlighter-rouge">ergonomics</code>: fail</li>
  <li>🧠 <code class="language-plaintext highlighter-rouge">IntelliJ</code>: good</li>
  <li>⚙️ <code class="language-plaintext highlighter-rouge">config</code>: many options</li>
</ul>

<p>There are many languages that look like Java (because they all descend from C), so it makes sense that there exist various tools that can format all of these languages: <a href="https://clang.llvm.org/docs/ClangFormat.html">ClangFormat</a>, <a href="https://astyle.sourceforge.net/">Artistic Style (astyle)</a>, <a href="https://github.com/uncrustify/uncrustify">Uncrustify</a>…</p>

<p>They are all similar in a way, in that they all have excellent command-line support. Some have IntelliJ plugins, some don’t, but all of them are hard to use from Maven: you’ll have to pre-install them and then use <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code> to run them.</p>

<p>Also, since there are so many configuration options, and the tools are focused on other languages than Java, they don’t look particularly well out of the box, so expect to invest a lot of time tweaking. For this reason, I won’t include examples, since they don’t mean much without configuration anyway.</p>

<p>Final verdict: <strong>fail</strong></p>

<h3 id="editorconfig"><a href="https://editorconfig.org/">EditorConfig</a></h3>

<p>For completeness’s sake, I’ll give a mention to EditorConfig. It’s a nice editor-agnostic and language-agnostic tool that ensures consistent use of tabs/spaces, EOL characters, and indentation. But nothing more.</p>

<p>It’s a very nice and useful tool that’s good at what it does, but since it’s not a full formatter, I won’t rate it here.</p>

<h3 id="spotless"><a href="https://github.com/diffplug/spotless">Spotless</a></h3>

<p>I’ll also give a shout-out to Spotless, since many people mention it on Reddit whenever somebody asks what Java formatter they use.</p>

<p>Spotless isn’t a formatter. What it is, is a plugin for both Maven and Gradle that you can use to run most of the formatters mentioned above, and it’s very good at that!</p>

<h3 id="summary">Summary</h3>

<p>If I compile the final verdicts for all the formatters I discussed, there’s a clear trend:</p>

<ul>
  <li>IntelliJ’s built-in formatter: <strong>fail</strong></li>
  <li>google-java-format: <strong>fail</strong></li>
  <li>Prettier Java: <strong>fail</strong></li>
  <li>Eclipse JDT formatter: <strong>fail</strong></li>
  <li>Palantir Java Format: <strong>fail</strong></li>
  <li>Spring Java Format: <strong>fail</strong></li>
  <li>Cross-language code formatters: <strong>fail</strong></li>
</ul>

<p>None of the formatters that I could find provide a decent solution to all of the stated requirements.</p>

<p>This makes the Java ecosystem very different from the ecosystems of most other (modern) languages, where there’s a single canonical tool that satisfies all the requirements.</p>

<p>Why can’t Java have that?</p>

<h2 id="wrapping-up">Wrapping up</h2>

<p>I’m currently using Prettier Java for EqualsVerifier, but I’m very open to switching to another tool. However, I have a hard time picking one, because they all have problems that make me not want to use them. I’ve already switched formatters before (once from nothing to google-java-format, and once from google-java-format to Prettier Java), so if I’m going to switch again, I want to switch to something <em>really good</em>, and that simply doesn’t exist right now.</p>

<p>On the other hand, inertia means that I have to stay with Prettier Java, which causes annoying problems every time it updates.</p>

<p>And then there’s the Git history pollution that occurs when you switch formatters (or update Prettier Java). Fortunately, Git has a clever way to deal with that: you can add a file with Git commit hashes to your repo, and then Git is able to ignore those commits when doing a Git blame. Read <a href="https://moxio.com/blog/ignoring-bulk-change-commits-with-git-blame/">this</a> for more info on that.</p>

<h2 id="conclusion">Conclusion</h2>

<p>If you don’t care about running the formatter from the command-line, and you are ethically OK with it, I think Palantir is probably your best option. Otherwise, there is no formatter I can currently recommend. Maybe Eclipse is the lesser of all evils.</p>

<p>Is there a formatter I missed? Or some configuration or feature that could change my assessment for a formatter? Please let me know!</p>]]></content><author><name>Jan Ouwens</name></author><category term="java" /><category term="formatter" /><summary type="html"><![CDATA[In which I compare and rate code formatting tools for Java.]]></summary></entry><entry><title type="html">Looking back on 15 years of EqualsVerifier</title><link href="https://jqno.nl/post/2024/06/01/looking-back-on-15-years-of-equalsverifier/" rel="alternate" type="text/html" title="Looking back on 15 years of EqualsVerifier" /><published>2024-06-01T00:00:00+00:00</published><updated>2024-06-01T00:00:00+00:00</updated><id>https://jqno.nl/post/2024/06/01/looking-back-on-15-years-of-equalsverifier</id><content type="html" xml:base="https://jqno.nl/post/2024/06/01/looking-back-on-15-years-of-equalsverifier/"><![CDATA[<p>Today, I celebrate EqualsVerifier’s 15th birthday (observed) with this retrospective blog post. It’s a long read, but then, 15 years is a long time too!</p>

<p>In those 15 years, I met and married my wife, had a daughter who now goes to school, moved house three times, visited twelve countries across three continents, lived through a global pandemic, changed jobs four times…a lot happened.</p>

<p>I’d like to take a moment to look back, and write down some of my memories from the project. I’ll start with EqualsVerifier itself, and finish by talking a bit about the people around the project.</p>

<h2 id="table-of-contents">Table of contents</h2>

<p>This is a long post; feel free to skip to the sections that sound most interesting to you!</p>

<ul>
  <li><a href="#evolution">The evolution of EqualsVerifier</a>
    <ul>
      <li><a href="#beginning">The early beginning</a></li>
      <li><a href="#0x-era">0.x era</a></li>
      <li><a href="#1x-era">1.x era</a></li>
      <li><a href="#2x-era">2.x era</a></li>
      <li><a href="#3x-era">3.x era</a></li>
    </ul>
  </li>
  <li><a href="#languages">Java and other languages</a></li>
  <li><a href="#people">People</a>
    <ul>
      <li><a href="#contributors-users">Contributors and users</a></li>
      <li><a href="#conference-interactions">Conference interactions</a></li>
      <li><a href="#honorable-mentions">Honorable mentions</a></li>
    </ul>
  </li>
  <li><a href="#personal-life">Personal life</a></li>
  <li><a href="#different">What would I have done differently?</a></li>
  <li><a href="#whats-next">What’s next</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<p><a id="evolution"></a></p>
<h2 id="the-evolution-of-equalsverifier">The evolution of EqualsVerifier</h2>

<p><a id="beginning"></a></p>
<h3 id="the-early-beginning">The early beginning</h3>

<p>15 years ago, I was working for <a href="https://www.topdesk.nl">TOPdesk</a>, and I was trying to introduce the practice of unit testing in my team. I noticed that testing an <code class="language-plaintext highlighter-rouge">equals</code> method was pretty hard to do right. You either had to write lots of screens of code, or skip over things.</p>

<p>I was reading a lot of programming books at the time, and J.B. Rainsberger’s <a href="https://www.amazon.com/JUnit-Recipes-Practical-Methods-Programmer/dp/1932394230">JUnit Recipes</a> pointed me to <a href="https://gsbase.sourceforge.net/index.html">GSBase</a>’s EqualsTester. It was very nice, but it required that you implement <code class="language-plaintext highlighter-rouge">equals</code> with <code class="language-plaintext highlighter-rouge">getClass()</code> instead of <code class="language-plaintext highlighter-rouge">instanceof</code>. This is technically a violation of the <a href="https://www.baeldung.com/cs/liskov-substitution-principle">Liskov Substitution Principle</a> and I was still young and idealistic enough to care about that. Also, it wasn’t very thorough. There are plenty of ways in which the <code class="language-plaintext highlighter-rouge">equals</code> can be broken, and while it tested the most important ones, it skipped over many of them too.</p>

<p>In 2008, I attended Devoxx Belgium for the first time, and saw a talk about this cool new programming language, Scala. I bought the book and started reading it cover to cover, and promised myself I’d do a Scala side project once I’d finished reading the book. However, near the end of the book, for some reason there was a chapter on how you can write an <code class="language-plaintext highlighter-rouge">equals</code> method that could be overridden to add state while still fulfilling both the <code class="language-plaintext highlighter-rouge">equals</code> contract <em>and</em> the Liskov Substitution Principle (you can read it online <a href="https://www.artima.com/articles/how-to-write-an-equality-method-in-java">here</a>). Josh Bloch, in his <a href="https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997">Effective Java</a>, said this was impossible, so this tickled me.</p>

<p>So one weekend, I started coding (in Java, not Scala) to see if I could apply what I’d learned from the Scala book to write a replacement for EqualsTester, maybe even applying some reflection tricks to make the API a bit nicer. And indeed, in two weekends I had something working. I called it EqualsVerifier, because the name EqualsTester was obviously already taken and I couldn’t come up with something better.</p>

<p>I don’t remember the exact date when I first started coding on EqualsVerifier, but <a href="https://github.com/jqno/equalsverifier/commits/main/?since=2009-06-01&amp;until=2009-06-02">I first uploaded it on June 1st, 2009</a>, which is why I observe its birthday on that date.</p>

<p>I did, eventually, get around to that <a href="https://jqno.nl/post/2012/09/11/foobal-predicting-soccer-matches-with-scala-and-drools/">Scala side project</a> I promised myself too, but it took a while. I also completely blew off a friend who wanted to create a game with me. I kept promising him that I’d get to it when EqualsVerifier was done, but here we are. Harald, if you’re reading this, I’m sorry!</p>

<p><a id="0x-era"></a></p>
<h3 id="0x-era">0.x era</h3>

<p>What I uploaded on June 1st, 2009 was the source, dependencies, and binaries of version 0.1. It ran on Java 6. I used <a href="https://en.wikipedia.org/wiki/Google_Developers#Google_Code">Google Code</a> to host the source code and the binaries, because it was the popular and obvious choice at the time. Google was still a reliable and exciting company where you could be sure that if it launched a product, it would be there <a href="https://killedbygoogle.com/">forever</a>. It was very modern, as it natively supported SVN for source control.</p>

<p>In the meantime, Roel, my co-worker and mentor at the time, was working with a friend on a small open source Java library of his own which you may have heard of, called Lombok. They were using this weird new GitHub thing that nobody had heard of yet. We had many conversations about our respective projects, which were incredibly helpful to me. Roel, if you’re reading this: thanks for helping me when I was just starting out!</p>

<p>A typical call to EqualsVerifier, at the time, looked like this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Foo</span> <span class="n">first</span> <span class="o">=</span> <span class="o">...</span>
<span class="nc">Foo</span> <span class="n">second</span> <span class="o">=</span> <span class="o">...</span>

<span class="nc">EqualsVerifier</span><span class="o">.</span><span class="na">forExamples</span><span class="o">(</span><span class="n">first</span><span class="o">,</span> <span class="n">second</span><span class="o">)</span>
        <span class="o">.</span><span class="na">verify</span><span class="o">();</span>
</code></pre></div></div>

<p>There was <code class="language-plaintext highlighter-rouge">.forClass()</code> too, but I wasn’t confident enough yet that it was good enough to be able to generate all the necessary instances that EqualsVerifier needed. So I kept <code class="language-plaintext highlighter-rouge">.forExamples()</code> around as a fallback for a long time.</p>

<p>I was particularly proud of my Ant script that automatically uploaded artifacts to Google Code, so people could download the JAR file directly. However, people quickly started pestering me to add support for Maven, which was this new thing that started to get momentum. And the <a href="https://github.com/jqno/equalsverifier/issues/8">very first contribution from somebody I didn’t know</a> solved exactly that: it added a custom Maven repository to the SVN repository that people could add in their pom file’s <code class="language-plaintext highlighter-rouge">&lt;repositories&gt;</code> section, and then they could automatically download EqualsVerifier from there.</p>

<p><a id="1x-era"></a></p>
<h3 id="1x-era">1.x era</h3>

<p>In 2011, I changed jobs and moved back to my home town. EqualsVerifier was already pretty stable at the time, so I decided it was time for the big 1.0. In this version, EqualsVerifier was able to detect annotations for the first time: there was support for <code class="language-plaintext highlighter-rouge">@NotNull</code>, <code class="language-plaintext highlighter-rouge">@Immutable</code>, and rudimentary support for JPA.</p>

<p>After some more pestering from people, I finally uploaded an EqualsVerifier release to Maven Central for the first time: version 1.0.2, in August of 2011, and every release since then was uploaded there. Including the <a href="https://jqno.nl/post/2013/06/11/what-happened-to-equalsverifier-1-3/">botched 1.3 release</a>, which will remain available for as long as Maven Central will be around since Maven Central is immutable.</p>

<p>I set up continuous integration with Travis CI in 2013, because I thought it would be nice if I could test EqualsVerifier against a bunch of different Java versions before each release.</p>

<p>In the meantime, EqualsVerifier was slowly spreading. It’s very difficult to keep track of usage of an open source Java library. Maven Central provides some download numbers, but they don’t say a lot: a company might download the artifact and put it in their cache and a hundred people could use it without me knowing about it; or a single person using three computers might download it on each, and it would count for three downloads. However, one obvious milestone was the first time someone <a href="https://stackoverflow.com/q/19926486/127863">asked a question on StackOverflow</a> (they even introduced the <code class="language-plaintext highlighter-rouge">[equalsverifier]</code> tag for it!), I was pretty excited when I found it, until I discovered it had been posted a year earlier and I’d never even noticed…</p>

<p>Late in the 1.x era, I made some major changes to the build and distribution. In 2014, I finally switched from Ant to Maven, and I also began distributing EqualsVerifier as an “uberjar” (a single JAR file that contains all dependencies), after receiving several issues from people with version conflicts in EqualsVerifier’s dependencies. The tool I used to create it, had the best name of any open source project I have ever seen: <em>Jar Jar Links</em>. Get it? It links jars!</p>

<p>Around this time, I also discovered a couple of really dirty hacks: <a href="https://jqno.nl/post/2014/08/20/the-things-we-do-for-compatibility/">compiling at runtime</a> (which I used to support multiple Java versions from a single codebase) and <a href="https://jqno.nl/post/2015/02/28/hacking-java-enums/">creating enum entries at runtime</a> (which I discovered by accident while debugging an EqualsVerifier issue). Both ended up in my <a href="https://jqno.nl/talks/dont-hack-the-platform/">Don’t hack the platform? ☠️💣💥</a> talk.</p>

<p>In 2015, Google announced that they were retiring Google Code. I was still pretty upset about the death of <a href="https://en.wikipedia.org/wiki/Google_Reader">Google Reader</a> two years prior. Wait, who am I kidding? I’m still upset today, and since the demise of these two services (as well as Google Music and Google Wave), I refuse to start using any new service Google introduces, since they’ll probably kill it off anyway by the time I’m really invested in the product.</p>

<p>Anyway, I moved EqualsVerifier over to GitHub. I’d moved the code there earlier, but I’d kept Google Code around for the issue tracker. Fortunately, I found a script that was able to migrate all the issues, so I was able to keep them for reference. This also meant the end of my cool script to upload artifacts to Google Code, but it didn’t matter, because Maven Central was serving very well.</p>

<p><a id="2x-era"></a></p>
<h3 id="2x-era">2.x era</h3>

<p>In 2016, EqualsVerifier jumped to 2.0. There were a couple of breaking changes: I finally dropped support for Java 6, I changed the default behaviour for EqualsVerifier to always check that all fields are used unless disabled explicitly (before, this was an opt-in behaviour), and I replaced CGLib, the bytecode magic library, with Byte Buddy, which could do the same things but more and better and with much better support for new Java versions (turns out one of EqualsVerifier’s users liked using Early Access versions and was not afraid to open issues 😄). I also introduced a lot of static analysis, using <a href="https://checkstyle.org/">Checkstyle</a>, <a href="https://en.wikipedia.org/wiki/FindBugs">FindBugs</a> (later SpotBugs), and of course <a href="https://pitest.org/">PIT</a> for mutation testing.</p>

<p>The main reason for the jump to 2.0, however, was the support for generics. Someone had an edge case where they would do this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Foo</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Bar</span><span class="o">&gt;</span> <span class="n">bars</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">equals</span><span class="o">(</span><span class="nc">Object</span> <span class="n">obj</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!(</span><span class="n">obj</span> <span class="k">instanceof</span> <span class="nc">Foo</span><span class="o">))</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="nc">Foo</span> <span class="n">other</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Foo</span><span class="o">)</span><span class="n">obj</span><span class="o">;</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">bars</span><span class="o">.</span><span class="na">size</span><span class="o">();</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="nc">Bar</span> <span class="n">a</span> <span class="o">=</span> <span class="n">bars</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
            <span class="nc">Bar</span> <span class="n">b</span> <span class="o">=</span> <span class="n">other</span><span class="o">.</span><span class="na">bars</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
            <span class="c1">// Compare Bars</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>That would lead to a <code class="language-plaintext highlighter-rouge">ClassCastException</code>, because due to type erasure, EqualsVerifier didn’t know the generic type of the <code class="language-plaintext highlighter-rouge">List</code>, and would just treat it like a raw <code class="language-plaintext highlighter-rouge">List</code> and put strings in that list which obviously can’t be assigned to a variable of type <code class="language-plaintext highlighter-rouge">Bar</code>. This was generally ok though, because normally you would just call <code class="language-plaintext highlighter-rouge">bars.equals(other.bars)</code>, and <code class="language-plaintext highlighter-rouge">List</code>’s <code class="language-plaintext highlighter-rouge">equals</code> method would figure it out. There’s no good reason to unwrap lists in an <code class="language-plaintext highlighter-rouge">equals</code> method like this, except with Android’s <code class="language-plaintext highlighter-rouge">SparseArray</code> class which, for some bizarre reason, didn’t implement <code class="language-plaintext highlighter-rouge">equals()</code>.</p>

<p>So I over the course of 4 months and 165 commits (yes, I kept track), I made two attempts (the first one failed), and finally, EqualsVerifier could overcome type erasure and figure out the appropriate types to populate the <code class="language-plaintext highlighter-rouge">List</code> (and any other generic data structure). I wrote about it in two parts: <a href="https://jqno.nl/post/2016/06/23/generics-in-equalsverifier-part-1/">part 1</a> and <a href="https://jqno.nl/post/2016/06/26/generics-in-equalsverifier-part-2/">part 2</a>.</p>

<p>In this period, I also made huge improvements to the documentation. For instance, I added a full <a href="https://jqno.nl/equalsverifier/manual/">manual</a> to the website while on vacation in Paris. And I got to give <a href="https://www.youtube.com/watch?v=pNJ_O10XaoM">a talk about EqualsVerifier at Devoxx Belgium</a>! The recording is from 2017, but it’s still relevant today.</p>

<p><a href="https://www.youtube.com/watch?v=pNJ_O10XaoM"><img src="/images/2024-06-01-looking-back-on-15-years-of-equalsverifier/devoxx2.png" alt="Me presenting EqualsVerifier at Devoxx 2017" /></a><br />Me presenting EqualsVerifier at Devoxx 2017</p>

<p><a id="3x-era"></a></p>
<h3 id="3x-era">3.x era</h3>

<p>2018 already saw the release of EqualsVerifier 3.0. The main reason for this were some new features, like re-usable configurations and the <code class="language-plaintext highlighter-rouge">report()</code> function that allows integration with other tooling. I took the opportunity to drop Java 7 support (and replace <em>a lot</em> of anonymous inner classes with nice lambdas) and improve error messages.</p>

<p>At some point I found out that Travis CI wasn’t able to cache Maven artifacts, so every build required downloading the entire internet. That’s time-consuming, polluting and pointless, so I decided to switch to Circle CI. Around that time, GitHub Actions was introduced too, and not long after, I moved my pipelines again, so I would have everything in a single place on GitHub. Later on, I also started using the excellent <a href="https://jreleaser.org/">JReleaser</a> to run releases directly from GitHub Actions instead of from my local machine, as I was doing before.</p>

<p>In 2019, I introduced a code formatter, since I no longer wanted to have to thing about spacing and making everything look consistent. I was working with Scala at the time, and its excellent <a href="https://scalameta.org/scalafmt/">Scalafmt</a> made me want to have similar capabilities for Java. At first, I picked the <a href="https://github.com/google/google-java-format">Google Java formatter</a> for this task, but I never really liked it. True, it integrates nicely with Maven, and I like its philosophy of being unconfigurable so people can’t bikeshed formatting settings. However, its decision to enforce a two-space indent in an ecosystem that has used 4 spaces since the 90s, is just <em>weird</em>. And while it does have an Android flag (which I used) to make it use 4 spaces, this also makes everything look really cramped. I found myself changing the logic of the code to make it look better after formatting. So I quickly switched to <a href="https://github.com/jhipster/prettier-java">Prettier Java</a>, a plugin to <a href="https://prettier.io/">Prettier</a>, which has saner defaults and results in code that’s more readable. Its biggest downside is that it requires NPM to run, so I’m still looking out for something better. Please let me know if you know something! (And no, IntelliJ’s built-in formatter is not better, since you can’t run it from Maven.)</p>

<p>I did learn about <a href="https://www.michaelheap.com/git-ignore-rev/">.git-blame-ignore-revs</a> though, which you can use to skip commits when doing a <code class="language-plaintext highlighter-rouge">git blame</code>. Very handy when you need to reformat all your code…</p>

<p>In 2022,  while doing maintenance on the build script, I noticed a missing entry in the <code class="language-plaintext highlighter-rouge">MANIFEST.MF</code> file. I don’t remember if this actually got released or not, but I decided I needed to be able to “unit test” my build. Also, with the advent of new language features like records and sealed types, I wanted to step up my game of supporting multiple Java versions from a single code base, and start using multi-release JAR files. This resulted in a huge overhaul of the build scripts and the introduction of Maven submodules. I actually received a lot of help from <a href="https://twitter.com/khmarbaise">Karl Heinz Marbaise</a> (current chairman of the Apache Maven project). Karl, if you’re reading this: thank you!</p>

<p>The last big feature I added was improved JPA support, mainly because I started using JPA myself for the first time. Yes, this happened in 2022.</p>

<p><a id="languages"></a></p>
<h2 id="java-and-other-languages">Java and other languages</h2>

<p>The funny thing is, for most of these past 15 years, I didn’t even use Java for my main job. I had a long stint (more than 6 years) as a Scala developer (buying that book turned out a life-changing event!), I did C# for two years, and for one year my main languages were Ruby and <a href="https://en.wikipedia.org/wiki/Jess_(programming_language)">Jess</a>, which is a Prolog-like language with a Lisp syntax.</p>

<p>There was a time where I was seriously considering doing an EqualsVerifier.NET together with a colleague, since C#’s <code class="language-plaintext highlighter-rouge">Equals()</code> method has basically the same contract as the Java one. But then I found out that <a href="https://github.com/magicmonty/equalsverifier-net">someone already beat me to it</a>, although sadly this project didn’t get very far.</p>

<p>While I was doing Scala, I could of course have been using EqualsVerifier. However, I never did—in all those years, I only had to write an <code class="language-plaintext highlighter-rouge">equals</code> method twice, because Scala’s case classes made it virtually unnecessary to do so.</p>

<p>However, having EqualsVerifier as a side project did keep me grounded in the Java community, which was nice.</p>

<p><a id="people"></a></p>
<h2 id="people">People</h2>

<p><a id="contributors-users"></a></p>
<h3 id="contributors-and-users">Contributors and users</h3>

<p>In its 15 years of existence, 32 people (and 2 bots) have contributed to EqualsVerifier, which seems small if you compare it to other open source projects. But let’s not fall prey to survivorship bias. This is actually really amazing for a project that I made to scratch my own itch, and that I never even actively advertised—at least, not until I first started speaking at conferences.</p>

<p>It’s even more amazing when you start considering the people I interacted with along the way, who didn’t necessarily make code contributions to the project.</p>

<p>136 people were thoughtful enough to open an issue on GitHub when they found a problem with EqualsVerifier, and this doesn’t even include the issues that were raised back in the Google Code days. Some of these issues were easily solved. Some of these were a pain in the behind to deal with. But all of them made EqualsVerifier better, and I’m grateful for each of them. Especially for the hard ones!</p>

<p>But the most fun thing for me is finding out who are using EqualsVerifier, and where. As I mentioned before, there’s no good way to track this, so I have to rely on people to tell me, which they sometimes do. GitHub issues are a nice source for this: people who open an issue, often have their employer and/or their country of origin in their profile. Another great source is friends and (former) coworkers who will sometimes let me know that their new company is using EqualsVerifier. The most fun occurrence of this happened recently, when a colleague started working for my first employer’s biggest competitor and sent me a screenshot of a package statement and a few import statements, proving that EqualsVerifier was used there.</p>

<p><img src="/images/2024-06-01-looking-back-on-15-years-of-equalsverifier/devoxx.jpg" alt="Me presenting EqualsVerifier at Devoxx 2017" /><br />Me presenting EqualsVerifier at Devoxx 2017</p>

<p><a id="conference-interactions"></a></p>
<h3 id="conference-interactions">Conference interactions</h3>

<p>And then there’s conference interactions. At Devoxx Belgium 2014, I saw a talk about <a href="https://pitest.org/">PIT</a>. I was immediately excited to try it out in EqualsVerifier, and said so on Twitter. I quickly got a reply from its creator, <a href="https://twitter.com/_pitest">Henry Coles</a>, who commented that would be a nice symmetry since he uses EqualsVerifier in PIT! Henry, if you’re reading this: thank you for that tweet, for allowing me to test-drive Arcmutate, and for the chats at JavaZone in Oslo!</p>

<p>In 2016, when I was a Scala developer at Codestar, I gave <a href="https://www.youtube.com/watch?v=W37Mp3mBYLw">my first and only talk at a Scala conference</a>, the Typelevel Summit in Olso. In the introduction, I casually mentioned that I must be the only attendee who did Scala for their day job and Java as a hobby, which got a good laugh.</p>

<p>While at that same conference in Oslo, I was thinking about something a former colleague once said: don’t be afraid of your heroes; they’re people too! I remembered that <a href="https://twitter.com/rafaelcodes">Rafael Winterhalter</a>, creator of Byte Buddy, lived in Oslo, so I sent him a message and we ended up having lunch together. It was a nice experience. Rafael, if you’re reading this, thank you for that, and also for helping me integrate Byte Buddy!</p>

<p>I also won’t forget how, at Devoxx Belgium 2023, I was talking with two acquaintances in the exhibit hall, and somebody walked past, slowed down for a moment as he looked at me, yelled “EqualsVerifier!”, and continued walking. I still have no idea who that was…</p>

<p>EqualsVerifier also comes up occasionally in conferences that I don’t attend, because several kind people have included it in their talks. For example, <a href="https://twitter.com/johanjanssen42">Johan Janssen</a>’s “<a href="https://youtu.be/pH_mHWQmtDst?t=1160">Java Hidden Gems</a>”, <a href="https://twitter.com/TCoolsIT">Tom Cools</a>’s and <a href="https://twitter.com/Elien_Callens">Elien Callens</a>’s “<a href="https://youtu.be/rJ08SZEe36g&amp;t=2795">Leaving a Legacy</a>” and <a href="https://twitter.com/michaelvitz">Michael Vitz</a>’s “<a href="https://www.youtube.com/watch?v=vn9P38o03TQ&amp;t=1346s">Beyond Built-in: Advanced Testing Techniques for Spring Boot Applications</a>”.</p>

<p>I found out about these, because friends who did attend have sent me pictures. In fact, I only found out about the last one <em>yesterday</em> when Paco sent me a picture from Spring I/O 2024! So, thanks Jaap, Jan-Hendrik, Paco, and more, for sending me these pictures!</p>

<p>Johan, Tom, Elien and Michael, a special thanks to you for the shout-outs! It makes me really happy to know that I’ve built something that you like so much that you include it in your talks.</p>

<p><a id="honorable-mentions"></a></p>
<h3 id="honorable-mentions">Honorable mentions</h3>

<p>And then there are some other nice moments that happened as a result of EqualsVerifier:</p>

<ul>
  <li>
    <p>I was invited for an interview by the <a href="https://anchor.fm/barcoding/episodes/Episode-20---The-tales-of-the-EqualsVerifier-project-e1993ev">BarCoding Podcast</a>. Paulien, Arnout, if you’re reading this: thanks for having me on the show.</p>
  </li>
  <li>
    <p>Tom Cools made a slick <a href="https://www.youtube.com/watch?v=ivRjf8yvVMk">getting-started video about EqualsVerifier</a>, which he graciously allowed me to link to. It’s a great addition to the wall of text that is EqualsVerifier’s website. Tom, if you’re reading this, thank you for the video, for being a woefully underpaid de-facto devrel for EqualsVerifier, and for being great company at conferences!</p>
  </li>
  <li>
    <p>I also liked when J.B. Rainsberger, the very person who wrote the book that introduced me to EqualsTester, posted this tweet:<br /><a href="https://twitter.com/jbrains/status/661556171166392320"><img src="/images/2024-06-01-looking-back-on-15-years-of-equalsverifier/needless-constraints.png" alt="J.B. Rainsberger: &quot;All right. Goodbye. EqualsVerifier. I cannot handle your needles constraints.&quot;" /></a><br />It inspired me to find ways to improve the user experience for EqualsVerifier. One thing I did was to write a <a href="https://jqno.nl/equalsverifier/manual/">manual</a>, where I attempt to explain the reasoning behind the contraints and how to deal with them. Another is the introduction of the <code class="language-plaintext highlighter-rouge">simple()</code> method, which makes it easy to side-step some of them. J.B., if you’re reading this, thanks for speaking up!</p>
  </li>
  <li>
    <p>Henry Coles named <a href="https://blog.arcmutate.com/the-verifier-test-pattern/">his verifier test pattern</a> after EqualsVerifier, which I found amusing, because as I mentioned, I picked ‘Verifier’ only because the name EqualsTester was already taken. I never actually liked the word… Anyway, Henry, thank you for that one too!</p>
  </li>
  <li>
    <p><a href="https://twitter.com/johanjanssen42">Johan Janssen</a> wrote an <a href="https://www.infoq.com/news/2023/11/equalsverifier-jpa-entities/">InfoQ article</a> about EqualsVerifier’s improved JPA support. Johan, if you’re reading this: thanks!</p>
  </li>
  <li>
    <p>Somehow, EqualsVerifier also made it to <a href="https://www.baeldung.com/java-equals-hashcode-contracts">Baeldung</a>.</p>
  </li>
  <li>
    <p>Finally, most of my past and current employers allowed me to work on EqualsVerifier on company time, for example during 10% time or R&amp;D days. <a href="https://www.topdesk.nl">TOPdesk</a>, <a href="https://www.sioux.eu/">Sioux</a>, <a href="http://www.codestar.nl">Codestar</a> (Ordina), <a href="https://yoink.nl/">Yoink</a>: thank you, I appreciate it!</p>
  </li>
</ul>

<p><a id="personal-life"></a></p>
<h2 id="personal-life">Personal life</h2>

<p>For the first half of EqualsVerifier’s existence, I was single and living alone, so I had a lot of free time to hack on EqualsVerifier. And I did. Of course after I met my wife and had a daughter, things started to change. I managed to clear every single GitHub issue, for the first time ever, a few days before our daughter was born, and for about a year I actually managed to keep up. EqualsVerifier was already pretty stable, there weren’t a lot of issues, and the ones that did come in were relatively easy to solve.</p>

<p>Of course, when you have a family, your priorities shift (as they should!), and unfortunately so did my energy levels. As a result, the past years I haven’t been able to spend as much time on EqualsVerifier as before. I haven’t been able to add a lot of new features or do big refatorings recently. I still try to keep up with the issues, but I have some hard ones that have been there for more than a year. I do intend to get to them some day though!</p>

<p><a id="different"></a></p>
<h2 id="what-would-i-have-done-differently">What would I have done differently?</h2>

<p>I’ve enjoyed my run of making EqualsVerifier, and I think the project has found its niche of happy users. Also, I’m not someone who regrets things. In life and work, I make decisions. Some of them go well, some of them don’t, and every decision is an opportunity for learning and experiencing thing that I wouldn’t have had if I’d made a different decision.</p>

<p>If I really have to name one thing, it’s the mutable nature of EqualsVerifier’s architecture. When EqualsVerifier instantiates an object, it mutates the state of that object for each test it performs. Then it has to be careful to restore the state for the next test. This has caused some gnarly debugging sessions over the years. If I had to rewrite EqualsVerifier today, I’d create a new instance for every test and then discard it.</p>

<p>Ironically, the Scala book that triggered it all already emphasized immutability. I just hadn’t internalized that message yet when I started EqualsVerifier, and I’ve been stuck with that decision ever since.</p>

<p><a id="whats-next"></a></p>
<h2 id="whats-next">What’s next</h2>

<p>EqualsVerifier has been reasonably stable for some years now. There haven’t been a lot of big new features, and most issues that people open pertain to prefab values, which I can deal with quickly. Every six months, I need to update the build pipeline to ensure everything still works on the latest and greatest Java version.</p>

<p>Still, part of the reason why I still enjoy working on EqualsVerifier, is that it’s a place where I can do experiments and gain some deep knowledge about Java. That’s why I plan to fully adopt the Java module system for the 4.0 release, which I’m hoping to start working on soon-ish. And if I’m doing 4.0, it’s also time to bump the Java version again. I’m not sure yet if I’ll go to 11, or all the way up to 17. I’m open to opinions on this matter.</p>

<p>Another thing I’d like to do, is to reduce the need for adding prefab values, both for the end user and internally, because that would save me a bunch of GitHub issues every year. I have some ideas on how to do that, but they’re hard to implement and might not make it into 4.0 yet. They require a more immutable architecture, and in fact I’ve already started some refactorings in that area. But given my time constraints, we’ll have to see how that goes.</p>

<p><a id="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>

<p>It’s been fun looking back on these past 15 years. EqualsVerifier has gone through a lot of changes, and has matured a lot. And so have I.</p>

<p>I might write another post like this in 15 years, so if you liked this, please check back in 2039!</p>]]></content><author><name>Jan Ouwens</name></author><category term="equalsverifier" /><summary type="html"><![CDATA[In which I take a trip down memory lane.]]></summary></entry><entry><title type="html">Running a local LLM with Ollama</title><link href="https://jqno.nl/post/2024/01/25/running-a-llm-assistant-with-ollama/" rel="alternate" type="text/html" title="Running a local LLM with Ollama" /><published>2024-01-25T00:00:00+00:00</published><updated>2024-01-25T00:00:00+00:00</updated><id>https://jqno.nl/post/2024/01/25/running-a-llm-assistant-with-ollama</id><content type="html" xml:base="https://jqno.nl/post/2024/01/25/running-a-llm-assistant-with-ollama/"><![CDATA[<p>It’s January 2024 as I write this; I fully expect this post to be out of date by tomorrow, or even sooner. But I think this is exciting!</p>

<p>Running a Large Language Model, or LLM, or AI assistant locally always seemed like something that only the really dedicated hobbyists could do. It seemed to require lots of manual build steps and complicated tinkering to get something working. This is no longer true.</p>

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

<p>Why would you want to run an LLM locally? I’ve written about <a href="/tags/#ai">AI</a>, and specifically ChatGPT, before. ChatGPT and most other popular AI tools have a big disadvantage: everything you put in can be used to train their underlying models. That’s a big problem for privacy and compliance. Most of the companies I work for don’t allow their data to be sent to these services, and therefore, I can’t always use them at work.</p>

<p>If only I could run an LLM on my laptop: then the data would never leave my machine, and it would be OK to use. That would be amazing!</p>

<h2 id="how">How?</h2>

<p>Using a tool called <a href="https://ollama.ai/">Ollama</a>, which could be described as a kind of Docker for LLMs, it’s surprisingly easy to set up:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell">brew <span class="nb">install </span>ollama
ollama serve</code></pre></figure>

<p>Now you can open another terminal, and do this:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell">ollama run llama2</code></pre></figure>

<p>This gets you a ChatGPT-like prompt in your terminal. That’s it!</p>

<p>Note that the first time you do this, it will pull the <code class="language-plaintext highlighter-rouge">llama2</code> model. This is a few gigs large, so the download might take a while. Of course that’s no different from the average Docker image. (Note that there are many models to choose from; see <a href="https://ollama.ai/library">the list on Ollama’s website</a>.)</p>

<p>Instead of running the prompt on your terminal, you can also integrate it with your IDE. There’s <a href="https://github.com/David-Kunz/gen.nvim">Gen.nvim</a> for Neovim, and Continue for <a href="https://plugins.jetbrains.com/plugin/22707-continue">Jetbrains</a> and <a href="https://marketplace.visualstudio.com/items?itemName=Continue.continue">VS Code</a>. There’s also an official <a href="https://hub.docker.com/r/ollama/ollama">Docker image</a>.</p>

<h2 id="performance">Performance</h2>

<p>If you’re doing all this on an Apple Silicon MacBook, you’re golden. These machines seem to be built for this kind of stuff.</p>

<p>However, if you have a different setup, things might be slow. For instance, I’m running Linux on a Dell XPS with two GPUs. By default, Ollama will use my built-in GPU, which is slower. I can’t make it use the dedicated GPU directly. Instead, I have to install the <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">Nvidia Container Toolkit</a> and run Ollama inside a Docker container with the <code class="language-plaintext highlighter-rouge">nvidia</code> runtime, like this:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell">docker run <span class="nt">-d</span> <span class="nt">--runtime</span><span class="o">=</span>nvidia <span class="nt">--gpus</span><span class="o">=</span>all <span class="se">\</span>
    <span class="nt">-v</span> ollama:/root/.ollama <span class="nt">-p</span> 11434:11434 <span class="se">\</span>
    <span class="nt">--name</span> ollama ollama/ollama

docker <span class="nb">exec</span> <span class="nt">-it</span> ollama ollama run llama2</code></pre></figure>

<p>By doing this, Ollama will use the dedicated GPU. Unfortunately, it’s a hassle and the speed improvement is not significant. I might have to switch to Apple…</p>

<p>In the meantime, it helps to use a smaller model. I haven’t done a lot of research into this, but <a href="https://ollama.ai/library/deepseek-coder:1.3b">DeepSeek-Coder 1.3B</a> seems to work well.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I’m really impressed by this. It’s impressive that it’s possible to run a local LLM in the first place. It’s impressive that it can be done on hardware that regular people can buy (even if it’s still really really expensive hardware). And it’s impressive how easy it is to get working.</p>]]></content><author><name>Jan Ouwens</name></author><category term="ai" /><summary type="html"><![CDATA[In which I describe how easy it is to set up a local LLM on my laptop.]]></summary></entry><entry><title type="html">About fonts</title><link href="https://jqno.nl/post/2024/01/10/about-fonts/" rel="alternate" type="text/html" title="About fonts" /><published>2024-01-10T00:00:00+00:00</published><updated>2024-01-10T00:00:00+00:00</updated><id>https://jqno.nl/post/2024/01/10/about-fonts</id><content type="html" xml:base="https://jqno.nl/post/2024/01/10/about-fonts/"><![CDATA[<p>I fell into a bit of a rabbit hole after reading Matthew Butterick’s <a href="https://practicaltypography.com/">Practical Typography</a>: I got a little obsessed with fonts for a while. It convinced me that I want my website to look pretty, and one important part of that is the choice of font. There are three applications of fonts that have been on my mind: headings and body text on my website, and a coding font for my editor (and also for code snippets on my website). Let’s look at each in turn, but first, let’s set some criteria.</p>

<h2 id="criteria-for-a-font">Criteria for a font</h2>

<p>The purpose of a font is to make text look good.</p>

<p>That’s it. That’s the criterion.</p>

<p>However, this is hard to determine. Of course, there’s no accounting for taste, but Butterick talks about a lot of fonts in his book, and mentions which ones are good and which ones <a href="https://practicaltypography.com/system-fonts.html">aren’t</a>. However, he doesn’t give a lot of guidance on how to see the difference. Blogs on the web claim to do so, but don’t go into a lot of depth and generally constrains themselves to free fonts.</p>

<p>Which leads to another factor: cost. According to Butterick, <em>“<a href="https://practicaltypography.com/free-fonts.html">most free fonts are garbage</a>“</em>. I don’t mind spending some money, but fonts can get really expensive. Butterick also designs fonts, and his seem to be relatively cheap. Still, it’s a lot of money for a website that doesn’t get a lot of traffic.</p>

<p>But sometimes, big-name companies such as <a href="https://github.com/IBM/plex">IBM</a>, <a href="https://www.cooperhewitt.org/open-source-at-cooper-hewitt/cooper-hewitt-the-typeface-by-chester-jenkins/">The Smithsonian</a>, Adobe <a href="https://github.com/adobe-fonts/source-serif">1</a>, <a href="https://github.com/adobe-fonts/source-sans">2</a> and <a href="https://github.com/mozilla/Fira">Mozilla</a> release good fonts (that Butterick even approves of) and that are designed by renowned professionals, which is nice! In these cases though, for ideological reasons, I do prefer companies that have somewhat of a proven track record in free and open source software. For instance, all else being equal, I’d pick Mozilla’s Fira over IBM Plex.</p>

<p>Coding fonts have some other requirements as well. First of all, they need to be monospaced (well, for most people anyway), and they need to make the difference between certain symbols really clear: <code class="language-plaintext highlighter-rouge">0O</code>, <code class="language-plaintext highlighter-rouge">1lI</code></p>

<h2 id="coding">Coding</h2>

<p>So let’s talk about coding fonts first.</p>

<p><img src="/images/2024-01-10-about-fonts/coding.png" alt="Examples of coding fonts that I considered" /></p>

<p>The rabbit hole opened up in front of me when I read the section on <a href="https://practicaltypography.com/ligatures-in-programming-fonts-hell-no.html">ligatures in programming fonts</a> in Butterick’s book. He makes a few arguments for why they’re a bad idea, though I don’t think I ever experienced these problems myself. I’ve been using <a href="https://github.com/tonsky/FiraCode">Fira Code</a> (a fork of Mozilla’s Fira Mono with added ligatures) for a long while, and I have to say that I rather enjoy the ligatures.</p>

<p>But it did get me thinking, because I did get confused reactions sometimes from people looking at my screen, or at code snippets on my website or in <a href="https://jqno.nl/talks/">my talks</a>.</p>

<p>Also, Fira Code doesn’t have italics, so the font configuration in my terminal was kind of complex, pulling in a separate font for italics.</p>

<p>So I started looking around. I tried <a href="https://juliamono.netlify.app/">Julia Mono</a> for a while, but the letters felt uneven to me. I looked at <a href="https://markfromberg.com/projects/gintronic">Gintronic</a> which I think looks cute, but I wasn’t quite ready to drop $150 on it and risk getting bored with it later. I’ve also looked at <a href="https://www.ibm.com/plex/">IBM Plex Mono</a>, which is free and open source, but at the same time designed by typography professionals. But it left me feeling meh. I also wanted to try <a href="https://www.recursive.design/">Recursive Mono</a>, but I couldn’t get it to work in my terminal, <a href="https://sw.kovidgoyal.net/kitty/">Kitty</a>. Your mileage may vary, though.</p>

<p>Then I discovered GitHub’s new <a href="https://monaspace.githubnext.com/">Monaspace</a> font. I specifically liked its Argon variant. It has a very cool “texture healing” feature, which moves wide letters like <code class="language-plaintext highlighter-rouge">m</code> and narrow letters like <code class="language-plaintext highlighter-rouge">i</code> a few pixels to the side to make the text look more evenly spaced. But it’s <a href="https://github.com/githubnext/monaspace/issues/15">not supported on my terminal</a>. They’ve made a fix and promised to release it “soon”, but it’s been almost two months.</p>

<p>Eventually, I found <a href="https://commitmono.com/">Commit Mono</a>, a relatively new open source font that also does texture healing (they call it “smart kerning”). It has a few basic ligatures, which I’ve disabled. It looks really nice, but you do notice the letters “jumping around” a little while you type because of the smart kerning feature. I’m currently trying it out to see if I mind the jumping around, but I do like the overall looks of it. And it has a nice italic! I’ve already integrated it into my website.</p>

<p>Here’s an example of Commit Mono’s smart kerning feature at work. It’s especially visible around the <code class="language-plaintext highlighter-rouge">m</code>s, which seem to have a lot more space:</p>

<p><img src="/images/2024-01-10-about-fonts/smart-kerning.png" alt="Example of Commit Mono's smart kerning" /></p>

<blockquote>
  <p><strong>UPDATE, March 1st, 2024</strong>: I’ve concluded that I find the jumping around of letters with Commit Mono a bit annoying, so I’m back on the hunt for a new day-to-day font. I was able to make Recursive work in my terminal, but decided that I don’t like the looks enough. A former coworker <a href="https://twitter.com/skrebbel/status/1745360344803934450">suggested</a> I try <a href="https://typeof.net/Iosevka/">Iosevka</a>, which has <em>all the features</em> and then some. However, I just don’t like its narrow look. It’s great if you want to be able to see many columns of code in one window, but…I don’t. I prefer to see many lines at the same time, which means a wider font is more suited for me. I’d love to use Monaspace Argon, but GitHub still haven’t provided a new release, so it still doesn’t work with my terminal. I’m currently trying out the trial version of <a href="https://www.monolisa.dev/">MonoLisa</a>, which looks nice and ticks all the boxes, but I haven’t decided yet if I want to actually drop the cash on that one (though it’s relatively cheap for a font!) I could also see myself go back to Fira Code, but with ligatures disabled. In that case I’ll have to find another font to provide the italics, though.</p>

  <p>For the code samples on the website, it seemed clear to me that I should revert to Fira Code, for the simple reasons that a) I don’t use the italics there, and b) the body text is FiraGO (spoilers for the next section!), which is basically the same font. This makes things satisfyingly consistent. I mean, look at how those g’s look:</p>

  <p><span style="font-style:normal;">FiraGO: g</span><br /><span style="font-style:normal;"><code class="language-plaintext highlighter-rouge">FiraCode: g</code></span></p>
</blockquote>

<h2 id="body-text">Body text</h2>

<p>I’d never really considered the font of my website’s <a href="https://practicaltypography.com/body-text.html">body text</a> before. The theme I use sets it to this by default:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-apple-system</span><span class="o">,</span> <span class="nt">BlinkMacSystemFont</span><span class="o">,</span> <span class="s1">"Roboto"</span><span class="o">,</span> <span class="s1">"Segoe UI"</span><span class="o">,</span>
    <span class="s1">"Helvetica Neue"</span><span class="o">,</span> <span class="s1">"Lucida Grande"</span><span class="o">,</span> <span class="nt">Arial</span><span class="o">,</span> <span class="nt">sans-serif</span>
</code></pre></div></div>

<p>In other words, it selects whatever default font is installed on the system. This means two things:</p>

<ol>
  <li>My site will look different on different devices, and</li>
  <li>My site will look like every other site, and therefore boring.</li>
</ol>

<p>As Butterick says, <em>“<a href="https://practicaltypography.com/system-fonts.html">like Times New Roman, Arial is permanently associated with the work of people who will never care about typography</a>“</em></p>

<p><img src="/images/2024-01-10-about-fonts/body-text.png" alt="Examples of body text fonts that I considered" /></p>

<p>So I thought it would be nice to change it up a little, to give my site that little extra <em>je ne sais quoi</em>, to make it stand out from the pack without readers necessarily noticing why.</p>

<p>I mainly looked at sans-serif fonts, because I think these look nicer on screen. I did consider Butterick’s own font <a href="https://practicaltypography.com/heliotrope.html">Heliotrope</a> for a bit, but it has a slight weirdness to it which I decided doesn’t fit with my heading font of choice (see below).</p>

<p>Some sans-serif fonts I looked at were <a href="https://www.cooperhewitt.org/open-source-at-cooper-hewitt/cooper-hewitt-the-typeface-by-chester-jenkins/">Cooper Hewitt</a> and <a href="https://github.com/IBM/plex">IBM Plex Sans</a>, but I didn’t really like how they look.</p>

<p>In the end, I landed on Fira Code’s cousin <a href="https://github.com/bBoxType/FiraGO">FiraGO</a>, the successor to Mozilla’s <a href="https://github.com/mozilla/Fira">Fira Sans</a>. I think it looks nice! I particularly like the little opening in the bottom half of the g.</p>

<p>And you’re looking at it right now!</p>

<h2 id="headings">Headings</h2>

<p>Headings should look different from the body text, because their point is to reveal structure. Therefore, you can use a different font for them</p>

<p><img src="/images/2024-01-10-about-fonts/heading.png" alt="Example of TilburgsAns" /></p>

<p>My love for <a href="https://www.tilburgsans.nl/">TilburgsAns</a> is <a href="https://jqno.nl/other/#tilburgsans">no secret</a>. This font is an art project from my home town and it’s used there for many different things, like event announcement posters, shop window signs, and wine bottle labels.</p>

<p>It’s not a free font, but it has a very permissive license. Instead of paying for it, you can “<a href="https://www.tilburgsans.nl/nl/over-ans/adoptieplan.html">adopt</a>” a letter (each letter can be adopted only once) or a space (unlimited supply). I have adopted space #256.</p>

<p>I’ve used it for various private projects over the years, including my daughter’s birth announcement card. I’ve used it in the slides for <a href="https://jqno.nl/talks/">my talks</a>, and in fact, I’d already switched this site’s headers to TilburgsAns a few months ago.</p>

<p>Butterick warns against using <a href="https://practicaltypography.com/goofy-fonts.html">goofy fonts</a> in serious documents. I’m not sure if TilburgsAns qualifies as one, but I don’t care, I’m keeping it.</p>

<h2 id="what-ive-learned">What I’ve learned</h2>

<ul>
  <li>Apparently, I prefer humanist fonts over (neo-)grotesque or mechanic ones. The <a href="https://monaspace.githubnext.com/">Monaspace website</a> makes it easy to see the difference; generally, humanist fonts are more irregular. It’s not a deal breaker for me though.</li>
  <li>Speaking of <em>grotesque</em>, in the context of fonts, it means something completely different.</li>
  <li>And speaking of words, when people say <em>font</em>, they really mean <em>typeface</em>, but no-one really cares and neither do I. I’ll just continue saying <em>font</em> even if I mean <em>typeface</em>.</li>
  <li>Font websites can be really <em>out there</em>. Check out <a href="https://markfromberg.com/projects/gintronic">Gintronic</a>, <a href="https://commitmono.com/">Commit Mono</a>, and <a href="https://www.ibm.com/plex/">IBM Plex</a>. The IBM Plex one is especially infuriating.</li>
  <li>Many fonts have configuration options, with descriptive names like <code class="language-plaintext highlighter-rouge">ss01</code>, <code class="language-plaintext highlighter-rouge">ss02</code>, and <code class="language-plaintext highlighter-rouge">cv01</code>, which define the shape of certain letters or ligatures. They’re fun to play with!</li>
</ul>

<h2 id="the-end">The end?</h2>

<p>Of course, Butterick’s <a href="https://practicaltypography.com/">Practical Typography</a> has taught me a few things.</p>

<p>First of all, it brought home the point once more that making things available on the internet for free, is never actually really free. So if you’ve read his book as a result of this post, consider <a href="https://practicaltypography.com/how-to-pay-for-this-book.html">paying for it</a>. I have.</p>

<p>More importantly, the book has taught me that there’s more to making websites look good than just fonts. Much, much more. Way more than I thought. And since reading it, there’s loads of things I’d like to change about my website.</p>

<p>But I’m not going to do it. I’m not jumping into another rabbit hole.</p>

<p>At least not right now.</p>

<p>Therefore, I will leave you with a screenshot of what this page looks like <em>right now</em>. If you notice that this website looks very different, you will know that I’ve plunged into that rabbit hole after all. And that I’ve re-emerged. In that event, send me a message and ask me if I’m alright!</p>

<p><img src="/images/2024-01-10-about-fonts/screenshot.png" alt="Screenshot of what this site looks like in January 2024" /></p>]]></content><author><name>Jan Ouwens</name></author><category term="font" /><category term="typography" /><category term="blog" /><summary type="html"><![CDATA[In which I fall into a typographic rabbit hole.]]></summary></entry><entry><title type="html">Using ChatGPT to write a complete app</title><link href="https://jqno.nl/post/2023/05/09/using-chatgpt-to-write-a-complete-app/" rel="alternate" type="text/html" title="Using ChatGPT to write a complete app" /><published>2023-05-09T00:00:00+00:00</published><updated>2023-05-09T00:00:00+00:00</updated><id>https://jqno.nl/post/2023/05/09/using-chatgpt-to-write-a-complete-app</id><content type="html" xml:base="https://jqno.nl/post/2023/05/09/using-chatgpt-to-write-a-complete-app/"><![CDATA[<p>When I first started playing with ChatGPT, I had it write a <a href="/post/2023/01/25/my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/">simple, low effort blog post</a> to see what it could do (and <a href="/post/2023/03/15/my-adventure-with-chatgpt-a-developers-perspective/">a follow-up using the same prompts</a> when GPT-4 came out). Those posts were a bit lazy. This is going to be a more serious exploration, written manually (but reviewed by ChatGPT 😉).</p>

<p>I wanted to write a full app with ChatGPT, just to see how far I could take it. I was still using Foobal, the soccer match predictor I use to play a pool with my relatives, which <a href="/post/2012/09/11/foobal-predicting-soccer-matches-with-scala-and-drools/">I’ve</a> <a href="/post/2012/12/31/foobal-end-of-year-update/">also</a> <a href="/post/2013/08/05/foobal-a-new-hope/">blogged</a> <a href="/post/2014/08/04/on-profiling/">about</a> <a href="/post/2015/06/11/foobal-moneyball/">before</a>. I never really maintained that app, while the ecosystem had progressed, so I have a hard time even building the thing these days. Also, it only has a CLI, and I want a web app. Time for an upgrade!</p>

<h2 id="getting-started">Getting Started</h2>

<p>I decided on the following tech stack:</p>

<ul>
  <li>Go for the backend. I dislike Go, mainly because of how it forces you to mix error handling with business logic, instead of allowing you to separate the concerns. But I wanted something that produces native binaries, and it seemed easier to troubleshoot than Rust.</li>
  <li>Svelte for the frontend. It seemed like a nice, minimalistic framework.</li>
  <li>Postgres for the database. It’s free, it’s reliable, it provides everything I need.</li>
  <li>Fly.io for deployment. Infra is a weak point for me, and it promises to take that away for you, while still having a free tier. And it supports Postgres.</li>
</ul>

<p>The plan was to have ChatGPT write everything, and not write any code myself. For the most part, that worked fine. As ChatGPT conversations grew, though, it tended to start hallucinating, assuming that certain bits of code were written a certain way when they weren’t; and as the program grew, it became harder for ChatGPT to maintain everything in its context. So I sometimes had to start new conversations and paste in relevant bits of context, and manually glue them together. But I never had to hand-write anything big.</p>

<h2 id="backend">Backend</h2>

<p>Go turned out to be quite pleasant to work with, though I never had to write any error handling myself. I might think differently if I had had to. Also, Docker containers are absolutely tiny! I love that!</p>

<p>Scraping <a href="https://www.fcupdate.nl/voetbalcompetities/nederland/keuken-kampioen-divisie/programma-uitslagen">the match results website</a>, storing everything in the database, implementing a prediction algorithm: it all went very well.</p>

<p>Setting up infra with Fly.io was a breeze, too. It’s well documented, and ChatGPT has obviously been trained with that documentation as well. That was a real confidence boost for me.</p>

<h2 id="frontend">Frontend</h2>

<p>The frontend did not go so well at first. Setting up Svelte went fine, but when I started to do authentication using Google, things started going badly. Apparently Google had changed its API for that after ChatGPT’s training cutoff and I could not get it to work with Svelte. I decided to throw out Svelte and go with raw HTML and JavaScript instead, and for a while that seemed to work, but after some more testing it turned out that Google auth still didn’t work well, so I threw that out as well and opted for <a href="https://en.wikipedia.org/wiki/Basic_access_authentication">Basic authentication</a>. It’s not as secure, but at least it works, and it’s simple. I’ll definitely have to revisit this later.</p>

<p>Another thing to revisit, is Svelte. I realize now that Svelte’s authentication documentation wasn’t the problem; it was Google’s. While my app only has 2 pages (one for login and one for the predictions themselves), I already encountered some scaling issues where I want to share bits of code but can’t really do so. So at least the experience has taught me the value of JavaScript frameworks, and I’d like to try again with Svelte, as it seems to be the most minimalistic one. Something like Angular is way too heavy for my two screens.</p>

<p>By the way, when I finally had a working frontend, I asked ChatGPT to “make it pretty”. You can see the result below. There definitely still a place for designers in the new, AI-enhanced world!</p>

<p><img src="/images/2023-05-09-using-chatgpt-to-write-a-complete-app/pretty.png" alt="A not-very-pretty user interface" /></p>

<h2 id="dont-trust-chatgpt">Don’t trust ChatGPT</h2>

<p>The most important thing I’ve learned going this project, is never to trust any code that ChatGPT produces. That feels ironic, when building a complete app with it, and certainly during the initial hype of GPT-4 I didn’t appreciate that enough. But as I mentioned before, ChatGPT often hallucinates code that simply doesn’t work. Often it’s easy enough to notice, for example when it produces something that simply doesn’t compile with the existing codebase. But sometimes the bugs are more subtle, for example when I had it generate authentication: it looked right, I was able to log in to my app, so I didn’t properly test it and continued with other parts of the app, until I noticed that it didn’t actually check credentials and would log me in with incorrect credentials too. Oops. That was when I had to redo the whole thing using Basic authentication.</p>

<p>That also means I should probably have ChatGPT generate me some unit tests, because right now, I have none 🙊</p>

<p>Another thing I found, was that debugging an app written by ChatGPT is kind of hard, simply because I didn’t write any of the code myself and sometimes I simply didn’t know how it worked 😅</p>

<p>And finally, a lot of refactoring is also needed. ChatGPT doesn’t always generate the simplest or cleanest code. Especially when you have to start a new conversation and it doesn’t know all of the context anymore, because then you have to glue bits together that don’t always fit perfectly.</p>

<p>Of course, these things apply for manually written code as well, but when using AI it’s easy to forget. After all, what could go wrong with code that’s generated by a machine? If there’s a problem with it, the machine can fix it, right? Well, no, because a fix generated by ChatGPT might change existing behavior, so you still need tests. Code quality is still important, at least with the current generation of AI tools.</p>

<h2 id="conclusion">Conclusion</h2>

<p>All in all, it was a pleasant experience, and I was surprised at how fast I got something in production. I definitely feel a lot more productive using ChatGPT than I do without. I’ve since used ChatGPT for other projects too, that I probably wouldn’t even have started otherwise, such as <a href="https://extensions.gnome.org/extension/5696/one-window-wonderland">a GNOME extension</a> (I have another one planned). It has saved me from crawling Linux man pages, because it will just generate the right command line for me based on a badly specified prompt. And it’s absolutely great at critiquing and improving texts, such as this one.</p>

<p>I’m happy with my tech stack. Go was nicer than I expected; Postgres has been reliable as always; Fly.io was an absolute delight! And I really need to give Svelte another chance, because I didn’t give it enough of one.</p>

<p>ChatGPT itself was impressive, even if you always have to double-check what it gives you. It’s a great enabler for doing things you aren’t familiar with yet.</p>

<p>And I’ve successfully been doing soccer predictions using balGPT for a few weeks now. It’s not going to be giving better predictions than Foobal was, because in the end they both use the same algorithms, but at least I’m now able to do it from my phone, because of my shiny new web interface.</p>

<p>All the code is on <a href="https://github.com/jqno/balGPT">GitHub</a> if you’re interested, and I’ve stored <a href="https://github.com/jqno/balGPT/tree/main/prompts">all the prompts</a> in the GitHub repo, so you can check those out for yourself as well.</p>

<h2 id="ps">PS</h2>

<p>When I asked ChatGPT to fix the capitalizations in my final draft, it hallucinated this second conclusion for me. What do you think? Should I have kept it? 😉</p>

<blockquote>
  <p>In conclusion, while ChatGPT has its limitations, it can still be an incredibly valuable tool in speeding up development, exploring unfamiliar technologies, and even getting creative input. With a critical eye and proper testing, it’s possible to build a complete app with AI assistance. This experience has shown me the potential of AI in the world of software development and has inspired me to continue exploring and pushing the boundaries of what AI tools like ChatGPT can achieve.</p>

  <p>As the field of AI continues to develop, it will be exciting to see how these tools evolve, and how they can be further integrated into our workflows to make us even more productive and efficient. The future of AI-assisted development is bright, and I look forward to the advancements and discoveries yet to come.</p>
</blockquote>]]></content><author><name>Jan Ouwens</name></author><category term="foobal" /><category term="balgpt" /><category term="chatgpt" /><category term="ai" /><category term="software-development" /><summary type="html"><![CDATA[In which I share my experiences building an app with ChatGPT so I can start annoying my uncle again.]]></summary></entry><entry><title type="html">My Adventure with ChatGPT: A Developer’s Perspective</title><link href="https://jqno.nl/post/2023/03/15/my-adventure-with-chatgpt-a-developers-perspective/" rel="alternate" type="text/html" title="My Adventure with ChatGPT: A Developer’s Perspective" /><published>2023-03-15T00:00:00+00:00</published><updated>2023-03-15T00:00:00+00:00</updated><id>https://jqno.nl/post/2023/03/15/my-adventure-with-chatgpt-a-developers-perspective</id><content type="html" xml:base="https://jqno.nl/post/2023/03/15/my-adventure-with-chatgpt-a-developers-perspective/"><![CDATA[<p><em>In a <a href="/post/2023/01/25/my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/">previous blog post</a>, I explored the capabilities of GPT-3.5 using a series of prompts and outlines. Curious to see if the newer GPT-4 could outshine its predecessor, I decided to put it to the test using the same set of prompts and outlines. Join me as I compare my experiences and evaluate whether GPT-4 has improved upon the strengths and weaknesses of GPT-3.5 in the ever-evolving world of AI.</em></p>

<h2 id="hesitant-beginnings-worrying-about-ai-and-turning-42">Hesitant Beginnings: Worrying about AI and Turning 42</h2>

<p><img src="/images/2023-01-25-my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/developer.png" alt="A picture of a developer sitting behind a screen, generated by DALL-E" />
<em>Prompt: An image of a developer working on a computer with ChatGPT open in the background</em></p>

<p>As a developer, I recently decided to try out ChatGPT, but I must admit, I was initially hesitant. I was worried about resource consumption and the potential impact on climate change. I couldn’t help but think of Douglas Adams, who famously said:</p>

<blockquote>
  <p>Anything that is in the world when you’re born is normal and ordinary and is just a natural part of the way the world works. Anything that’s invented between when you’re fifteen and thirty-five is new and exciting and revolutionary and you can probably get a career in it. Anything invented after you’re thirty-five is against the natural order of things.</p>
</blockquote>

<p><img src="/images/2023-01-25-my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/adams.png" alt="A picture of Douglas Adams saying something unintelligble, generated by DALL-E" />
<em>Prompt: An image of Douglas Adams with a quote bubble saying “Anything invented after you’re thirty-five is against the natural order of things.”</em></p>

<p>I turn 42 next month, and I don’t want to be one of those “35 people”! So, I took the plunge and decided to give ChatGPT a shot.</p>

<h2 id="first-impressions-a-promising-start">First Impressions: A Promising Start</h2>

<p>My initial experience with ChatGPT was pretty impressive. On my first attempt, I asked it to help me translate PL/SQL to Java. I provided an example, and to my surprise, it returned the rest of the translation without any errors. Beginners luck, perhaps?</p>

<p><img src="/images/2023-01-25-my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/translation.png" alt="A picture of something vaguely technical-looking, generated by DALL-E" />
<em>Prompt: A before and after image of a code snippet with PLSQL on one side and Java on the other</em></p>

<p>For my second attempt, I threw it a curveball: I needed help with a niche project involving a GNOME extension. The GNOME documentation can be a bit like navigating through a maze, but ChatGPT managed to provide me with a full boilerplate to get started!</p>

<p>On top of that, it even explained the difference between yyyy and uuuu in Java date patterns, which I found quite informative. It’s like asking your friend for advice, except that friend is an AI that doesn’t need to sleep.</p>

<h2 id="hitting-some-bumps-when-things-go-south">Hitting Some Bumps: When Things Go South</h2>

<p>However, it wasn’t all sunshine and rainbows. ChatGPT did provide some answers that were just plain wrong:</p>

<ol>
  <li>It explained how to write an equals method in Java with inheritance and adding state, but it got it all mixed up.</li>
  <li>It tried to help me solve a certain, badly documented issue with my GNOME extension but ended up giving me a dead end.</li>
</ol>

<p><img src="/images/2023-01-25-my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/gnome.png" alt="A picture of something reminiscent of a Linux desktop, generated by DALL-E" />
<em>Prompt: An image of a GNOME desktop with a highlighted extension</em></p>

<p>When I asked it repeatedly for project name suggestions, it gave me duplicates. And when I asked how to test the equals method in Java, it didn’t mention EqualsVerifier. So, like a bowl of mixed nuts, you have to pick out the good parts and leave the rest behind.</p>

<p>The lesson here? Always double-check whatever ChatGPT provides you.</p>

<h2 id="silver-linings-apologies-and-rubber-ducks">Silver Linings: Apologies and Rubber Ducks</h2>

<p>But there were some silver linings too. For one, ChatGPT apologizes a lot (sometimes, more than a Canadian!). After a particularly frustrating interaction, it even wrote a solid Stack Overflow post for me.</p>

<p>Even when it provided incorrect answers, it was still a useful rubber duck. You know, like explaining your problem to an inanimate object and suddenly realizing the solution yourself!</p>

<p><img src="/images/2023-01-25-my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/rubberduck.png" alt="A picture of a creepy rubber duck, generated by DALL-E" />
<em>Prompt: An image of a “rubber duck” toy with the caption “even with incorrect answers, it can still be a useful ‘rubber duck’ to help you think through a problem”</em></p>

<h2 id="final-thoughts-a-useful-tool-but-not-a-replacement">Final Thoughts: A Useful Tool, but Not a Replacement</h2>

<p>Overall, I found ChatGPT to be a useful tool, much like Stack Overflow. But, I won’t be making it my full-time job anytime soon. It’s more like an extra set of hands on a project, rather than a one-stop solution.</p>

<p>By the way, this blog post was written with the help of ChatGPT! I had to tweak the outline quite a bit to get satisfactory results, but it’s always nice to have a little AI assistance in my corner. Like a sous-chef in a kitchen, it can’t replace the head chef, but it can make life a little bit easier.</p>

<p>And who knows? Maybe someday, ChatGPT will be the ultimate developer sidekick, serving up not just code snippets but also expert advice, witty jokes, and maybe even a fresh cup of coffee. For now, though, it’s a good companion to have on hand when navigating the ever-changing world of technology.</p>

<p>So, as I embrace my 42nd birthday, I’m grateful for ChatGPT and the chance to stay ahead in the tech game. After all, if a hitchhiker can make it through the galaxy with just a towel, I can certainly tackle my next project with the help of an AI assistant!</p>

<h2 id="addendum-list-of-prompts">Addendum: List of Prompts</h2>

<ul>
  <li>Write a blog post in markdown format from the following outline. Flesh it out a bit. Include an analogy and a few jokes in the text.</li>
  <li>Continue.</li>
  <li>Give me a one-sentence summary that starts with the phrase “In which I”.</li>
  <li>Write a short introductory paragraph that mentions I used the same prompts and outlines in an earlier piece about gpt 3.5 to see if gpt 4 did better.</li>
  <li>Give me a <a href="https://mastodon.online/@jqno/110026908508073155">clickbaity tweet</a> to announce this follow-up post.</li>
  <li>Write an addendum containing a list of all the prompts I gave you.</li>
</ul>

<h2 id="original-outline">Original outline</h2>

<p>The outline is identical to the one in the <a href="/post/2023/01/25/my-experience-with-chatgpt-as-a-developer-impressed-but-with-caveats/">previous post</a>.</p>

<ul>
  <li>Tried ChatGPT as a developer
    <ul>
      <li>initially hesitant</li>
      <li>worried about resource consumption and climate change</li>
      <li>Douglas Adams
        <ul>
          <li>mention his quote about technology, the things that are in the world at age 15 and 35, and the natural order of things</li>
          <li>i turn 42 next month but don’t want to be a 35 person</li>
        </ul>
      </li>
      <li>gave it a shot</li>
    </ul>
  </li>
  <li>Initially impressed
    <ul>
      <li>First attempt:
        <ul>
          <li>yranslating PLSQL to Java</li>
          <li>gave an example and got the rest, without errors</li>
        </ul>
      </li>
      <li>Second attempt
        <ul>
          <li>niche project: GNOME extension</li>
          <li>GNOME docs hard to navigate</li>
          <li>gave me full boilerplate</li>
        </ul>
      </li>
      <li>Also
        <ul>
          <li>was able to explain the difference between yyyy and uuuu in Java date patterns</li>
          <li>explain it</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Found issues
    <ul>
      <li>some answers to my questions were plain wrong:
        <ul>
          <li>explanation how to write equals method in Java with inheritance and adding state</li>
          <li>solving a certain, badly documented thing in GNOME extension</li>
        </ul>
      </li>
      <li>when repeatedly asked for names for a project, gave duplicates</li>
      <li>when asked how to test equals in java, didn’t mention EqualsVerifier :(</li>
      <li>always double-check whatever it gives you</li>
    </ul>
  </li>
  <li>Silver lining
    <ul>
      <li>it apologizes a lot</li>
      <li>it wrote a good StackOverflow post after frustrating interaction</li>
      <li>even with incorrect answers, is sometimes useful rubber duck</li>
    </ul>
  </li>
  <li>I like it
    <ul>
      <li>useful tool like stack overflow</li>
      <li>but won’t make it my job</li>
    </ul>
  </li>
  <li>mention that this post was written by chatgpt
    <ul>
      <li>but had to tweak the outline a lot to produce good results</li>
    </ul>
  </li>
</ul>]]></content><author><name>Jan Ouwens</name></author><category term="chatgpt" /><category term="ai" /><category term="software-development" /><summary type="html"><![CDATA[In which I explore the ups and downs of using ChatGPT as a developer, navigating the maze of GNOME documentation, learning from its mistakes, and appreciating its value as a useful AI sidekick.]]></summary></entry></feed>