<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>3fx.ch</title><link>https://3fx.ch/blog/</link><description>Recent Blog posts on 3fx.ch</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 14 Feb 2022 12:00:00 +0100</lastBuildDate><atom:link href="https://3fx.ch/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Why Wordle is so successful</title><link>https://3fx.ch/blog/2022/02/14/wordle/</link><pubDate>Mon, 14 Feb 2022 12:00:00 +0100</pubDate><guid>https://3fx.ch/blog/2022/02/14/wordle/</guid><description>&lt;p>The game &lt;a href="https://www.nytimes.com/games/wordle/index.html">Wordle&lt;/a> exploded in
popularity recently, to the point that it was &lt;a href="https://www.bbc.com/news/business-60208463">bought by the New York
Times&lt;/a> because of its popularity.
But why is Wordle such a resounding success? I will take a swing at an
explanation in this post.&lt;/p>
&lt;h1 id="simple-rules-with-a-clever-twist">Simple rules with a clever twist&lt;/h1>
&lt;p>Wordle is a modified variant of the board game
&lt;a href="https://en.wikipedia.org/wiki/Mastermind_(board_game)">Mastermind&lt;/a>. In
Mastermind, in which one player constructs a &lt;em>code word&lt;/em> consisting of four
colored pegs, while the other player&amp;rsquo;s objective is to determine said code word
in a limited number of tries. Any combination of pegs is admissible, making it
mostly a game of deduction&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>The cleverness of Wordle is the fact that, instead of colored pegs, the game
uses the letters A through Z and a restricted set of admissible code words&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.
Instead of requiring the player to reason about code words in abstract terms,
five-letter English words are much more familiar to the player. This gives
Wordle a tangibility that Mastermind cannot quite achieve.&lt;/p>
&lt;h1 id="perfect-game-length">Perfect game length&lt;/h1>
&lt;p>Another important factor to Wordle&amp;rsquo;s success lies in its format: a single Wordle
can be played per day, and everybody guesses the same code word. Playing a round
of Wordle takes a few minutes at most, and can be played anywhere: in bed after
waking up, on the way to work, during a work break or in the evening to relax.
There is no endless optimization or retrying for a better score. Once a game is
done it is done, and the next game can only be played the next morning. This
prevents people from burning out on Wordle after playing too much, and leaves us
excited for the next day&amp;rsquo;s Wordle.&lt;/p>
&lt;h1 id="casual-and-competitive-with-just-the-right-amount-of-luck">Casual and competitive, with just the right amount of luck&lt;/h1>
&lt;p>Wordle has two competitive aspects: correctly guessing the word, and doing it in
fewer guesses than your friends and family. I wager most English-speaking people
can guess almost every code word in the alotted six tries, but it isn&amp;rsquo;t a
trivial task: finding a good guess feels very rewarding. With sufficient skill
the game&amp;rsquo;s code word choice is insignificant, thus ensuring that good players
don&amp;rsquo;t feel punished by unfair word choices&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Competing with other players is very simple: the player to guess the word in
fewer tries wins, and if both players require the same amount of tries, a draw
is declared. This competition is interesting in the sense that it is possible to
avoid bad scores with sufficient skill, but a player&amp;rsquo;s initial guess plays an
important role in a day&amp;rsquo;s score: there is a fair amount of random chance and/or
luck involved in getting very good scores. Thus, it is impossible for a player
to consistently dominate the game. I would describe Wordle as similar to Mario
Kart in this instance: in Mario Kart, skill is an important factor to not lose,
but in the end it comes down to luck and chance who places first.&lt;/p>
&lt;h1 id="the-perfect-package">The perfect package&lt;/h1>
&lt;p>Wordle has it all: a solid foundation, the perfect length to keep up engagement
over a longer time frame, both for casual and more competitive gamers. Coupled
with a very clean user interface and a satisfying guess revelation animation it
is not surprising that it has become as successful as it has. It is an instant
classic, on par with other puzzle-type games such as crosswords and sudokus.&lt;/p>
&lt;p>I&amp;rsquo;ve spent some time thinking about which other games I can think of that fit in
the above category, but I could not come up with a comparable game. There&amp;rsquo;s
always something odd to it such that doesn&amp;rsquo;t quite measure up to Wordle. If you
have an idea, &lt;a href="https://mastodon.3fx.ch/web/statuses/107797302731067358">let me know&lt;/a>!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>It has been shown that the classic Mastermind &lt;a href="https://www.cs.uni.edu/~wallingf/teaching/cs3530/resources/knuth-mastermind.pdf">can always be solved in
five
tries&lt;/a>.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>The full list of admissible guesses and possible code words is directly
exposed in the &lt;a href="https://www.nytimes.com/games/wordle/main.bd4cb59c.js">game&amp;rsquo;s JavaScript
code&lt;/a>.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>The only way to ensure that every word is fair is by hand-picking the
entire word list.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>CSS: Avoid horizontal scrolling</title><link>https://3fx.ch/blog/2021/07/25/css-avoid-horizontal-scrolling/</link><pubDate>Sun, 25 Jul 2021 10:30:40 +0200</pubDate><guid>https://3fx.ch/blog/2021/07/25/css-avoid-horizontal-scrolling/</guid><description>&lt;blockquote>
&lt;p>This piece is a bit of a rant and a bit of advice. If you are not in the mood
to read this, there is a TL;DR at the bottom.&lt;/p>
&lt;/blockquote>
&lt;p>If you had told me last year that I&amp;rsquo;d write not one, but two blog posts about
web design, I would not have believed you. Alas here we are, and I&amp;rsquo;m going to
tell you about another pet peeve of mine and how to fix it.&lt;/p>
&lt;p>I view at least half the written content I consume via my mobile phone, which
belongs to the class of phones with a slightly smaller than average screen. In
fact, it&amp;rsquo;s width as reported by the Firefox and Chrome viewport simulators is
375 pixels wide, small but not tiny. And yet many websites I visit hinder my
mobile viewing experience because of a rather simple issue: horizontal
scrolling.&lt;/p>
&lt;p>There I am, happily reading a blog post or article from the front page of
&lt;code>$NEWSAGGREGATOR&lt;/code>. But suddenly I&amp;rsquo;m not scrolling down, but down-right and
constantly have to scroll back to the left edge in order to read full sentences.
My experience is even worse if the site for some reason has a minimum width
larger than my screen is wide.&lt;/p>
&lt;p>Reading articles on those sites &lt;em>really&lt;/em> cumbersome and more often than not I
decide that it&amp;rsquo;s not worth the hassle to continue reading and move on to the
next article. I find that quite sad, since the author invested time and effort
to produce it, and the only reason I&amp;rsquo;m not reading is because of its subpar
layouting.&lt;/p>
&lt;h1 id="two-types-of-scrolling-madness">Two types of scrolling madness&lt;/h1>
&lt;p>The smartphone browsers I have used in the past had two scrolling modes
when scrolling is possible in both horizontal and vertical directions.&lt;/p>
&lt;p>One of them is to always allow the user to scroll in both directions. This makes
thumb-scrolling a tedious task, as the natural scrolling motion is not completely
vertical, and leads to horizontal displacement&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>The other one locks a scroll direction at the start of the scroll and restricts
movement to that axis. This seems like a good idea in practice, but it does not
work well for small adjustments. A slight horizontal movement on the initial
scroll can lock the wrong axis, and there is a small cooldown time until the
lock releases.&lt;/p>
&lt;p>Finally, there&amp;rsquo;s the problem that horizontal scrolling is already used for
navigation, at least on iOS Safari: scrolling horizontally triggers history
navigation, which makes for a smooth user experience. Except when I
unintentionally trigger the movement options when navigating a horizontally
scrolling site. Most sites handle accidental navigation cleanly and return to
where I left from, but some&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> sites don&amp;rsquo;t, and that&amp;rsquo;s another source of
annoyance for me.&lt;/p>
&lt;p>My conclusion is that both scrolling behaviours suck, so it&amp;rsquo;s best to avoid them
as much as possible. So let&amp;rsquo;s fix it! Here&amp;rsquo;s a bunch of tips that hopefully help
you understand and prevent horizontal scrolling. I will concern myself with
mobile design tips, as this issue is not as prevalent on laptops and desktops.&lt;/p>
&lt;h1 id="use-viewport-scaling">Use viewport scaling&lt;/h1>
&lt;p>Some sites do not have &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag">viewport
scaling&lt;/a>
enabled. This is a browser feature which accounts for pixel density and screen
size differences between low and high resolution screens. For example, even
though my phone actually has a screen that is 750px wide, displaying the site
for a 750px wide screen causes the content to become tiny and unreadable.
Readers then have to use zoom and use horizontal scrolling to read the content.&lt;/p>
&lt;p>Using viewport scaling accounts for this discrepancy, and ensures that the page
is rendered at a sane size.&lt;/p>
&lt;h1 id="vertical-design-is-superior">Vertical design is superior&lt;/h1>
&lt;p>When designing a site, make sure that the mobile version is designed with
verticality in mind. Smartphone screens are narrow so information rarely, if
ever, fits nicely side-by-side. Instead use the infinite vertical space you have
available and stack it on top of each other.&lt;/p>
&lt;p>Sometimes it might be better to outright remove some features of the full-width
site. Is a navbar really necessary when viewing a blog post on a mobile screen?
Probably not. There should still be some navigation which allows you to go back
to the index, for example. It is important to strike the right balance of
feature and layout here, and some experimenting might be required.&lt;/p>
&lt;h1 id="be-conservative-about-minimum-width">Be conservative about minimum width&lt;/h1>
&lt;p>When I was still in high school a few years back my best friend used to have an
interesting metric for selecting his mobile phones: more durability equals better.
He was, in fact, quite clumsy&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> when handling his phone so it made a lot of
sense to him to choose a cheap and durable phone. I do not quite recall what the
actual model was, but it was an LG phone similar to the &lt;a href="https://www.lg.com/ch_de/smartphones-handys/lg-E400-optimus-l3">Optimus L3
E400&lt;/a>, with a
whopping 256MB RAM and a 320x240 display. He used it as his daily driver, as it
could do everything he needed: play music and browse the web.&lt;/p>
&lt;p>I do not know whether he was as annoyed as I am about horizontal scrolling, but
every time I work on mobile web design I think of him and his phone, and that he
and others with really narrow screens would like to enjoy my content as well.
Thus I try to design everything to be as usable as possible, even on a 320x240
screen&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>. Starting at 360px is fine for most purposes as well &amp;ndash; it is definitely
an improvement over doing nothing.&lt;/p>
&lt;h1 id="dealing-with-wide-content">Dealing with wide content&lt;/h1>
&lt;p>Paragraphs of prose have the neat property that they are easily compressed
horizontally, and still look nice on small screen sizes. Unfortunately, this is
not possible for very long words, as the default CSS behaviour is to let them
overflow to the side. The CSS property
&lt;a href="https://developer.mozilla.org/de/docs/Web/CSS/word-break">&lt;code>word-break&lt;/code>&lt;/a> can
help here: setting it to &lt;code>break-word&lt;/code> allows the browser to break very long
words. It is not a very nice way of breaking words (no hyphen is inserted
to indicate breakage), but it makes the page fit nicely without horizontal
scrolling, which is more important. You can see an example of this happening
here: Donaudampfschifffahrtsgesellschaftskapitän. Try viewing it in 240px or
360px width&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> and note how only the offending word is broken apart.&lt;/p>
&lt;p>There are two other content types where I frequently encounter overflow: images
and source code.&lt;/p>
&lt;p>Images are easily handled: make sure they are always at most 100% wide.
Sometimes the image loses detail on very small screens, but that is totally
fine. Make sure users can still access the raw image, for example by its URL.&lt;/p>
&lt;p>Source code poses a bigger problem: line wrapping for code straight up sucks, so
it has to be presented as is. Here we cannot nicely avoid horizontal scrolling,
but we can ensure that we only scroll what is necessary, and not the entire page.
There are 1001 different ways to achieve this, on this blog I &lt;a href="https://git.sr.ht/~bfiedler/website/tree/2915f7b00f1034e64c1e3f4562bfb4bc3f0df302/item/assets/style.scss#L177">set
the &lt;code>overflow-x&lt;/code> property on the &lt;code>&amp;lt;pre&amp;gt;&lt;/code>
tags&lt;/a>.&lt;/p>
&lt;p>This is the thing most blogs or articles I read get wrong: Most of the content
neatly fits into the screen width, but overflow of one code block spills over
and ruins the scrolling experience on the entire page.&lt;/p>
&lt;h1 id="test-it">Test it!&lt;/h1>
&lt;p>Finally, all theoretical discussion is useless if you do not test your design!
All major modern browsers have some responsive design testing mode which allows
you to set the viewport&amp;rsquo;s dimensions and see how your site behaves. On Firefox
and Chrome it is part of the developer tools, which can be accessed by the keyboard
shortcut &lt;code>Ctrl+Shift+I&lt;/code>.&lt;/p>
&lt;p>And if you have some &lt;del>unwilling test subjects&lt;/del> friends, ask them to check out
your site on mobile and give feedback! As an added bonus you get a wider browser
coverage this way. There are a lot of subtle compatibility issues between web
browsers, and the only way to figure them out is to test everything.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Hopefully this post explained why I think we should abolish horizontal
scrolling, and give you practical tips on how to avoid it. If you have any
questions or comments feel free to reach out to me &lt;a href="https://mastodon.3fx.ch/@bfiedler">on
Mastodon&lt;/a> or via my &lt;a href="https://lists.sr.ht/~bfiedler/public-inbox">public
inbox&lt;/a>.&lt;/p>
&lt;h1 id="tldr">TL;DR&lt;/h1>
&lt;ul>
&lt;li>Horizontal scrolling sucks&lt;/li>
&lt;li>Vertical composition is superior&lt;/li>
&lt;li>Be conservative about minimum width, support at least 360px, if not 240px&lt;/li>
&lt;li>If you need wider content then scroll locally&lt;/li>
&lt;li>Test your site using the devtools supported by all major modern browsers&lt;/li>
&lt;/ul>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>This is worse for left-handed people, as their natural thumb scrolling motion
goes down-right.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Especially the JS-heavy sites&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>You know who you are ;)&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Who knows, one day you might want to browse the web on your smart fridge.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>On iOS devices you can achieve this via the page zoom setting.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>CSS: prefers-color-scheme</title><link>https://3fx.ch/blog/2021/01/27/css-prefers-color-scheme/</link><pubDate>Wed, 27 Jan 2021 16:45:30 +0100</pubDate><guid>https://3fx.ch/blog/2021/01/27/css-prefers-color-scheme/</guid><description>&lt;p>When developing tools that interface with users in a visual sense, I believe it
is important to cater to the users&amp;rsquo; wants (and needs) as much as possible,
whilst retaining a recognizable product identity. Probably the brightest
decision in this regard is the background color: light or dark?&lt;/p>
&lt;p>Specifically regarding webpages, a variety of solutions have been developed:
JavaScript triggers, cookie-based theming, user stylesheets and time-based theming. All of these solution falls short in one important aspect: they do not
cover 100% of a user&amp;rsquo;s interaction with a site. The first two solutions have no
safe default - they may always show the user the &amp;ldquo;wrong&amp;rdquo; theme first, and
require active engagement to fix, which is a burden on the user. User
stylesheets do work on first visit, however one often has to deal with broken
sites, since they were not designed for theming. Finally, time-based theming is
silly in its own right&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;h1 id="the-solution">The solution&lt;/h1>
&lt;p>In 2017, the &lt;a href="https://www.w3.org/TR/mediaqueries-5/">CSS Media Queries Level 5&lt;/a>
specification was released, and it proposes the best solution to date: a media
query named &lt;code>prefers-color-scheme&lt;/code>. It is set by your browser, and can be set to
either &lt;code>light&lt;/code> or &lt;code>dark&lt;/code> - more values may be supported in the future. Instead
of requiring a user to interact with &lt;em>each&lt;/em> site they visit, the decision is
only made once at the browser, and can be applied to every site, even on the
first visit! And it is supported by &lt;a href="https://caniuse.com/prefers-color-scheme">all major desktop and mobile
browsers&lt;/a>, simply marvelous.&lt;/p>
&lt;p>Using this feature is as simple as adding a media query to your styling. Using
&lt;a href="https://www.w3.org/TR/css-variables/">CSS custom properties&lt;/a> (&amp;ldquo;variables&amp;rdquo;), all
you need to do is define the appropriate colors and you&amp;rsquo;re good to go.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">prefers-color-scheme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">light&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">//&lt;/span> &lt;span class="nt">light&lt;/span> &lt;span class="nt">styling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">//&lt;/span> &lt;span class="nt">the&lt;/span> &lt;span class="nt">default&lt;/span> &lt;span class="nt">if&lt;/span> &lt;span class="nt">no&lt;/span> &lt;span class="nt">preference&lt;/span> &lt;span class="nt">set&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">prefers-color-scheme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">dark&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">//&lt;/span> &lt;span class="nt">dark&lt;/span> &lt;span class="nt">styling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>prefers-color-scheme&lt;/code> still has one drawback: correct page rendering must
be delayed until CSS is downloaded and parsed, and might cause flickering on
slow connections, when the browser defaults to a white background but the user
prefers dark mode for example. Using the &lt;code>color-scheme&lt;/code> meta tag, the webpage
can immediately signal which themes it supports, and the browser can react by
styling the background dark immediately, before applying CSS, which prevents
the aforementioned flickering. The first named theme is the author&amp;rsquo;s preferred
default, which the browser can respect if the user has not given an explicit
preference.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- supports light and dark mode, defaults to light mode--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;color-scheme&amp;#34;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;light dark&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="trying-it-out">Trying it out&lt;/h1>
&lt;p>This website fully supports theme selection based &lt;code>prefers-color-scheme&lt;/code>, try it
out! It even inverts images&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> to perfectly match your preferred style, a
feature I&amp;rsquo;m very proud of, even if it is virtually invisible (since almost
nobody looks at both styles). See the &lt;a href="https://git.sr.ht/~bfiedler/website">website&amp;rsquo;s
source&lt;/a> to see the actual SCSS source
files.&lt;/p>
&lt;p>On desktop browsers you can test it using the developer tools
(&lt;a href="https://stackoverflow.com/a/60481298/">Firefox&lt;/a>,
&lt;a href="https://stackoverflow.com/a/59223868/">Chrome&lt;/a>,
&lt;a href="https://webkit.org/blog/8840/dark-mode-support-in-webkit/">Safari&lt;/a>). On mobile,
modern smartphones (both iOS and Android) set your preferred color scheme based
on the system theme, so changing that should also change the appearance of this
site.&lt;/p>
&lt;p>Sadly, many of the sites I interact with daily do not support
&lt;code>prefers-color-scheme&lt;/code>. Especially people who rarely do frontend work (such as
myself) may not know of this feature, and I hope to raise awareness for
user-friendly theme support.&lt;/p>
&lt;p>If you have a personal blog, product or company website or any other side,
consider adding support for &lt;code>prefers-color-scheme&lt;/code> to your theme. Even though it
has no impact on the majority of people, we ultimately want design our products
to be as user-friendly as possible. Right?&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Bonus question: server time or client time? One is inaccurate for large parts
of your visitors, the other one requires JavaScript.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>when it makes sense, so photos for example are left as-is&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Bundlewrap: First Impressions</title><link>https://3fx.ch/blog/2021/01/25/bundlewrap-first-impressions/</link><pubDate>Mon, 25 Jan 2021 00:00:00 +0100</pubDate><guid>https://3fx.ch/blog/2021/01/25/bundlewrap-first-impressions/</guid><description>&lt;p>I have spent quite some time with configuration management for my home infra
setup, and I have recently come across a new tool that I&amp;rsquo;m excited to share with
you. It&amp;rsquo;s called &lt;a href="https://bundlewrap.org">Bundlewrap&lt;/a>, and it is a flexible,
small-scale&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> configuration management solution. Bundlewrap is written in
Python 3&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, and is unique in the sense that the infrastructure configuration
is also written in Python. I was first introduced to Bundlewrap by
&lt;a href="https://chaos.social/@kunsi">@kunsi&lt;/a> in December 2020, who gave a brief
presentation at a cozy conference.&lt;/p>
&lt;p>My current setup uses &lt;a href="https://ansible.com">Ansible&lt;/a> and the oldest parts are
about two years old, so it&amp;rsquo;s mostly written for Ansible 2.4 and later. Most
comparisons done are against Ansible.&lt;/p>
&lt;h1 id="brief-overview">Brief overview&lt;/h1>
&lt;p>Bundlewrap manages nodes via so-called &lt;em>bundles&lt;/em>, which roughly correspond to
Ansible roles. A bundle describes a desired state on the target nodes,
comprising of one or more &lt;em>items&lt;/em>, which would be tasks in Ansible.&lt;/p>
&lt;p>Nodes are defined in &lt;code>nodes.py&lt;/code>. Each node has a separate dictionary
called &lt;code>metadata&lt;/code> associated with it, which holds all the node&amp;rsquo;s
configuration data and can be read and written by the bundles. Machines can be
grouped and group metadata can be applied to all members by specifying it in
&lt;code>groups.py&lt;/code>.&lt;/p>
&lt;p>Let&amp;rsquo;s look at an example: the bundle &lt;code>ssh-server&lt;/code> should install and configure
an SSH server on our node &lt;code>test&lt;/code>. To do so we have to specify the &lt;em>items&lt;/em> we
want to use, here &lt;a href="https://docs.bundlewrap.org/items/pkg_apt/">&lt;code>pkg_apt&lt;/code>&lt;/a>,
&lt;a href="https://docs.bundlewrap.org/items/file/">&lt;code>file&lt;/code>&lt;/a> and
&lt;a href="https://docs.bundlewrap.org/items/svc_systemd/">&lt;code>svc_systemd&lt;/code>&lt;/a>. An entire
repository could look something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── bundles &lt;span class="c1"># All bundles are in the directory bundles/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   └── ssh-server &lt;span class="c1"># Our ssh-server bundle&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   ├── files &lt;span class="c1"># File templates for the ssh-server bundle&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   │   └── sshd_config &lt;span class="c1"># SSH server configuration template (omitted)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   ├── items.py &lt;span class="c1"># Item definition file, Ansible: tasks/main.yml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   └── metadata.py &lt;span class="c1"># Metadata definition file, Ansible: defaults/main.yml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── groups.py &lt;span class="c1"># Group configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── nodes.py &lt;span class="c1"># Node configuration&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-py" data-lang="py">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># nodes.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">nodes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;test&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;hostname&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;198.51.100.1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;bundles&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;ssh-server&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;metadata&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;nodevar&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;string&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># groups.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">groups&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;group&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;members&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;test&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;metadata&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;groupvar&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># items.py for bundle ssh-server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">pkg_apt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;openssh-server&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;installed&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">files&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;/etc/ssh/sshd_config&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;source&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;sshd_config&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;triggers&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;svc_systemd:sshd:restart&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">svc_systemd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;sshd&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;enabled&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;running&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;needs&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s1">&amp;#39;pkg_apt:openssh-server&amp;#39;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># metadata.py for bundle ssh-server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">defaults&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;bundlevar&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;abc&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;def&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Configuration is done via Python dictionaries, and since the files are literal
Python code, you can embed arbitrary logic in these files. Bundlewrap is
&lt;a href="https://docs.bundlewrap.org">well-documented&lt;/a>, and I encourage you to read the
docs if you want to figure out what an item does.&lt;/p>
&lt;p>Now that we have seen the general structure of bundles, let us come to the
feature comparison.&lt;/p>
&lt;h1 id="the-good">The Good&lt;/h1>
&lt;h2 id="agentless-push-based-no-python-required-on-guest">Agentless, push-based, no Python required on guest&lt;/h2>
&lt;p>Just like Ansible, Bundlewrap is both agentless and push-based. Managed nodes
are accessed via SSH, which is my preferred way. Contrary to Ansible though,
Bundlewrap does not require Python to be installed on the managed hosts, and
instead relies on &lt;a href="https://docs.bundlewrap.org/guide/installation/#requirements-for-managed-systems">common
tools&lt;/a>.&lt;/p>
&lt;p>Privilege escalation must work noninteractively. Since I anyway dislike entering
passwords, this not a problem for me. The exact privilege escalation method used
is configurable, so BSDs for example have the option to use &lt;code>doas&lt;/code> instead of
&lt;code>sudo&lt;/code> (the default).&lt;/p>
&lt;p>Bundlewrap does &lt;em>not&lt;/em> do SSH multiplexing by default, but it is possible to pass
arbitrary arguments to the underlying ssh invocation via an environment
variable.&lt;/p>
&lt;h2 id="automatic-metadata-merging-metadata-generation">Automatic metadata merging, metadata generation&lt;/h2>
&lt;p>In the example above you might have noticed that we have defined metadata for
both the node and the group it belongs to. Generally in Bundlewrap,
non-collection metadata follows a strict hierarchy: node metadata overrides
group metadata overrides bundle metadata. Collections are merged recursively,
which is one of the best features Bundlewrap has. We can instruct Bundlewrap to
display the metadata associated with &lt;code>test&lt;/code>. The output is color-coded&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> according
to where the key comes from (group/node/bundle), which is very helpful.&lt;/p>
&lt;pre tabindex="0">&lt;code>% bw metadata test
{
&amp;#34;bundlevar&amp;#34;: [ # Colored blue = from bundle
&amp;#34;abc&amp;#34;,
&amp;#34;def&amp;#34;
],
&amp;#34;groupvar&amp;#34;: 5, # Colored yellow = from group
&amp;#34;nodevar&amp;#34;: &amp;#34;string&amp;#34; # Colored red = from node
}
&lt;/code>&lt;/pre>&lt;p>This is one of the features I miss most from Ansible. I have a ton of roles
which would like to have their variables merged. One example is my Prometheus
setup: My monitoring server has to know about every exporter that a node has
installed in order to scrape all of them. Ideally I&amp;rsquo;d just have a list for each
node which has &lt;code>(exporter, port)&lt;/code> pairs and each exporter role appends a pair to
this node, thus allowing the monitioring role to work independently of the
available exporters. However, since Ansible does not allow appending to an
existing variable, I am stuck hardcoding every possible exporter into the main
prometheus role.&lt;/p>
&lt;p>Bundlewrap also allows for generating new metadata from existing metadata, using
a concept called &lt;em>metadata reactors&lt;/em>. These are defined at the bundle level and
are extremely powerful. You can, for example, ensure that every virtual host
automatically also gets issued a letsencrypt certificate, while still separating
the webhost and letsencrypt bundles.&lt;/p>
&lt;h2 id="secret-derivation">Secret derivation&lt;/h2>
&lt;p>Ansible has secrets, which allow you to store encrypted data and decrypt it with
a static key. Bundlewrap can also do this, but additionally it allows you to
generate secrets dynamically, which you can extract on demand. This is
especially useful for automatic password generation for user accounts or when
connecting a service to a DB user account: In both cases I don&amp;rsquo;t really care
&lt;em>what&lt;/em> the secret is, only that 1. it is a &lt;em>secret&lt;/em> known only to the correct
parties and 2. I can recover it if needed. Additionally, the secrets
can easily be rotated by replacing the key used for secret derivation! Of
course, now anyone in possession of the Bundlewrap master secret can derive all
your passwords, so be sure to secure it well.&lt;/p>
&lt;h2 id="offline-testing">Offline testing&lt;/h2>
&lt;p>This one&amp;rsquo;s huge: Bundlewrap supports sensible offline testing. Bundlewrap tests
involve assembling all metadata for all nodes, checking that all items are
well-formed, all templates instantiate without errors, and so on. This is a
feature I sorely miss from Ansible. While Ansible has the &lt;code>--check&lt;/code> parameter, it
still simulates each step by connecting to the target node, which is &lt;em>really&lt;/em>
slow compared to local execution. Plus, you can run Bundlewrap tests as part of
your CI pipeline (even works for secrets without the decryption/generation
keys!).&lt;/p>
&lt;pre tabindex="0">&lt;code>% bw test
✓ No reactors violated their declared keys
✓ group has no subgroup loops
✓ test has no metadata conflicts
✓ test ssh-server file:/etc/ssh/sshd_config
✓ test ssh-server pkg_apt:openssh-server
✓ test ssh-server svc_systemd:sshd
✓ test ssh-server svc_systemd:sshd:restart
✓ test ssh-server svc_systemd:sshd:reload
&lt;/code>&lt;/pre>&lt;h2 id="small-core">Small core&lt;/h2>
&lt;p>Bundlewrap has an extremely small &amp;ldquo;standard library&amp;rdquo; of items, and prides itself
on staying that way. Personally, I value scope-restriction a lot in projects, so
this is a good thing. On the other hand it means that, more often than not, you
have to write the code for new items yourself, e.g. support for a new package
manager. Fortunately, the code is quite accessible, and the methods you need to
implement are well-documented.&lt;/p>
&lt;h1 id="the-neutral">The Neutral&lt;/h1>
&lt;h2 id="python-dicts">Python dicts&lt;/h2>
&lt;p>Python dictionaries look much more like JSON than YAML, however in my opinion
this does not impact readability. Writing Python dicts is slightly more pleasant
than raw JSON, since it allows the use of single quotes for string
identifiers&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>. Formatting is taken care of by any linter, which is nicer
than YAML, where indentation cannot be automatically inferred. Of course, this
is true for raw Python code as well.&lt;/p>
&lt;h2 id="statistics-and-dependency-graphs">Statistics and dependency graphs&lt;/h2>
&lt;p>This is undoubtedly a cool feature: Bundlewrap can output graphs (in graphviz
format) visualizing the item/bundle dependencies on a node, or your
repositories&amp;rsquo; group relationships. And it also keeps track of statistics such as
the number of items, nodes, groups, bundles and so on. These features don&amp;rsquo;t have
a downside, however I also haven&amp;rsquo;t (yet) discovered clear upsides other than &amp;ldquo;ooh,
shiny&amp;rdquo;.&lt;/p>
&lt;figure class="invertable resizable">&lt;img src="https://3fx.ch/blog/img/bundlewrap-plot.svg"
alt="Output of bw plot test"/>&lt;figcaption>
&lt;p>Output of bw plot test&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;h1 id="the-bad">The Bad&lt;/h1>
&lt;h2 id="python">Python&lt;/h2>
&lt;p>I really, really, really dislike Python. Mainly because it is interpreted
and dynamically typed, which means that most errors will occur at runtime when
it is too late to fix stuff. Working with Bundlewrap snippets is even worse,
since some variables are passed automagically, which confuses my poor language
server (and &lt;code>mypy&lt;/code> as well), so any possibility of static type checking is
chucked right out of the window.&lt;/p>
&lt;p>Bundlewraps excellent local testing feature alleviates this issue somewhat.&lt;/p>
&lt;h2 id="turing-complete-config-language">Turing-complete config language&lt;/h2>
&lt;p>Having all the flexibility and power of Python also means having more footguns
available to shoot yourself with. Bundlewrap relies much more on the user for
constraining the bundle complexity. Personally, I think that for small
infrastructures (such as what I run at home) this is fine, however I would be
wary of this power for bigger deployments.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>I&amp;rsquo;ve spent the last month thinking about and testing configuration management
systems, and believe I have found a hidden gem in Bundlewrap. The only other
notable mention I tried was &lt;a href="https://cdi.st">cdist&lt;/a>, however it has its fair
share of oddities, most notably being a 100% sh-based solution. Of course, this
doesn&amp;rsquo;t mean that it&amp;rsquo;s not good for you! Go check it out if that premise excites
you.&lt;/p>
&lt;p>I hope you got a brief overview of Bundlewrap and it&amp;rsquo;s features. Personally, I
think it&amp;rsquo;s a better solution for my usecase than Ansible, and I&amp;rsquo;m going to
slowly port my Ansible roles to Bundlewrap this year. If you&amp;rsquo;re interested and
want to see more configurations/examples check out
&lt;a href="https://git.sr.ht/~bfiedler/bundlewrap">my&lt;/a> and (especially)
&lt;a href="https://git.kunsmann.eu/kunsi/bundlewrap">Franziska&amp;rsquo;s&lt;/a> repositories.&lt;/p>
&lt;p>If you have any questions or comments feel free to reach out to me via my
&lt;a href="https://lists.sr.ht/~bfiedler/public-inbox">public inbox&lt;/a> or toot
&lt;a href="https://mastodon.3fx.ch/@bfiedler">@bfiedler&lt;/a> on Mastodon.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>think &amp;lt;=1000 nodes/machines&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Quick reminder that Python 2 went EOL in January 2020.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>on your terminal, at least&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Laugh at me all you want, that is my biggest gripe when writing JSON by hand.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>On Decidability and the MU puzzle</title><link>https://3fx.ch/blog/2020/12/13/on-decidability-and-the-mu-puzzle/</link><pubDate>Sun, 13 Dec 2020 00:00:00 +0100</pubDate><guid>https://3fx.ch/blog/2020/12/13/on-decidability-and-the-mu-puzzle/</guid><description>&lt;p>&lt;a href="https://en.wikipedia.org/wiki/G%C3%B6del,_Escher,_Bach">Gödel, Escher, Bach&lt;/a>
takes the reader on a journey through mind, music, machines and self-reference.
In the first few chapters, Hofstadter introduces a formal system called the
MIU-system. The MIU-system consists of four simple rules for manipulating
strings consisting of the characters &lt;code>M&lt;/code>, &lt;code>I&lt;/code> and &lt;code>U&lt;/code>.&lt;/p>
&lt;ol>
&lt;li>&lt;code>xI -&amp;gt; xIU&lt;/code>, where &lt;code>x&lt;/code> matches the rest of the string&lt;/li>
&lt;li>&lt;code>Mx -&amp;gt; Mxx&lt;/code>, where &lt;code>x&lt;/code> matches the rest of the string&lt;/li>
&lt;li>&lt;code>xIIIy -&amp;gt; xUy&lt;/code>, where &lt;code>x&lt;/code> and &lt;code>y&lt;/code> match the rest of the string&lt;/li>
&lt;li>&lt;code>xUUy -&amp;gt; xy&lt;/code>, where &lt;code>x&lt;/code> and &lt;code>y&lt;/code> match the rest of the string&lt;/li>
&lt;/ol>
&lt;p>Note that the placeholders &lt;code>x&lt;/code> and &lt;code>y&lt;/code> must always match the entire string,
i.e. the application &lt;code>MII -&amp;gt; MIII&lt;/code>, choosing &lt;code>x = I&lt;/code>, is not valid. The correct
application is &lt;code>MII -&amp;gt; MIIII&lt;/code>.&lt;/p>
&lt;p>Then Hofstadter asks the reader to answer the MU puzzle:&lt;/p>
&lt;blockquote>
&lt;p>Given the initial string &lt;code>MI&lt;/code>, is it possible to construct the string &lt;code>MU&lt;/code> using only the four above rules?&lt;/p>
&lt;/blockquote>
&lt;p>Take a few minutes and try for yourself. Many people quickly suspect that it is
impossible, but why?&lt;/p>
&lt;h1 id="the-solution">The solution&lt;/h1>
&lt;p>Let us add an additional, imaginary rule.&lt;/p>
&lt;!-- raw HTML omitted -->
&lt;p>Each string &lt;code>Mx&lt;/code> in the new system now has a form using only an &lt;code>M&lt;/code> followed by
&lt;code>I&lt;/code>s, constructed by expanding all &lt;code>U&lt;/code>s.&lt;/p>
&lt;pre tabindex="0">&lt;code> MUIU
=&amp;gt; MIIIIIII
MU
=&amp;gt; MIII
MI
=&amp;gt; MI
&lt;/code>&lt;/pre>&lt;p>Define the &lt;em>value&lt;/em> of a string to be the number of &lt;code>I&lt;/code>s in it, after
transforming it to its &lt;code>MIIIIII...III&lt;/code> form.&lt;/p>
&lt;pre tabindex="0">&lt;code>value(MUIU) = 7
value(MU) = 3
value(MI) = 1
&lt;/code>&lt;/pre>&lt;p>The value of our target &lt;code>MU&lt;/code> is 3, which is divisible by 3, while the value of
our starting string &lt;code>MI&lt;/code> is 1, which is not divisible by 3. If we can show that,
starting with a string of value not divisible by 3, every rule application
cannot create a string with value divisible 3, then it is also impossible to get
&lt;code>MU&lt;/code> by starting with &lt;code>MI&lt;/code>.&lt;/p>
&lt;p>So take a string &lt;code>Mx&lt;/code> whose value is not divisible by 3. Rules 1, 3 and 4
preserve the value of &lt;code>Mx&lt;/code> modulo 3, so by assumption the resulting string also
has value not divisible by 3.&lt;/p>
&lt;p>Rule 2 doubles the value of a string. However, by doubling any number which is
not a multiple of 3 we can never create a number divisible by 3: a number &lt;code>n&lt;/code> is
divisible by 3 iff 3 is one of its prime factors. When doubling &lt;code>n&lt;/code> the only
prime factor we add is 2, hence the resulting number also cannot
have 3 as a prime factor. We can express this more succinctly as&lt;/p>
&lt;pre tabindex="0">&lt;code>∀x. x ≠ 0 (mod 3) --&amp;gt; 2x ≠ 0 (mod 3)
&lt;/code>&lt;/pre>&lt;p>Thus &lt;code>2*n&lt;/code> is not divisible by 3, and we can never construct the string &lt;code>MU&lt;/code>,
starting from &lt;code>MI&lt;/code>.&lt;/p>
&lt;h1 id="characterizing-all-generatable-strings">Characterizing all generatable strings&lt;/h1>
&lt;p>Not only have we shown that &lt;code>MU&lt;/code> is not constructible, starting from &lt;code>MI&lt;/code>, but
also any other string with a value divisible by 3. The question remains: which
strings can we generate? Is it possible to generate all other strings, i.e. all
strings &lt;code>Mx&lt;/code> such that &lt;code>value(Mx) != 0 (mod 3)&lt;/code>?&lt;/p>
&lt;p>The answer turns out to be yes, using the following algorithm.&lt;/p>
&lt;ol>
&lt;li>Generate &lt;code>My = MIIIIII...III&lt;/code> by applying rule 1 to &lt;code>MI&lt;/code>, such that the following holds:
the value of &lt;code>My&lt;/code> is larger than &lt;code>Mx&lt;/code> and &lt;code>value(My) = value(Mx) (mod 3)&lt;/code>.&lt;/li>
&lt;li>Append &lt;code>U&lt;/code> if &lt;code>value(My) != value(Mx) (mod 6)&lt;/code>.&lt;/li>
&lt;li>Merge &lt;code>IIIIII&lt;/code> to &lt;code>UU&lt;/code> and delete until &lt;code>value(My) == value(Mx)&lt;/code>.&lt;/li>
&lt;li>Replace the &lt;code>MIIII...III&lt;/code> with &lt;code>Mx&lt;/code> by applications of rules 2 and 3.&lt;/li>
&lt;/ol>
&lt;p>It is always possible to apply step 1: the infinite sequence of strings
generated by repeatedly applying rule 1 to &lt;code>MI&lt;/code> has values &lt;code>1, 2, 4, 8, 16, 32, ...&lt;/code>, generating all powers of 2. Taking these values modulo 3 we get &lt;code>1, 2, 1, 2, 1, 2, 1, 2, ...&lt;/code>, i.e. &lt;code>2^i (mod 3)&lt;/code> is &lt;code>1&lt;/code> if &lt;code>i&lt;/code> is even, and &lt;code>2&lt;/code> otherwise. Since
&lt;code>value(Mx) != 0&lt;/code> by assumption, there always exists a longer string &lt;code>My&lt;/code> such that
&lt;code>value(My) = value(Mx) (mod 3)&lt;/code>.&lt;/p>
&lt;p>In step 3 we need to delete &lt;code>U&lt;/code> pairs until we have that &lt;code>value(My) = value(Mx)&lt;/code>. Unfortunately, we can only decrease &lt;code>value(My)&lt;/code> in steps of six,
since we can only remove &lt;code>U&lt;/code>s in pairs. This is where rule 2 comes into play: if
&lt;code>value(My) != value(Mx) (mod 6)&lt;/code>, then there would always be one &lt;code>U&lt;/code> left over.
(Note: since these values are congruent modulo 3, the only possible case is that
&lt;code>value(Mx) == value(My) + 3 (modulo 6)&lt;/code>). Appending an additional &lt;code>U&lt;/code> before
deleting &lt;code>UU&lt;/code>s, increases &lt;code>value(My)&lt;/code> by 3, and everything works out.&lt;/p>
&lt;p>Step 4 is simple: &lt;code>Mx&lt;/code> has the same value as &lt;code>My&lt;/code> and we can use rule 2 to
convert &lt;code>III&lt;/code>s to &lt;code>U&lt;/code>s, in the right positions. Thus we have shown that the
&lt;code>MIU&lt;/code> system lets us generate precisely the strings which have a value not
divisible by 3.&lt;/p>
&lt;h1 id="the-miu-system-and-decidability">The MIU-system and decidability&lt;/h1>
&lt;p>The MIU-system isn&amp;rsquo;t just a neat puzzle to solve: Hofstadter shows the reader
that some questions about formal systems cannot be answered solely from within.
Rather, we had to step outside the restrictions placed upon us by the four rules
to successfully answer the question.&lt;/p>
&lt;p>Given infinite time we could have concluded this ourselves, by generating all
possible strings: however, in this case there exists a solution which is finite.
We have constructed a &lt;a href="https://en.wikipedia.org/wiki/Decision_problem">&lt;em>decision
procedure&lt;/em>&lt;/a> which solves not
only the MU-problem, but any decision problem of the form &amp;ldquo;Does candidate string
&lt;code>Mx&lt;/code> belong to the MIU-system, starting from &lt;code>MI&lt;/code>?&amp;rdquo;.&lt;/p>
&lt;p>It is not always possible to find a finite decision procedure. Take for example
all strings which are valid C programs (or choose any other &lt;a href="https://en.wikipedia.org/wiki/Turing_completeness">sufficiently
powerful&lt;/a> language, it
doesn&amp;rsquo;t matter). The decision problem &amp;ldquo;Does a given C program terminate at some
point?&amp;rdquo; is not solvable in finite time, as shown by &lt;a href="https://www.cs.virginia.edu/~robins/Turing_Paper_1936.pdf">Alan Turing
(1936)&lt;/a>. This problem
is also known as the &lt;a href="https://en.wikipedia.org/wiki/Halting_problem">halting
problem&lt;/a>, and is one of the most
famous &lt;a href="https://en.wikipedia.org/wiki/List_of_undecidable_problems">undecidable
problems&lt;/a>.&lt;/p>
&lt;p>These abstract problems can even have real world consequences: this year it was
shown that type-checking a Swift program is &lt;a href="https://forums.swift.org/t/swift-type-checking-is-undecidable/39024">also an undecidable
problem&lt;/a>.
The author shows that in order to type-check a program, the compiler must solve the
&lt;a href="https://en.wikipedia.org/wiki/Word_problem_for_groups">word problem for finitely generated
groups&lt;/a>. I like how this
example shows us that some abstract problems pop up in unexpected places, and
why seemingly purely theoretical knowledge matters, even for applied problems
such as building compilers.&lt;/p>
&lt;p>If you have any questions or comments feel free to reach out to me via my
&lt;a href="https://lists.sr.ht/~bfiedler/public-inbox">public inbox&lt;/a>. If you are
interested in undecidability in other programming languages, you might also like
this website I made: &lt;a href="https://3fx.ch/typing-is-hard.html">typing-is-hard.ch&lt;/a>.&lt;/p></description></item><item><title>Composition in Trick-Taking Card Games</title><link>https://3fx.ch/blog/2020/12/03/composition-in-trick-taking-card-games/</link><pubDate>Thu, 03 Dec 2020 00:00:00 +0900</pubDate><guid>https://3fx.ch/blog/2020/12/03/composition-in-trick-taking-card-games/</guid><description>&lt;blockquote>
&lt;p>This article was written as part of the &lt;a href="https://adventofhaskell.com">Advent of Haskell
2020&lt;/a>, be sure to check it out for other cool
Haskell content!&lt;/p>
&lt;/blockquote>
&lt;p>A &lt;a href="https://en.wikipedia.org/wiki/Trick-taking_game">trick-taking game&lt;/a> is a
genre of (primarily) card games. Play revolves around rounds, in which each
player plays exactly one card. The player who played the highest card wins the
round, or &lt;em>trick&lt;/em>.&lt;/p>
&lt;p>On the surface these games seem to possess imperative core, processing
well-defined actions once after the other. In this blog post we dive a bit
deeper into the implementation of determining trick winners, and discover a neat
application of composition to solve this problem, using Haskell and its standard
library.&lt;/p>
&lt;h1 id="hearts">Hearts&lt;/h1>
&lt;p>We consider concrete games, increasing in complexity, starting with
&lt;a href="https://en.wikipedia.org/wiki/Hearts_(card_game)">Hearts&lt;/a>. Hearts is played
with a 52-card deck of four suits (Clubs, Spades, Hearts, Diamonds) and thirteen
ranks (from highest to lowest: A-K-Q-J-10-9-&amp;hellip;-3-2).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">data&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Clubs&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Spades&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Hearts&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Diamonds&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">deriving&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Show&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Eq&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">data&lt;/span> &lt;span class="kt">Rank&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Two&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Three&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Four&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="o">..&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Jack&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Queen&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">King&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kt">Ace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">deriving&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Show&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Eq&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Ord&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">data&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">getRank&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Rank&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">deriving&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Show&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Eq&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>deriving&lt;/code> clauses cause Haskell to automatically derive some boilerplate
functions for us. This includes the &lt;code>show&lt;/code> function for converting &lt;code>Card&lt;/code>s,
&lt;code>Suit&lt;/code>s and &lt;code>Rank&lt;/code>s to &lt;code>String&lt;/code>s, equality predicates for all three types and,
in case of &lt;code>Rank&lt;/code>s, an ordering such that we have &lt;code>Two &amp;lt; Three &amp;lt; .. &amp;lt; Ace&lt;/code>. We
can verify that this works using &lt;code>ghci&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">Main&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="kt">:&lt;/span>&lt;span class="n">l&lt;/span> &lt;span class="kt">Cards&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">*&lt;/span>&lt;span class="kt">Cards&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">aceOfSpades&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="kt">Spades&lt;/span> &lt;span class="kt">Ace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">*&lt;/span>&lt;span class="kt">Cards&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">aceOfSpades&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">Card&lt;/span> &lt;span class="kt">Spades&lt;/span> &lt;span class="kt">Ace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">*&lt;/span>&lt;span class="kt">Cards&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">twoOfDiamonds&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="kt">Diamonds&lt;/span> &lt;span class="kt">Two&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">*&lt;/span>&lt;span class="kt">Cards&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">aceOfSpades&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">twoOfDiamonds&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">*&lt;/span>&lt;span class="kt">Cards&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">aceOfSpades&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">aceOfSpades&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">*&lt;/span>&lt;span class="kt">Cards&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="kt">Two&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="kt">Three&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">True&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To determine the winner of a trick, we first inspect the suit which was &lt;em>led&lt;/em>,
i.e. played first. The highest card of that suit wins. Programatically this
corresponds to choosing the maximum of a list, for which Haskell has the aptly
named &lt;code>maximum&lt;/code> function in its standard library. The &lt;code>maximum&lt;/code> function
requires that the type of elements of the list has &lt;code>Ord&lt;/code> instance, i.e. is
orderable. Unfortunately this is not the case: the ordering we choose depends on
the suit that led, there is no global ordering we can apply to every Hearts
trick. Haskell allows us to use a custom ordering function in this case, and the
function &lt;code>maximumBy ordFunc&lt;/code> computes the maximum using &lt;code>ordFunc&lt;/code> as underlying
ordering.&lt;/p>
&lt;p>The comparison function &lt;code>ordFunc&lt;/code> must take two list elements and return a value
of type &lt;code>Ordering&lt;/code>, where &lt;code>Ordering&lt;/code> can be &lt;code>LT&lt;/code> (for less), &lt;code>EQ&lt;/code> (for equal) or
&lt;code>GT&lt;/code> (for greater).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-haskell" data-lang="haskell">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nn">Data.Foldable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nn">Data.Ord&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">heartsCompare&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">heartsCompare&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Card&lt;/span> &lt;span class="n">s1&lt;/span> &lt;span class="n">r1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Card&lt;/span> &lt;span class="n">s2&lt;/span> &lt;span class="n">r2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="n">s1&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">s2&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">r1&lt;/span> &lt;span class="p">`&lt;/span>&lt;span class="n">compare&lt;/span>&lt;span class="p">`&lt;/span> &lt;span class="n">r2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="n">s1&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">s2&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">GT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="n">s1&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">s2&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">LT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="n">otherwise&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">undefined&lt;/span> &lt;span class="c1">-- neither c1 nor c2 have the leading suit,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">-- we cannot compare these two cards&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">trickWinner&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Card&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">trickWinner&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">lead&lt;/span>&lt;span class="kt">:&lt;/span>&lt;span class="n">rest&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">maximumBy&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">heartsCompare&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="kt">:&lt;/span>&lt;span class="n">rest&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mathematically inclined readers will notice two things: Firstly, the Haskell
&lt;code>Ord&lt;/code> type class models a &lt;em>total order&lt;/em>, which means that it assumes that any
two cards are comparable. The Hearts ordering function we constructed however is
actually a partial order, since we cannot compare two cards of different suits.
This is not a problem however: Our ordering is always defined if the leading
suit is played in a trick, and this is, by definition of leading suit, always
the case.&lt;/p>
&lt;p>Secondly, the ordering suspiciously looks like a &lt;em>lexicographic (partial)
ordering&lt;/em>: first we compare suits, and the &amp;ldquo;higher&amp;rdquo; suit wins&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. If the suits are
equal then we move on to ranks. Lexical composition on orderings returns the
first decisive (i.e. non &lt;code>EQ&lt;/code>) ordering, or &lt;code>EQ&lt;/code> if both are equal.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">lexicalComp&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Ordering&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">lexicalComp&lt;/span> &lt;span class="kt">EQ&lt;/span> &lt;span class="n">o2&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">o2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">lexicalComp&lt;/span> &lt;span class="n">o1&lt;/span> &lt;span class="kr">_&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">o1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Looking a bit closer we can see two interesting properties of the &lt;code>lexicalComp&lt;/code>
function: It is associative, and it has a neutral element (&lt;code>EQ&lt;/code>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- the function is associative&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">lexicalComp&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">lexicalComp&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">lexicalComp&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">lexicalComp&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- the function has a left and right identity element&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">lexicalComp&lt;/span> &lt;span class="kt">EQ&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">lexicalComp&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="kt">EQ&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Thus lexical composition of &lt;code>Ordering&lt;/code>s forms a &lt;code>Monoid&lt;/code> with identity element
&lt;code>EQ&lt;/code>. This instance is available in &lt;a href="https://hackage.haskell.org/package/base-4.14.0.0/docs/src/GHC.Base.html#line-415">Haskell&amp;rsquo;s standard
library&lt;/a>,
so we can shorten our definition somewhat.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nn">Data.Monoid&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">lexicalComp&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- used infix like a &amp;lt;&amp;gt; b&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Using this idea we can simplify our definition of
&lt;code>heartsCompare&lt;/code>. Note that the cases which previously were &lt;code>undefined&lt;/code> are
now defined, however as argued before it does not matter much how we decide in
these cases.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitCompare&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitCompare&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">comparing&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankCompare&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Rank&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Rank&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankCompare&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">compare&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">heartsCompare&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">heartsCompare&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">suitCompare&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getSuit&lt;/span> &lt;span class="n">c1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getSuit&lt;/span> &lt;span class="n">c2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">rankCompare&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getRank&lt;/span> &lt;span class="n">c1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getRank&lt;/span> &lt;span class="n">c2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We have separated comparison of suit and rank and combine the results using
abstractions provided by the Haskell standard library. Instead of combining the
results we can also combine the comparison functions themselves, by defining a
&lt;code>newtype&lt;/code> for &lt;code>Comparison&lt;/code> functions.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">newtype&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">getComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Ordering&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- we can combine any two ordering functions, as long as&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- they compare the same elements&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">instance&lt;/span> &lt;span class="kt">Monoid&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Comparison&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kr">where&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mappend&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">g&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getComparison&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getComparison&lt;/span> &lt;span class="n">g&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mempty&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">EQ&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We would like to use this comparison on the above definition of
&lt;code>heartsComparison&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Suit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">comparing&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Rank&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankComparison&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="n">compare&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- type error! `suitCompare` is of type `Comparison Suit` and&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- `rankCompare` is of type `Comparison Rank`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">badHeartsComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">badHeartsComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">suitComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">rankComparison&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This, however, is a type error: The comparisons have different types, so we
cannot combine them. Instead, we have to ensure that both &lt;code>Comparison&lt;/code>s act on
&lt;code>Card&lt;/code>s.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitCardComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitCardComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">comparing&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getSuit&lt;/span> &lt;span class="n">c1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getSuit&lt;/span> &lt;span class="n">c2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankCardComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankCardComparison&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">compare&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getRank&lt;/span> &lt;span class="n">c1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getRank&lt;/span> &lt;span class="n">c2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">verboseHeartsComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">verboseHeartsComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">suitCardComparison&lt;/span> &lt;span class="n">lead&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">rankCardComparison&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>These definitions are verbose and repetitive, since all we did was to apply an
extra function prior to applying the comparison function. Also both definitions
look similar, so let us extract the functionality into a new function &lt;code>mystery&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">mystery&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">mystery&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">c&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getComparison&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span> &lt;span class="n">c1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span> &lt;span class="n">c2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- now we can express suitCardComparison and rankCardComparison more succinctly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitCardComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">mystery&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="n">suitComparison&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">rankCardComparison&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">mystery&lt;/span> &lt;span class="n">getRank&lt;/span> &lt;span class="n">rankComparison&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can abstract &lt;code>mystery&lt;/code> even further, by replacing &lt;code>Card&lt;/code> with a type
parameter &lt;code>b&lt;/code>. The resulting type looks almost like the definition of a
&lt;code>Functor&lt;/code>, with flipped arrows!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">fmap&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">mystery&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">b&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">mystery&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">c&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">c1&lt;/span> &lt;span class="n">c2&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getComparison&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span> &lt;span class="n">c1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span> &lt;span class="n">c2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Indeed, &lt;code>Comparison&lt;/code> is what the literature calls a &lt;em>contravariant functor&lt;/em>,
which is related to the (covariant) &lt;code>Functor&lt;/code>. The terms co- and contravariant
come from the definitions of the mapping function: A type is called &lt;em>covariant&lt;/em>
when it preserves the direction of the mapping function (going from &lt;code>a -&amp;gt; b&lt;/code> to
&lt;code>f a -&amp;gt; f b&lt;/code>), while a &lt;em>contravariant&lt;/em> reverses the direction of the arrow (going
from &lt;code>b -&amp;gt; a&lt;/code> to &lt;code>f a -&amp;gt; f b&lt;/code>. There is a lot of literature available&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> for
the theoretical background for interested readers, we will not go into further
detail here.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">class&lt;/span> &lt;span class="kt">Functor&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="kr">where&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fmap&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">class&lt;/span> &lt;span class="kt">Contravariant&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="kr">where&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">contramap&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">b&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The abstractions &lt;code>Covariant&lt;/code> and &lt;code>Comparison&lt;/code> we invented before are part of
Haskell&amp;rsquo;s standard library, including the &lt;code>Monoid&lt;/code> instance of &lt;code>Comparison&lt;/code>.
They can found in the
&lt;a href="https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Functor-Contravariant.html">&lt;code>Data.Functor.Contravariant&lt;/code>&lt;/a>
module.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nn">Data.Functor.Contravariant&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- Data.Functor.Contravariant defines defaultComparison for us&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">--defaultComparison = Comparison compare&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Suit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">comparing&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">heartsComparison&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">heartsComparison&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">suitComparison&lt;/span> &lt;span class="n">lead&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="n">getRank&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">defaultComparison&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">trickWinner&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Card&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">trickWinner&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">lead&lt;/span>&lt;span class="kt">:&lt;/span>&lt;span class="n">rest&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">maximumBy&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getComparison&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="n">heartsComparison&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="kt">:&lt;/span>&lt;span class="n">rest&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When I first read about contravariant functors I had troubles imagining where
these might come in handy, but hopefully this example demonstrates their
usefulness.&lt;/p>
&lt;h1 id="other-trick-taking-games">Other Trick-taking games&lt;/h1>
&lt;p>This seems like an overly complicated solution if we are only interested in
implementing the Hearts card game. However, this abstraction proves very useful
if we turn to other trick-taking games, which have more complicated rules. Let&amp;rsquo;s
look at &lt;a href="https://en.wikipedia.org/wiki/Skat_(card_game)">Skat&lt;/a> next, one of my
favorite trick-taking games. Skat is played with three players and a 32-card
deck, consisting of the same suits as Hearts, however only using cards of rank
seven and up.&lt;/p>
&lt;p>There is a variety of possible game modes, each with different orderings
to score the tricks. The typical the so-called &lt;em>Farbspiel&lt;/em>, in which all &lt;em>Buben&lt;/em>
(jacks) and all colors of a specific suit are trump cards. The jacks have an
ordering based on suits, from highest to lowest: Clubs-Spades-Hearts-Diamonds.
Trump cards always win a trick, and if multiple trumps are played in the same
trick then the highest one wins. If no trump is played, then the trick is scored
as it is in Hearts: The highest card of the leading suit wins the trick.&lt;/p>
&lt;p>Notably, the suit cards are ranked A-10-K-Q-9-8-7, with 10 between ace
and king. The jacks are omitted since they belong to the trump cards. Let&amp;rsquo;s
first define the &lt;code>skatRank&lt;/code> comparison.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nn">Data.List&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">elemIndex&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- If `a` is an instance of the `Ord` typeclass,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- then we can also order elements of type `Maybe a`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- using&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- compare Nothing (Just x) = LT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">highTen&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Rank&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">highTen&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="p">(`&lt;/span>&lt;span class="n">elemIndex&lt;/span>&lt;span class="p">`&lt;/span> &lt;span class="n">skatRanks&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">defaultComparison&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">where&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skatRanks&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Seven&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Eight&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Nine&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Queen&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">King&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Ten&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Ace&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">compareSkatRank&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">compareSkatRank&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="n">getRank&lt;/span> &lt;span class="n">highTen&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The function &lt;code>highTen&lt;/code> captures the anomaly in the Skat ranking, namely that
&lt;code>Ten&lt;/code> is the second-highest ranked card of a suit. We can use the same technique
as above to rank the suits when comparing the suits of two jacks.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitOrder&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Suit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">suitOrder&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="p">(`&lt;/span>&lt;span class="n">elemIndex&lt;/span>&lt;span class="p">`&lt;/span> &lt;span class="n">rankedSuits&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">defaultComparison&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">where&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rankedSuits&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="kt">Diamonds&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Hearts&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Spades&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Clubs&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">compareSuits&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">compareSuits&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="n">suitOrder&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally we can combine the two &lt;code>Comparison&lt;/code>s above to determine the winner of a
trick in a Farbspiel. To do this we consider a lexicographical composition of
three orderings: First we compare both cards according to the jack-comparison
rules. A jack wins against a non-jack and if both cards are jacks we compare
using the defined &lt;code>suitOrder&lt;/code>. Then we compare the trumps on both sides. Again
trump always wins against non-trump, and if both are trumps then we need to
consider the &lt;code>skatRank&lt;/code> order. Finally if neither of these is true then we use
the same algorithm as in the Hearts game, awarding the trick to the highest card
of the leading suit.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- generalization of suitComparison, useful when we only care&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- about a particular subset of values (e.g. one suit, one rank, etc.)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">when&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Bool&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">when&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">cmp&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="n">aux&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">where&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">aux&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">getComparison&lt;/span> &lt;span class="n">cmp&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="n">otherwise&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">comparing&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">whenSuit&lt;/span> &lt;span class="n">s&lt;/span> &lt;span class="n">cmp&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">when&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">s&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="n">getSuit&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">cmp&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">whenRank&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="n">cmp&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">when&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="o">==&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="n">getRank&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">cmp&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">farbspiel&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">farbspiel&lt;/span> &lt;span class="n">trump&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">whenRank&lt;/span> &lt;span class="kt">Jack&lt;/span> &lt;span class="n">compareSuits&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="n">trump&lt;/span> &lt;span class="n">compareSkatRank&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="n">compareSkatRank&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now we can easily use our powerful combinators to implement the &lt;em>Grand&lt;/em> variant,
in which there is no trump suit, only the jacks are trumps.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- in a grand only the four jacks are trumps&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">grand&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">grand&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">whenRank&lt;/span> &lt;span class="kt">Jack&lt;/span> &lt;span class="n">compareSuits&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="n">compareSkatRank&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Even different rankings are not a problem: In the so called &lt;em>Nullspiel&lt;/em> no
trumps exist, and the cards are sorted as they are in the Hearts game. The
player&amp;rsquo;s goal is to ensure that he does not win a trick.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">nullspiel&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">nullspiel&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="n">heartsComparison&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This example highlights the use of composition very well: Instead of writing one
large comparison function per card game we can combine four simple ones using
the powerful abstractions in the Haskell standard library.&lt;/p>
&lt;h1 id="bonus-game-doppelkopf">Bonus game: Doppelkopf&lt;/h1>
&lt;p>Lastly, let&amp;rsquo;s take a short look at another one of my favorite games:
&lt;a href="https://en.wikipedia.org/wiki/Doppelkopf">Doppelkopf&lt;/a>. Doppelkopf is special
since it is not played with one, but two sets of cards, with the same suits as
before and ranks 9-J-Q-K-10-A&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. Most Doppelkopf games are scored similar to a
&lt;em>Farbspiel&lt;/em> where Diamonds are trump, with some extra trumps: all jacks and all
queens are trumps, with the queens ranked higher than the jacks, and both queens
and jacks being ordered by the same &lt;code>suitOrder&lt;/code> as before. The two tens of
hearts are the two highest trumps, and are called &lt;em>Dullen&lt;/em> (sg. &lt;em>Dulle&lt;/em>&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>).&lt;/p>
&lt;p>To uniquely determine a trick winner now, we also need to know who played which
card, since the same card may be present twice. The official tournament rules
state: If the highest card is present twice, then the first card played wins.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">firstWins&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">GT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">dokoTournamentRules&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">dokoTournamentRules&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">when&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="kt">Hearts&lt;/span> &lt;span class="kt">Ten&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenRank&lt;/span> &lt;span class="kt">Queen&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSuits&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenRank&lt;/span> &lt;span class="kt">Jack&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSuits&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="kt">Diamonds&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSkatRank&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSkatRank&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">dokoTrickWinner&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="p">[(&lt;/span>&lt;span class="kt">Card&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Player&lt;/span>&lt;span class="p">)]&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">Card&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">Player&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">dokoTrickWinner&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lead&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">player&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="kt">:&lt;/span>&lt;span class="n">rest&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">maximumBy&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">getComparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">contramap&lt;/span> &lt;span class="n">fst&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">dokoTournamentRules&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">getSuit&lt;/span> &lt;span class="n">lead&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="kt">:&lt;/span>&lt;span class="n">rest&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Doppelkopf has more variants than any other card game I know of, ranging from
slight alterations to total game changers. The most common variant we play with
concerns the ten of hearts, in which the second ten of hearts beats the first.
This sounds like a complete headache to model, however using the abstractions we
developed for Skat already modeling Doppelkopf is child&amp;rsquo;s play.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">firstWins&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">GT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">secondWins&lt;/span> &lt;span class="ow">=&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="nf">\&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">LT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">secondHeartsTenWins&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Suit&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">secondHeartsTenWins&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">when&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="kt">Hearts&lt;/span> &lt;span class="kt">Ten&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">secondWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenRank&lt;/span> &lt;span class="kt">Queen&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSuits&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenRank&lt;/span> &lt;span class="kt">Jack&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSuits&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="kt">Diamonds&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSkatRank&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">whenSuit&lt;/span> &lt;span class="n">lead&lt;/span> &lt;span class="o">$&lt;/span> &lt;span class="n">compareSkatRank&lt;/span> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">firstWins&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Another variant is called &lt;em>Schweinchen&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>&lt;/em>, which the player having both
Diamond Aces must announce. Both Diamond Aces then become the highest trumps.
Even playing with the piglet rule, some games may not feature piglets at all, in
which case the Diamond Aces are not special.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hs" data-lang="hs">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">withSchweinchen&lt;/span> &lt;span class="ow">::&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="ow">-&amp;gt;&lt;/span> &lt;span class="kt">Comparison&lt;/span> &lt;span class="kt">Card&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">withSchweinchen&lt;/span> &lt;span class="n">normalComparison&lt;/span> &lt;span class="ow">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">when&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">==&lt;/span> &lt;span class="kt">Card&lt;/span> &lt;span class="kt">Diamond&lt;/span> &lt;span class="kt">Ace&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">firstWins&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;lt;&amp;gt;&lt;/span> &lt;span class="n">normalComparison&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="drawbacks">Drawbacks&lt;/h1>
&lt;p>I want to point out some drawbacks of this approach. The &lt;code>Ord&lt;/code> typeclass models
total orderings, and this ordering is not total, only partial. The comparisons
we created may not work with minimums or other ordering functions, for example.
Arguably, a new type class &lt;code>PartialOrd&lt;/code> would be cleaner, including an
&amp;ldquo;incomparible&amp;rdquo; comparison result in &lt;code>PartialOrdering&lt;/code>. To my knowledge, these are
not available in the standard library sadly, although there exists an implementation in the
&lt;a href="https://hackage.haskell.org/package/lattices-2.0.2/docs/Algebra-PartialOrd.html">&lt;code>lattices&lt;/code>&lt;/a>
package.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>We have developed an abstraction for modelling trick-taking games. In the
process, we (re)discovered concepts from the Haskell standard library.
Hopefully, this post helped you in understanding these concepts and their
applications. If you have any questions or comments feel free to reach
out to me via my &lt;a href="https://lists.sr.ht/~bfiedler/public-inbox">public inbox&lt;/a>.&lt;/p>
&lt;p>Thanks for sticking with me, now go play some card games!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Where the leading suit is higher than the other suits, and the other suites are incomparable.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>&lt;a href="https://stackoverflow.com/questions/38034077/what-is-a-contravariant-functor">This StackOverflow question&lt;/a> gives a good overview and has links to great resources.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Local rules may even omit nines.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>I do not know the etymology of the German word &lt;em>Dulle&lt;/em>, so I cannot translate it.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>German for &lt;em>piglets&lt;/em>.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Build Your Own Wi-Fi Antenna</title><link>https://3fx.ch/blog/2020/11/28/build-your-own-wi-fi-antenna/</link><pubDate>Sat, 28 Nov 2020 16:00:00 +0100</pubDate><guid>https://3fx.ch/blog/2020/11/28/build-your-own-wi-fi-antenna/</guid><description>&lt;p>Building a wireless antenna is not hard. While the underlying physics and
protocols are relatively complex, an antenna&amp;rsquo;s job is very simple: it captures
or creates electromagnetic waves.
We built a basic parabolic receiving antenna using a 3D printer and a
female-female &lt;a href="https://en.wikipedia.org/wiki/N_connector">N-connector&lt;/a>,
available in any well-stocked electronic parts store.&lt;/p>
&lt;p>There exist a multitude of different antenna designs, each with different
characteristics. The simplest type is the &lt;em>isotropic&lt;/em> antenna, which sends or
receives from all (three dimensional) directions approximately equally well.
Closely related is the &lt;em>omnidirectional&lt;/em> antenna, which receives equally well
from all compass directions, but is sensitive to the vertical angle of incoming
electromagnetic waves. The WiFi antennas used in laptops and smartphones are
often (close to) omnidirectional, as it would be really impractical if
orientation had an effect on signal strength!&lt;/p>
&lt;p>Antennas which are sensitive to the angles of incoming radio waves are called
&lt;em>directional&lt;/em>. Common uses of directional antennas are at space observatories,
but also satellite TV antennas or military and civilian radars. They allow for
efficient communication over much greater lengths than omnidirectional or
isotropic antennas. Directional antennas come in different forms, a well-known
one being the &lt;a href="https://en.wikipedia.org/wiki/Parabolic_antenna">&lt;em>parabolic&lt;/em>
antenna&lt;/a>, which uses a
parabolic dish to focus the electromagnetic waves on a single point.&lt;/p>
&lt;h1 id="the-build">The build&lt;/h1>
&lt;p>The most important part of a parabolic antenna is the parabola: it is described
by the equation $y = x^2 / 4a$, where $a$ is the &lt;em>focal length&lt;/em>, i.e. the
distance from the center of the antenna to the &lt;em>focal point&lt;/em>, at which the
signal is concentrated. Of course it is infeasible to build an infinitely large
antenna, so the parabola is cut off at some point, resulting in a diameter $d$
and height $h$. These parameters are important when printing the design, since
they impact the stability and feasibility of the print.&lt;/p>
&lt;figure class="invertable resizable">&lt;img src="https://3fx.ch/blog/img/antenna-schema.png"
alt="Optimal parabolic antenna equation"/>&lt;figcaption>
&lt;p>Optimal parabolic antenna equation&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>Such a parabolic antenna has a theoretical gain of $η * (π * d / λ)²$, where $λ$
is the wavelength of the measured signal and $η$ is the so-called &lt;em>aperture
efficiency&lt;/em>, commonly between 0.5 and 0.7. It is a catch-all for uneven dish
surface, poor antenna placement and other errors.&lt;/p>
&lt;p>Balz, a friend of mine, helped us model and print the antenna using
&lt;a href="https://www.autodesk.com/products/fusion-360/personal">Fusion360&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> and his
&lt;a href="https://www.prusa3d.com/original-prusa-i3-mk3/">Prusa i3 MK3S+&lt;/a>. The 3D model can
be downloaded &lt;a href="https://3fx.ch/blog/static/antenna-model.stl">here&lt;/a>. Of course, any decent 3D
modeling software and printer will do.&lt;/p>
&lt;figure class="resizable">&lt;img src="https://3fx.ch/blog/img/antenna-render.png"
alt="Rendered antenna model"/>&lt;figcaption>
&lt;p>Rendered antenna model&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>Due to the layer-by-layer operation of 3D printing, shallow slopes often end up
with an uneven surface&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, which lowers the effectiveness of the antenna. We
attempted to fix this by taping a high-density rubber foam sheet in the dish,
and covering it with aluminium tape. This better reflects the electromagnetic
waves that hit the inside of the dish. Both materials can be bought cheaply in a
hardware store.&lt;/p>
&lt;figure class="resizable">&lt;img src="https://3fx.ch/blog/img/antenna-build.png"
alt="The completed build"/>&lt;figcaption>
&lt;p>The completed build&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>And all set! In order to measure how good our DIY antenna is we compared it
against a professional so-called
&lt;a href="https://en.wikipedia.org/wiki/Cantenna">&lt;em>cantenna&lt;/em>&lt;/a>, another type of
directional antenna which can be built easily at home. We used a &lt;a href="https://www.tp-link.com/us/home-networking/usb-adapter/tl-wn722n/">TP-Link
WN722N&lt;/a> USB
wifi adapter as sending station, and the &lt;a href="https://www.alfa.com.tw/products/awus036nh">Alfa
AWUS036NH&lt;/a> USB adapter as receiver.
Getting the driver of the TP-Link stick to work was quite the pain, but we
managed and set out to measure the different antennas. For our test setup we
created a Wi-Fi network on the TP-Link adapter and connected to it from the Alfa
adapter. We measured the signal strength using &lt;code>iwconfig&lt;/code>.&lt;/p>
&lt;p>There are many effects which influence the signal strength at the receiver, such
as the transmission power, sending and receiving antenna gains, distance between
sender and receiver, the carrier medium, and so on. Except for the receiving
antenna all other properties stay the same. Thus it suffices to measure the
received signal strength only - we can infer the relative gain between the
antennas that way.&lt;/p>
&lt;p>We are interested in determining the recipient&amp;rsquo;s antenna gain, i.e. how much the
antenna amplifies a signal when receiving. This is generally measured in
decibels-isotropic (dBi), which is the factor between this antenna&amp;rsquo;s gain and an
idealized isotropic antenna.&lt;/p>
&lt;p>Signal strength is measured in decibel-milliwatts (dBm), which expresses the
change in signal power level per milliwatt increase. Both antenna gain and
signal strength are logarithmically scaled, which simplifies calculations: the
signal strength measured using an antenna is the actual signal strength at the
receiver (in dBm) plus the antenna gain (in dBi). It is important to consider
the right environment when measuring antenna gain: there should be no reflective
surfaces such as building walls, bridges or similar close by. Also be sure to
have line-of-sight between both antennas, as any objects between them disturb
the measurements. We chose an open field near our city.&lt;/p>
&lt;p>We measured the signal strength once for increasing distances between 1m and
100m, and another time in a 360° radius at 20m, using 20° increments using three
antennas: Our DIY parabolic antenna, the professional cantenna, and the
omnidirectional antenna which came included with the Alfa adapter, which is
rated at 5dBi. Based on the omnidirectional antenna we can estimate the gain of
the other two antennas.&lt;/p>
&lt;h1 id="distance-measurements">Distance measurements&lt;/h1>
&lt;figure class="invertable resizable">&lt;img src="https://3fx.ch/blog/img/antenna-dist.png"
alt="Plot of signal strength with varying distance"/>&lt;figcaption>
&lt;p>Signal strength (dBm) relative to distance (m)&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>Surprisingly, the omnidirectional antenna outperforms not only our antenna (which
was probably to be expected), but also the professional cantenna.&lt;/p>
&lt;p>We have a few theories regarding the comparatively poor performance of our
antenna: during transport I accidentally bent the copper wire a bit, and we did
not manage to bend it back perfectly, potentially shifting wire from the focus.
Furthermore, our antenna dish is not a perfect parabola, but contains blemishes
from the underlying sponge rubber, diffracting signals instead of focusing them.&lt;/p>
&lt;h1 id="directionality-measurements">Directionality measurements&lt;/h1>
&lt;figure class="invertable resizable">&lt;img src="https://3fx.ch/blog/img/antenna-angle.png"
alt="Plot of signal strength with varying angle"/>&lt;figcaption>
&lt;p>Signal strength (dBm) relative to angle (°)&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>Unsurprisingly, the omnidirectional antenna is not sensitive to orientation,
while the other two antennas are. The cantenna displays a beautiful profile of
directionality: Good signal strength when pointing directly towards the source,
then decreasing until hitting the minimum around 180°. Our parabolic antenna
is also directional, albeit less consistently. In addition to the reasons listed above
the length of our copper wire may also be responsible: since it extends beyond
the focal point in both directions, signals which are reflected close to the
focal point hit the copper wire as well, reducing directionality.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Our DIY antenna is a success: while it is not as good as an off-the-shelf
omnidirectional antenna it is certainly good enough to receive Wi-Fi signals
over a range of 100m, comparable to a non-DIY cantenna. It displays some form of
directionality, if not a very good one. Coming back to antenna gain it seems
that our antenna&amp;rsquo;s gain is not even close to its theoretical maximum of 14 dBi,
we estimate it to be closer to 2-3 dBi.&lt;/p>
&lt;p>There are many possible improvements to this design. The easiest way to increase
antenna gain (and directionality) is to increase its diameter. While 200mm is
close to the maximum of the Prusa i3, it is possible to split the design into
quarter dishes, assembling a dish twice as large. Using a smoother material to
cover the inside of the dish may also help. Finally, replacing the copper wire
with a receiver only at the focal length of 100mm should increase
directionality, at cost of a significantly more complex model.&lt;/p>
&lt;p>A variety of other DIY antenna models also exist, such as the
&lt;a href="https://en.wikipedia.org/wiki/Cantenna">cantenna&lt;/a> model we compared against,
&lt;a href="https://en.wikipedia.org/wiki/WokFi">Wok-Fi&lt;/a> (using woks or similar kitchenware
as antenna dish), and many more. Hopefully this post showed you that building a
Wi-Fi antenna is doable using only cans or a 3D printer, as well as some
inexpensive hardware store/electronics. And can be a fun side project!&lt;/p>
&lt;p>If you have any questions or comments feel free to reach
out to me via my &lt;a href="https://lists.sr.ht/~bfiedler/public-inbox">public inbox&lt;/a>.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>See his &lt;a href="https://www.reddit.com/r/Fusion360/comments/ejg226/accurate_parabolas_in_fusion_360/">reddit
thread&lt;/a>
about parabolic antennas in Fusion360.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>also called the &amp;ldquo;staircase effect&amp;rdquo;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>The Online Knapsack Problem</title><link>https://3fx.ch/blog/2020/11/12/the-online-knapsack-problem/</link><pubDate>Thu, 12 Nov 2020 00:00:00 +0100</pubDate><guid>https://3fx.ch/blog/2020/11/12/the-online-knapsack-problem/</guid><description>&lt;blockquote>
&lt;p>Last semester I took a course on online and approximation algorithms, and I want
to present some really neat proofs related to the online knapsack problem. The
course was taught by Hans-Joachim Bökenhauer and Dennis Komm, wo incidentally
also first discovered the results I will present below. All results mentioned
(and more) can be found in the &lt;a href="https://www.research-collection.ethz.ch/bitstream/handle/20.500.11850/210420/eth-50826-01.pdf">original
paper&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;p>An online algorithm is given some input sequence $x1, x2, &amp;hellip;, xn$, and must
respond to each input $xi$ with some output $yi$, which may depend both on the
current (and previous) inputs and the algorithm&amp;rsquo;s previous outputs. The
algorithm does not know in advance how long the input sequence is, which
presents an additional challenge.&lt;/p>
&lt;p>Online algorithms are judged by their &lt;em>competitive ratio&lt;/em>, which is the ratio
between the score of an optimal solution of offline variant and the score of the
solution produced by the algorithm. If this ratio is less than some number $k$,
then we call the algorithm &lt;em>$k$-competitive&lt;/em>. If the competitive ratio in
unbounded, then we say the algorithm is &lt;em>not competitive&lt;/em>.&lt;/p>
&lt;p>Normally an offline algorithm is deterministic, which means it cannot access any
other information than the inputs its given. A randomized online algorithm can
additionally access an infinite stream of bits, each independently either $0$ or
$1$ with probability exactly $1/2$. In this case we score the algorithm by its
expected competitive factor, since its exact score may vary based on the
randomness. Finally, an online algorithm with advice is permitted to access an
infinite stream of advice bits, which an oracle has computed based on the input
it is given. When analyzing randomized online algorithms or online algorithms
with advice we are interested in minimizing the number of random or advice bits
used to achieve specific (expected) competitive ratios.&lt;/p>
&lt;p>We will focus on the simple online knapsack problem, which is the following:
Given a backpack of capacity $1$ and $n$ objects with weights $w1, &amp;hellip;, wn$,
fill the backpack as much as possible, without exceeding the capacity. We
process the weights in order, and for each weight we have to immediately decide
whether we pack it or not, and we cannot change our choice at a later point in
time. Restricting ourselves to capacity $1$ seems arbitrary, but we can easily
adjust for any backpack capacity $b$ by scaling the weights accordingly.&lt;/p>
&lt;p>First of all we notice that any deterministic algorithm for the online knapsack
problem is not competitive. We can &amp;ldquo;trick&amp;rdquo; any algorithm $A$ using the following
procedure: First we offer an object of weight $eps$, where $eps$ can be
arbirarily small. If $A$ takes this object, we offer an object of
weight $1$ and no further objects. Clearly an optimal solution can entirely fill
the back, while $A$ only achieved a fill of $eps$. Since $eps$ can be arbirarily
small, the competitive ratio is unbounded, and $A$ is not competitive. If on the
other hand $A$ refuses the element with weight $eps$ then we do not offer any
other objects, and again the competitive ratio is unbounded.&lt;/p>
&lt;p>Clearly, some help is needed. How much help do we need to be optimal? Using $n$
advice bits the online algorithm is trivially optimal: we simply encode for each
item one bit whether to take it or not. It turns out that we also need at least
$n-1$ advice bits to be optimal: Imagine the weight sequence $1/2, 1/4, &amp;hellip;,
1/2^(n-1), wb$. Let $b$ be a bit string of length $n-1$, and define the weight
$wb$ as follows:&lt;/p>
&lt;pre>&lt;code>wb = 1 - sum (i = 1 to n-1) b_i * 1/2^i
&lt;/code>&lt;/pre>
&lt;p>For each of the $2^(n-1)$ possible bitstrings, the weight of $wb$ uniquely
determines&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> which of the previous $n-1$ weights must be taken in order to achieve
the optimal fill of $1$. Thus any optimal algorithm must need at least $n-1$
advice bits to distinguish the $2^(n-1)$ identical prefixes.&lt;/p>
&lt;p>Being optimal however is quite an ambitious goal. Can we do better using fewer
advice bits? It turns out that we can: There exists a 2-competitive algorithm
using only one advice bit. What information do we encode in this one bit? Take a
few minutes to think about this if you wish.&lt;/p>
&lt;p>What we need to know is whether there exists an object with weight at least $1/2$.
If yes, then we simply wait until the first such object occurs and pack it.
Since the optimal solution can pack at most $1$ and we pack at least $1/2$ we
are 2-competitive. Now what if there does not exist such an object, and all
objects weigh less than $1/2$? Either the total sum of weights is less than $1$,
then we can pack all items into the bag greedily and are even optimal. If the
total weight is larger than $1$, then we claim that greedy is 2-competitive.
There must exist at least one element that greedy cannot pack since it overfills
the backpack. But by our assumption this object must weigh less than $1/2$,
meaning that our backpack must contain at least objects of total weight $1/2$,
which is 2-competitive by the same reasoning as in the first case.&lt;/p>
&lt;p>Until now we have only considered advice algorithms, it seems natural to ask how
a randomized algorithm performs. Naturally we expect a randomized algorithm to
perform a bit worse than an advice algorithm given the same number of bits.
Indeed, if we execute the same algorithm as above and guess the advice bit (by
sampling a random bit), then in half the cases we are 2-competitive, which
implies that we are 4-competitive overall.&lt;/p>
&lt;p>However it turns out that there exists a randomized algorithm which is
2-competitive using only one advice bit. I consider this algorithm and its
correctness proof to be one of the most elegant constructions I have seen to
date. Again I urge you to take some time to think about this yourself.&lt;/p>
&lt;p>The 2-competitive randomized online algorithm $A$ works as follows: We have two
algorithms $A1$ and $A2$, and $A$ simply chooses one of them uniformly at
random. Algorithm $A1$ uses a greedy approach: Pack every item that fits until
the backpack is full. We have already learned that this strategy by itself is
not competitive, the beauty comes in combination of $A2$: algorithm $A2$
&lt;em>simulates&lt;/em> $A1$, but does not pack any items. Once $A1$ cannot pack an item for
the first time, $A2$ begins to pack items greedily into its backpack.&lt;/p>
&lt;p>This algorithm is 2-competitive: If the sum of all weights are less than $1$, then
$A1$ is optimal and $A2$ never packs an item - in expectation $A$ achieves half
of the optimal solution and we are done. If the total weight is larger than $1$,
then the sum of weights packed by both algorithms must be greater than $1$, and
again on average we will have packed at least $1/2$ total weight, which makes
$A$ 2-competitive. I find this algorithm and it&amp;rsquo;s correctness proof both
amazingly simple and exceptionally clever, a beautiful combination that
awakes a motivation to delve deeper into this subject matter.&lt;/p>
&lt;p>If you want to learn more about the online knapsack problem then I recommend you
read &lt;a href="https://www.research-collection.ethz.ch/bitstream/handle/20.500.11850/210420/eth-50826-01.pdf">the original
paper&lt;/a>
by Bökenhauer, Komm, Královič and Rossmanith, the section on the simple&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>
knapsack problem is quite accessible even with little theoretical background. We
have barely scratched the surface, and the paper deals with further interesting
problems. If you are interested in online algorithms in general then I
recommend Komm&amp;rsquo;s &lt;a href="https://www.springer.com/gp/book/9783319427478">An Introduction to Online
Computation&lt;/a>, which holds its
promise and is also quite accessible with basic theoretical computer science
knowledge.&lt;/p>
&lt;p>I hope I could convey some of the appreciation I have for this topic and you
learned a little bit about online algorithms. If you have any questions or
comments feel free to reach out to me via my &lt;a href="https://lists.sr.ht/~bfiedler/public-inbox">public
inbox&lt;/a>.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Proving this is a simple but fun exercise in discrete mathematics.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>In the paper this is called the &amp;ldquo;unweighted case&amp;rdquo;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Long monotone trails in graphs</title><link>https://3fx.ch/blog/2019/12/10/long-monotone-trails-in-graphs/</link><pubDate>Tue, 10 Dec 2019 10:10:10 +0100</pubDate><guid>https://3fx.ch/blog/2019/12/10/long-monotone-trails-in-graphs/</guid><description>&lt;p>During a recent dinner discussion I was presented with a beautiful proof to the
following problem: Given an arbitrary graph $G = (V, E)$, and an ordering $\Phi$
of $E$, what is the longest monotone trail in $G$ that is guaranteed to exist?&lt;/p>
&lt;p>A trail in a graph is a sequence of edges such that&lt;/p>
&lt;ul>
&lt;li>adjacent edges share an incident vertex and&lt;/li>
&lt;li>no edge is traversed twice&lt;/li>
&lt;/ul>
&lt;p>Take a few moments to think about this problem before reading the solution, as
it such a simple seeming problem is quite tricky: Clearly there are graphs where
there exists no trail of length greater than zero, namely the empty graph. Thus
the solution must somehow correlate with some graph property.&lt;/p>
&lt;h3 id="the-solution">The solution&lt;/h3>
&lt;p>It turns out that the longest monotone trail has length equal to the average
degree $d$ of $G$. The proof is truly astouding:&lt;/p>
&lt;p>Let $n = |V|$ and $m = |E|$ the number of edges of $G$. Put a
person on each vertex of $G$, and for each edge $e$ in $E$, traversed in order of
$\Phi$, exchange the two people standing at vertices incident to $e$. Each
person will walk a monotone trail on $G$.&lt;/p>
&lt;p>All people together traversed a distance of $2m$ units, and since there are $n$
people the average distance traveled is $2m / n$, which is exactly the
average degree of $G$. By the &lt;a href="https://en.wikipedia.org/wiki/Pigeonhole_principle">pigeonhole
principle&lt;/a> at least one
person must have traveled $2m / n$ units, which concludes our proof.&lt;/p>
&lt;p>It is not difficult to see that this result is best possible. If $G$ is a
matching on $2k$ vertices, then the average degree $d$ in $G$ is $1$, and the
longest monotone trail also has length $1$.&lt;/p></description></item><item><title>'changeme' is valid base64</title><link>https://3fx.ch/blog/2019/12/09/changeme-is-valid-base64/</link><pubDate>Mon, 09 Dec 2019 00:10:58 +0100</pubDate><guid>https://3fx.ch/blog/2019/12/09/changeme-is-valid-base64/</guid><description>&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Base64">Base64-encoding&lt;/a> is ubiquitous in our
modern world. Many programs communicate in base64-encoded messages, since these
have nice properties: They consist of a very limited subset of ASCII characters,
and can thus be displayed as text. E-Mails for example are commonly encoded in
base64 in transfer, which can be seen in the &lt;code>Content-Transfer-Encoding&lt;/code> header.&lt;/p>
&lt;p>Where I volunteer we have a tradition of putting &lt;code>changeme&lt;/code> as a placeholder for
many template variables indicating that they should be overwritten later. Thus,
if we later still see &lt;code>changeme&lt;/code> anywhere we know that somewhere somebody must
have forgotten to specify a variable&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Sometimes these template variables should correspond to base64-encoded content,
i.e. the base64-decoding of said variable should produce meaningful content. But
what happens if somebody forgets to define said variable, and it is substituted
with &lt;code>changeme&lt;/code> instead? I always thought that it would fail to
decode the string since the probability that &lt;code>changeme&lt;/code> is actually valid base64
encoding must be very low. But lo and behold, this is not the case:&lt;/p>
&lt;pre tabindex="0">&lt;code>% echo changeme | base64 -d
r�[1m
&lt;/code>&lt;/pre>&lt;p>Running &lt;code>hexdump&lt;/code> on the output we can identify the characters:&lt;/p>
&lt;pre tabindex="0">&lt;code>% echo changeme | base64 -d | hexdump -C
00000000 72 16 a7 81 e9 9e |r.....|
00000006
&lt;/code>&lt;/pre>&lt;p>Mostly nonsense, but the second byte is interesting: It is an ASCII control
character &lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, meaning trips up all sorts of programs which expect to read
printable ASCII characters, such as JSON deserializers.&lt;/p>
&lt;p>This bug actually occurred when I was migrating some applications between
different Kubernetes clusters: In the old cluster the application ran without
problems, but I forgot to specify some variable in the new cluster, and it lead
to Go complaining about non-ASCII characters in a JSON string.&lt;/p>
&lt;p>I guess the moral of the story is: If you use dummy values be sure that they
cannot be misinterpreted.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>this is not the ideal way to solve this, but we have our historical reasons for choosing this solution.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>specifically &lt;code>Ctrl+V&lt;/code>&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Solving graph problems in bash</title><link>https://3fx.ch/blog/2019/11/22/solving-graph-problems-in-bash/</link><pubDate>Fri, 22 Nov 2019 18:30:40 +0100</pubDate><guid>https://3fx.ch/blog/2019/11/22/solving-graph-problems-in-bash/</guid><description>&lt;p>Many interesting problems in computer science are expressible as problems on
graphs: Finding the shortest path between two vertices, calculating a spanning
tree, finding a large independent set, etc. Often in Theoretical Computer
Science we only look for a theoretical solution - once we know how to model a
specific problem as graph we give an algorithm in pseudocode and leave it at
that. We will try an unconventional method practically solving these problems
using bash.&lt;/p>
&lt;p>At ETH, Computer Science Master students learn how to efficiently (and
practically) solve graph problems in the Algorithms Lab, using
&lt;a href="https://en.wikipedia.org/wiki/C%2B%2B">C++&lt;/a> and
&lt;a href="https://www.boost.org/doc/libs/1_66_0/libs/graph/doc/index.html">BGL&lt;/a>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. In
this course it is not enough to come up with a theoretical algorithm. One also
have to implement and pass testcases as part of his solution. The BGL offers
many sophisticated graph algorithms, each one cleverly optimized in order to
make the generated code as fast as possible. It does its job well: It makes it
(relatively) easy to use sophisticated graph theoretic algorithms in practice.
But it is huge and complicated. Is there maybe an easier way to solve graph
problems on a computer, without having to implement most of the boring
algorithms and without installing a bazillion dependencies?&lt;/p>
&lt;p>It turns out that we can (ab)use just about any Linux system for this, and we do
not even need a C++ compiler, or any other compiler for that matter. All we need
is any run-of-the-mill shell (bash will do just fine), and some standard system
utilities such as &lt;code>find(1)&lt;/code>, &lt;code>tr(1)&lt;/code>, &lt;code>paste(1)&lt;/code>, &lt;code>cut(1)&lt;/code>, etc.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, which (for
GNU/Linux systems) are packaged in the so-called &lt;a href="https://www.gnu.org/software/coreutils/">GNU
&lt;code>coreutils&lt;/code>&lt;/a>. Most other systems
provide similar, if not the same, commands - they may be packaged differently
however.&lt;/p>
&lt;p>We will start with a simple problem: Given an undirected, acyclic and connected
graph, commonly known as &lt;em>tree&lt;/em>, find the unique path between two vertices i and
j of said tree.&lt;/p>
&lt;p>The first problem we have to solve is &lt;em>representing&lt;/em> our graph. Recall from that
there are may possible ways to represent graphs: Adjacency lists, (for each
vertex list all its neighbors), Adjacency matrices (entry (i, j) is 1 iff edge
(i, j) exists), and many more. The most obvious solution would be to put all of
that information in a text file and then operate on that, but then we would be
no further than C++, since we would still have to implement most of the
algorithms by hand. So let us go up a step in the hierarchy: If files are bad,
can we maybe use directories instead?&lt;/p>
&lt;p>It turns out that this works nicely, but only when our graph is a &lt;em>rooted
tree&lt;/em>. In a rooted tree we choose an arbitrary vertex as root and then list all
vertices ordered by their distance from the root: First comes the root itself,
then the root&amp;rsquo;s neighbors, then the root&amp;rsquo;s neighbors&amp;rsquo; neighbors, etc.&lt;/p>
&lt;figure class="invertable resizable">&lt;img src="https://3fx.ch/blog/img/rooted-tree.svg"
alt="A tree with 7 vertices, rooted at vertex 0"/>&lt;figcaption>
&lt;p>A tree with 7 vertices, rooted at vertex 0&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>We can easily translate this tree into a directory structure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p 0/1/3/&lt;span class="o">{&lt;/span>4,5&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir -p 0/2/6
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>How do we display our tree? The best utility I know for this task is aptly named
&lt;code>tree(1)&lt;/code>. Sadly, it is not part of the GNU coreutils and you most likely have
to install it with your favorite package manager. Running &lt;code>tree&lt;/code> then shows us
the tree we constructed:&lt;/p>
&lt;pre tabindex="0">&lt;code>% tree
.
└── 0
├── 1
│   └── 3
│   ├── 4
│   └── 5
└── 2
└── 6
&lt;/code>&lt;/pre>&lt;p>Now we can answer questions such as &amp;ldquo;How can I get from 0 to i?&amp;rdquo;, where i is an
arbitrary vertex other&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> of our tree using the &lt;code>find(1)&lt;/code> command. The &lt;code>find&lt;/code>
command traverses all files in a given directory, and can optionally search for
specific criteria, for example directory names. If we wanted to know how to get
from vertex 0 to vertex 3 we can run the following command:&lt;/p>
&lt;pre tabindex="0">&lt;code>% find 0 -name 3
0/1/3
&lt;/code>&lt;/pre>&lt;p>which tells us that we can go from 0 to 3 via vertex 1. In fact, we can
generalize this to arbitrary vertices i and j: First we check how to get from 0
to i, and then from 0 to j, and finally we have to find the common ancestor of
both paths. Then we reverse the path from 0 to i (now it is a path from i to 0),
walk along it until we hit the common ancestor of i and j, and then walk down
the path from the common ancestor to j.&lt;/p>
&lt;p>The most difficult part of this procedure is finding the common ancestor. Lucky
for us the package &lt;code>diffutils&lt;/code> provides the &lt;code>cmp(1)&lt;/code> command, which can tell us
the first byte at which two files differ. The output of cmp is a bit verbose,
but using &lt;code>cut(1)&lt;/code> and &lt;code>tr(1)&lt;/code> from the coreutils we can extract the
relevant information:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">% cmp &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;testa&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;testb&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/proc/self/fd/11 /proc/self/fd/12 differ: byte 5, line &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">% cmp &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;testa&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;testb&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="p">|&lt;/span> cut -d &lt;span class="s2">&amp;#34; &amp;#34;&lt;/span> -f &lt;span class="m">5&lt;/span> &lt;span class="p">|&lt;/span> tr -cd &lt;span class="s2">&amp;#34;[:digit:]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">5&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>How does this work? The program &lt;code>cut&lt;/code> takes a delimiter (&lt;code>-d&lt;/code>) and some position
number (&lt;code>-f&lt;/code>) and returns the string at the given position (1-indexed) when
splitting on the delimiter specified by &lt;code>-d&lt;/code>. We use &lt;code>tr&lt;/code> to delete (&lt;code>-d&lt;/code>) all
non-digits (&lt;code>-c&lt;/code> stands for the complement of the character set) to ensure that
we get &lt;code>5&lt;/code> as result, and not &lt;code>5,&lt;/code>. So far so good, let us look where we are
now:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># In Bash arguments are passed implicitly and referenced&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># using $1, $2, etc. We use $1 as i and $2 as j&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path_between&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">path_to_i&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>find -name &lt;span class="nv">$1&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">path_to_j&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>find -name &lt;span class="nv">$2&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">first_diff_byte&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>cmp &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$path_to_i&lt;/span>&lt;span class="k">)&lt;/span> &amp;lt;&lt;span class="o">(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$path_to_j&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> cut -d &lt;span class="s2">&amp;#34; &amp;#34;&lt;/span> -f &lt;span class="m">5&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tr -c -d &lt;span class="s1">&amp;#39;[:digit:]&amp;#39;&lt;/span>&lt;span class="o">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>It remains to find the common ancestor of i and j. Since we know where the paths
differ, we know up to which character they are identical. Using &lt;a href="https://www.tldp.org/LDP/abs/html/parameter-substitution.html">bash variable
expansion&lt;/a> we can
identify the common prefix, and using &lt;code>cut&lt;/code> extract the vertex they have in
common. The &lt;code>rev&lt;/code> command from the &lt;code>util-linux&lt;/code> package reverses a string of
characters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">common_prefix&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">path_to_i&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nv">0&lt;/span>&lt;span class="p">:(( &lt;/span>&lt;span class="nv">$first_diff_byte&lt;/span>&lt;span class="p"> - 1 ))&lt;/span>&lt;span class="si">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">common_ancestor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> -n &lt;span class="nv">$common_prefix&lt;/span> &lt;span class="p">|&lt;/span> rev &lt;span class="p">|&lt;/span> cut -d / -f &lt;span class="m">2&lt;/span> &lt;span class="p">|&lt;/span> rev&lt;span class="k">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>The part in the double parenthesis is subject to the &lt;code>expr(1)&lt;/code> command&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>, which
and evaluates the expression as arithmetic expression. Now all we have left to
do is to stick all of it together:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># if we omit local here we set $PATH...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="si">${&lt;/span>&lt;span class="nv">path_to_i&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$common_prefix&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span> &lt;span class="s1">$&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tac &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> paste -s -d &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$path&lt;/span>/&lt;span class="nv">$common_ancestor&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">path_to_j&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$common_prefix&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Using the &lt;code>tr&lt;/code>, &lt;code>tac&lt;/code> and &lt;code>paste&lt;/code> commands (all part of coreutils) we can
reverse the path from i to the common ancestor of i and j. We do this by
changing the slashes which delimit our vertices to newlines and using
&lt;code>tac&lt;/code>&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> to reverse the order of our vertices. Finally
we can use &lt;code>paste&lt;/code> to put the reversed vertices back together in a single string
and then append the path from the common ancestor to j.&lt;/p>
&lt;p>Testing it on our input graph gives the following output:&lt;/p>
&lt;pre tabindex="0">&lt;code>% path_between 4 5
4/3/5
% path_between 5 4
5/3/4
% path_between 0 5
/.//1/3/5
% path_between 3 4
/1//4
&lt;/code>&lt;/pre>&lt;p>It seems like it fails when the common ancestor is either i or j. We can remedy
this by handling these cases separately: If i is the common ancestor, we can
simply take the path from i to j minus the path from 0 to the ancestor of i.
Else if j is the common ancestor then we have to first reverse the path from j
to i (so we get the path from i to j) and the remove the path to j&amp;rsquo;s ancestor.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="nv">$common_prefix&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nv">$path_to_i&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">path_to_j&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$common_prefix&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">elif&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="nv">$common_prefix&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nv">$path_to_j&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="si">${&lt;/span>&lt;span class="nv">path_to_i&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$common_prefix&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span> &lt;span class="s1">$&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tac &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> paste -s -d /&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$path$2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Do what we did before&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Now it gives correct results for any query:&lt;/p>
&lt;pre tabindex="0">&lt;code>% path_between 3 4
3/4
% path_between 4 3
4/3
% path_between 6 4
6/2/0/1/3/4
% path_between 0 1
0/1
% path_between 1 0
1/0
&lt;/code>&lt;/pre>&lt;p>That&amp;rsquo;s it. In 20 lines of bash we wrote a pathfinding algorithm for trees. Not
only that, but we did it without needing to compile any code, only using tools
available on most Linux systems! I find that quite impressive.&lt;/p>
&lt;h3 id="pathfinding-in-general-graphs">Pathfinding in general graphs&lt;/h3>
&lt;p>Restricting ourselves to rooted trees sounds pretty limiting doesn&amp;rsquo;t it? Can we
generalize our model to arbitrary graphs? At first it seems like we cannot,
since then our file system (which is where we store our graph) does not support
cycles. After all, no directory can be a (grand-)parent of itself. But it turns
out that using &lt;a href="https://en.wikipedia.org/wiki/Symbolic_link">symbolic links&lt;/a> we
can &amp;ldquo;point&amp;rdquo; to other directories, which can in turn point to other directories,
which can point to other directories, etc. We can create symlinks using the &lt;code>ln&lt;/code>
command (incidentally also found in coreutils), by writing &lt;code>ln -s &amp;lt;directory point to&amp;gt; &amp;lt;name of the link&amp;gt;&lt;/code>. For each vertex we want to create a directory and
in that directory create symbolic links to all of its neighbors. If you recall
the different forms of graph representation before this corresponds to adjacency
lists. Let us try this with a simple example.&lt;/p>
&lt;figure class="invertable resizable">&lt;img src="https://3fx.ch/blog/img/graph.svg"
alt="A triangle with a three-armed star, connected via one arm"/>&lt;figcaption>
&lt;p>A triangle with a three-armed star, connected via one arm&lt;/p>
&lt;/figcaption>
&lt;/figure>
&lt;p>We can create this graph as follows:&lt;/p>
&lt;pre tabindex="0">&lt;code>% mkdir `seq 0 5`
% ln -s ../0 1
% ln -s ../1 0
% ln -s ../2 0
% ln -s ../0 2
% &amp;lt;continue for each edge&amp;gt;
% tree
.
├── 0
│   ├── 1 -&amp;gt; ../1
│   └── 2 -&amp;gt; ../2
├── 1
│   ├── 0 -&amp;gt; ../0
│   └── 2 -&amp;gt; ../2
├── 2
│   ├── 0 -&amp;gt; ../0
│   ├── 1 -&amp;gt; ../1
│   └── 4 -&amp;gt; ../4
├── 3
│   └── 4 -&amp;gt; ../4
├── 4
│   ├── 2 -&amp;gt; ../2
│   ├── 3 -&amp;gt; ../3
│   └── 5 -&amp;gt; ../5
└── 5   
└── 4 -&amp;gt; ../4
&lt;/code>&lt;/pre>&lt;p>Now let us look at the same problem as before: We want to specify two vertices i
and j, and want to find a path between them (if it exists). Last time we used
the &lt;code>find(1)&lt;/code> utility to find these. This time we will explicitly tell &lt;code>find&lt;/code> in
which vertex i we would like to start by specifying it as the root of our
search:&lt;/p>
&lt;pre tabindex="0">&lt;code>% find 0/ -name 1
0/1
% find 0/ -name 3
&amp;lt;nothing&amp;gt;
&lt;/code>&lt;/pre>&lt;p>Hmm, it seems as if &lt;code>find&lt;/code> does not follow the symbolic links. Looking at
&lt;code>find&lt;/code>&amp;rsquo;s manual page (&lt;code>man find&lt;/code>) we see the following&lt;/p>
&lt;pre tabindex="0">&lt;code>OPTIONS The -H, -L and -P options control the treatment of symbolic links.
[...]
-P Never follow symbolic links. This is the default behaviour. When
find examines or prints information a file, and the file is a sym‐
bolic link, the information used shall be taken from the properties
of the symbolic link itself.
-L Follow symbolic links. When find examines or prints information
about files, the information used shall be taken from the properties
of the file to which the link points, not from the link itself (un‐
less it is a broken symbolic link or find is unable to examine the
file to which the link points). Use of this option implies -noleaf.
If you later use the -P option, -noleaf will still be in effect. If
-L is in effect and find discovers a symbolic link to a subdirectory
during its search, the subdirectory pointed to by the symbolic link
will be searched.
&lt;/code>&lt;/pre>&lt;p>Aha, we have to specify &lt;code>find -L&lt;/code> if we want to follow symlinks. But our
symlinks induce cycles in the file system, what happens if &lt;code>find&lt;/code> encounters a
cycle? Again the manpage has the answer for us:&lt;/p>
&lt;pre tabindex="0">&lt;code>The POSIX standard requires that find detects loops:
The find utility shall detect infinite loops; that is, entering a
previously visited directory that is an ancestor of the last file en‐
countered. When it detects an infinite loop, find shall write a di‐
agnostic message to standard error and shall either recover its posi‐
tion in the hierarchy or terminate.
&lt;/code>&lt;/pre>&lt;p>So we are good to go!&lt;/p>
&lt;pre tabindex="0">&lt;code>% find -L 0/ -name 3
find: File system loop detected; ‘0/2/4/5/4’ is part of the same file system loop as ‘0/2/4’.
0/2/4/3
find: File system loop detected; ‘0/2/4/3/4’ is part of the same file system loop as ‘0/2/4’.
find: File system loop detected; ‘0/2/4/2’ is part of the same file system loop as ‘0/2’.
find: File system loop detected; ‘0/2/1/2’ is part of the same file system loop as ‘0/2’.
find: File system loop detected; ‘0/2/1/0’ is part of the same file system loop as ‘0/’.
find: File system loop detected; ‘0/2/0’ is part of the same file system loop as ‘0/’.
find: File system loop detected; ‘0/1/2/4/5/4’ is part of the same file system loop as ‘0/1/2/4’.
0/1/2/4/3
find: File system loop detected; ‘0/1/2/4/3/4’ is part of the same file system loop as ‘0/1/2/4’.
find: File system loop detected; ‘0/1/2/4/2’ is part of the same file system loop as ‘0/1/2’.
find: File system loop detected; ‘0/1/2/1’ is part of the same file system loop as ‘0/1’.
find: File system loop detected; ‘0/1/2/0’ is part of the same file system loop as ‘0/’.
find: File system loop detected; ‘0/1/0’ is part of the same file system loop as ‘0/’.
&lt;/code>&lt;/pre>&lt;p>We get two paths and a whole lot of errors that find detected a loop. We can
just ignore these:&lt;/p>
&lt;pre tabindex="0">&lt;code>% find 0/ -name 3 2&amp;gt;/dev/null
0/2/4/3
0/1/2/4/3
&lt;/code>&lt;/pre>&lt;p>Perfect! Now the find command returns all the paths between 0 and 3 in the
graph. And it is much simpler than our previous solution: &lt;code>find&lt;/code> does all the
work for us.&lt;/p>
&lt;p>This approach can easily be extended to directed graphs by only symlinking in
the direction the edge goes, which means that we can find all paths between
vertices i and j in any graph in &lt;em>a single line of bash&lt;/em>. That&amp;rsquo;s pretty amazing,
don&amp;rsquo;t you think? We can even improve the solution a bit: What if we only want &lt;em>a&lt;/em>
path, and not all paths? The manual page of &lt;code>find&lt;/code> once again has the answer:&lt;/p>
&lt;pre tabindex="0">&lt;code>-quit Exit immediately. No child processes will be left running, but no
more paths specified on the command line will be processed. For ex‐
ample, find /tmp/foo /tmp/bar -print -quit will print only /tmp/foo.
Any command lines which have been built up with -execdir ... {} +
will be invoked before find exits. The exit status may or may not be
zero, depending on whether an error has already occurred.
[...]
-print True; print the full file name on the standard output, followed by a
newline.
&lt;/code>&lt;/pre>&lt;p>If we specify &lt;code>-print&lt;/code> and &lt;code>-quit&lt;/code> the &lt;code>find&lt;/code> command will immediately exit when
it finds the first path:&lt;/p>
&lt;pre tabindex="0">&lt;code>% find -L 0/ -name 3 -print -quit 2&amp;gt;/dev/null
0/2/4/3
&lt;/code>&lt;/pre>&lt;p>This works on any graph, directed or undirected, and will print a path if it
finds one. Since &lt;code>find&lt;/code> will never continue running if it found a loop this is
successful even if no paths exist between the given vertices:&lt;/p>
&lt;pre tabindex="0">&lt;code>% find -L 0/ -name 6 -print -quit 2&amp;gt;/dev/null
&amp;lt;nothing&amp;gt;
&lt;/code>&lt;/pre>&lt;p>Note that &lt;code>find&lt;/code> operates by &lt;a href="https://en.wikipedia.org/wiki/Depth-first_search">depth first
search&lt;/a>, and thus might not
find the shortest path between two vertices. The last thing we will show is how
to (ab)use the &lt;code>mindepth&lt;/code> and &lt;code>maxdepth&lt;/code> options in order to force find to do a
&lt;a href="https://en.wikipedia.org/wiki/Breadth-first_search">breadth first search&lt;/a>,
which always finds the shortest path in unweighted graphs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">find_shortest&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">i&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">p&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="o">[[&lt;/span> -z &lt;span class="nv">$p&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">p&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>find -L &lt;span class="nv">$1&lt;/span> -mindepth &lt;span class="nv">$i&lt;/span> -maxdepth &lt;span class="nv">$i&lt;/span> -name &lt;span class="nv">$2&lt;/span> -print -quit 2&amp;gt;/dev/null&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">i&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span> i+1 &lt;span class="k">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="nv">$p&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">% find_shortest &lt;span class="m">0&lt;/span> &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">0/2/4/3&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>While we have not found a path, we tell find to extend its search radius by one,
and once we found our desired path we output it. This approach is guaranteed to
always return the shortest path, although it is computationally more expensive
since we often traverse the same parts, and it also won&amp;rsquo;t detect if no path
exists between these vertices. We could extend this program to add termination
detection, however then it would lose its elegance, which is what I wanted to
show in this post.&lt;/p>
&lt;h3 id="closing-remarks">Closing remarks&lt;/h3>
&lt;p>Interestingly enough, somebody &lt;a href="https://github.com/tavianator/bfs">actually implemented
&lt;code>bfs(1)&lt;/code>&lt;/a>, which behaves like &lt;code>find&lt;/code> and
internally uses breadth-first search, but it is not (and probably will never) be
part of coreutils.&lt;/p>
&lt;p>I did use this algorithm in one of my courses to solve small pathfinding
problems&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>. Implementing this was quite fun
and not as much pain as I&amp;rsquo;d feared. Sometimes these `clever&amp;rsquo; solutions have their
merit - although for more sophicated problems I&amp;rsquo;d rather stick to C++ and BGL.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>short for Boost Graph Library&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>The numbers in parenthesis correspond to the man page section of the programs. Most programs have their manpage in section 1, which is for user commands, If you are interested in more details or the other sections &lt;code>man man&lt;/code> gives great insight.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Actually the case where we input the same number twice is tedious since our solution depends on the paths being different, as you will see later.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Also a part of the coreutils package&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>Like &lt;code>cat&lt;/code> but reverses the order of the lines&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>and so far I haven&amp;rsquo;t been kicked out&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item></channel></rss>