1086 lines
50 KiB
HTML
1086 lines
50 KiB
HTML
<!DOCTYPE html>
|
|
<html class="has-newsletter-footer" lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="HandheldFriendly" content="True">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<meta name="theme-color" content="#f5f5f4">
|
|
<meta name="referrer" content="no-referrer-when-downgrade">
|
|
<meta name="format-detection" content="telephone=no">
|
|
<link rel="me" href="https://octodon.social/@samwho">
|
|
<link rel="me" href="https://hachyderm.io/@samwho">
|
|
|
|
<title>Hashing</title>
|
|
|
|
<link rel="stylesheet" href="/css/reset.css?h=6f7861e8f1e8cd8577de">
|
|
<link rel="stylesheet" href="/giallo.css?h=314b13baed710ac3980f">
|
|
<link rel="stylesheet" href="/css/main.css?h=c23105589554dab2cb41">
|
|
|
|
|
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml">
|
|
|
|
|
|
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:creator" content="@samwhoo" />
|
|
|
|
<meta name="twitter:image" content="/images/hashing-card.png?h=08ea469310c79706ee33" />
|
|
<meta property="og:image" content="/images/hashing-card.png?h=08ea469310c79706ee33" />
|
|
|
|
|
|
<meta name="twitter:description" content="A visual, interactive introduction to hash functions.">
|
|
<meta property="og:description" content="A visual, interactive introduction to hash functions.">
|
|
<meta name="description" content="A visual, interactive introduction to hash functions.">
|
|
|
|
<meta name="twitter:title" content="Hashing" />
|
|
<meta property="og:title" content="Hashing" />
|
|
|
|
|
|
<meta property="og:type" content="article" />
|
|
<meta property="og:published_date" content="2023-05-24" />
|
|
<meta property="og:author" content="Sam Rose" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script src="/js/hashing/out/index.js?h=c888d2256024de14aa33"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
const url = new URL(window.location.href);
|
|
const params = url.searchParams;
|
|
const theme = params.get("theme") === "dark" ? "dark" : "light";
|
|
document.documentElement.dataset.theme = theme;
|
|
const hasNewsletterFooter =
|
|
document.documentElement.classList.contains("has-newsletter-footer");
|
|
document.querySelector('meta[name="theme-color"]').content =
|
|
theme === "dark"
|
|
? hasNewsletterFooter ? "#292524" : "#0c0a09"
|
|
: hasNewsletterFooter ? "#f5f5f4" : "#ffffff";
|
|
</script>
|
|
|
|
|
|
|
|
</head>
|
|
|
|
<body>
|
|
<div class="page-canvas">
|
|
|
|
<div class="bar" role="presentation">
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<a href="#content" class="skip-link">Skip to main content</a>
|
|
|
|
<header class="post-header">
|
|
<a href="/">
|
|
<img class="hero" alt="Samwho home" src="/images/samwho-keyslogo.svg" width="750" height="700" fetchpriority="high" />
|
|
</a>
|
|
<h1>Hashing</h1>
|
|
<div class="post-meta">
|
|
<time datetime="2023-05-24">2023-05-24</time>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="content" tabindex="-1">
|
|
<article class="post">
|
|
|
|
<div class="content">
|
|
<style>
|
|
form {
|
|
padding-top: 0.5em;
|
|
padding-left: 0.5em;
|
|
padding-right: 0.5em;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.3em;
|
|
}
|
|
|
|
form input[type=text] {
|
|
flex: 4 1 auto;
|
|
min-width: 0;
|
|
border-radius: 0.3em;
|
|
border: 1px solid #aaaaaa;
|
|
padding: 0.3em;
|
|
}
|
|
|
|
form button {
|
|
flex: 1 1 auto;
|
|
max-width: 140px;
|
|
}
|
|
|
|
form button:disabled {
|
|
opacity: 0.5 !important;
|
|
}
|
|
|
|
form button.add {
|
|
background-color: #009E73;
|
|
color: white;
|
|
border: 0;
|
|
border-radius: 0.3em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
form button.check {
|
|
background-color: #56B4E9;
|
|
color: white;
|
|
border: 0;
|
|
border-radius: 0.3em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
form button.clear {
|
|
background-color: #D55E00;
|
|
color: white;
|
|
border: 0;
|
|
border-radius: 0.3em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.grid-2x2 {
|
|
display: "grid";
|
|
}
|
|
|
|
.grid {
|
|
user-select: none;
|
|
cursor: pointer;
|
|
margin-top: 1rem;
|
|
margin-bottom: 1rem;
|
|
border: 1px solid #009E73;
|
|
width: 100%;
|
|
display: grid;
|
|
grid-template-columns: repeat(8, 1fr);
|
|
grid-template-rows: repeat(2, 1fr);
|
|
}
|
|
|
|
.grid-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
aspect-ratio: 1/1;
|
|
}
|
|
|
|
.grid-active {
|
|
background-color: #009E73;
|
|
color: white;
|
|
}
|
|
|
|
.above-grid {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.hash-examples {
|
|
padding-top: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
margin: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.hash-examples div {
|
|
margin: auto;
|
|
}
|
|
|
|
.hash-examples code {
|
|
display: block;
|
|
white-space: pre;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.hash-examples p {
|
|
font-size: 0.75rem;
|
|
font-style: italic;
|
|
text-align: center;
|
|
font-family: var(--heading-font);
|
|
width: 75%;
|
|
}
|
|
|
|
.blob {
|
|
cursor: pointer;
|
|
background: #CC79A7;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: 1.5rem;
|
|
color: white;
|
|
border-radius: 50%;
|
|
margin: 10px;
|
|
height: 3rem;
|
|
width: 3rem;
|
|
min-width: 3rem;
|
|
max-width: 3rem;
|
|
|
|
box-shadow: 0 0 0 0 #CC79A7FF;
|
|
transform: scale(1);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
transform: scale(0.85);
|
|
box-shadow: 0 0 0 0 #CC79A77F;
|
|
}
|
|
|
|
70% {
|
|
transform: scale(1);
|
|
box-shadow: 0 0 0 1rem rgba(0, 0, 0, 0);
|
|
}
|
|
|
|
100% {
|
|
transform: scale(0.85);
|
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
|
}
|
|
}
|
|
|
|
.blob-click {
|
|
cursor: default;
|
|
animation: tick 1s linear;
|
|
background: #009E73FF;
|
|
}
|
|
|
|
@keyframes tick {
|
|
0% {
|
|
transform: scale(1);
|
|
box-shadow: 0 0 0 0 #009E73FF;
|
|
}
|
|
|
|
50% {
|
|
box-shadow: 0 0 0 1rem #009E737F;
|
|
}
|
|
|
|
100% {
|
|
box-shadow: 0 0 0 2rem #009E7300;
|
|
}
|
|
}
|
|
|
|
.aside {
|
|
padding: 2rem;
|
|
width: 100vw;
|
|
position: relative;
|
|
margin-left: -50vw;
|
|
left: 50%;
|
|
background-color: #eeeeee;
|
|
|
|
display: flex;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.aside > * {
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.aside p {
|
|
padding-left: 1rem;
|
|
padding-right: 1rem;
|
|
max-width: 780px;
|
|
font-style: italic;
|
|
font-family: var(--heading-font);
|
|
text-align: center;
|
|
}
|
|
|
|
.pct25 {
|
|
width: 100%;
|
|
height: 200px;
|
|
}
|
|
</style>
|
|
<noscript>
|
|
<div class=aside>
|
|
<p>
|
|
This page makes heavy use of JavaScript to visualise the concepts discussed.
|
|
Viewing it without JavaScript will be a strange experience, as the text
|
|
talks about the visualisations. I strongly recommend either enabling
|
|
JavaScript, or not wasting your time.
|
|
</p>
|
|
</div>
|
|
</noscript>
|
|
<p>As a programmer, you use hash functions every day. They're used in databases
|
|
to optimise queries, they're used in data structures to make things faster,
|
|
they're used in security to keep data safe. Almost every interaction you have
|
|
with technology will involve hash functions in one way or another.</p>
|
|
<p>Hash functions are foundational, and they are <strong>everywhere</strong>.</p>
|
|
<p>But what <em>is</em> a hash function, and how do they work?</p>
|
|
<p>In this post, we're going to demystify hash functions. We're going to start by
|
|
looking at a simple hash function, then we're going to learn how to test if a
|
|
hash function is good or not, and then we're going to look at a real-world use
|
|
of hash functions: the hash map.</p>
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
var blob = document.querySelector(".blob");
|
|
blob.addEventListener("click", function () {
|
|
blob.classList.add("blob-click");
|
|
blob.innerText = "✓";
|
|
});
|
|
});
|
|
</script>
|
|
<div class=aside>
|
|
<div class=blob>
|
|
</div>
|
|
<p>
|
|
This article has visualisations that can be <span class="purple
|
|
bold">clicked</span>.
|
|
</p>
|
|
</div>
|
|
<h2 id="what-is-a-hash-function"><a class="anchor" href="#what-is-a-hash-function" aria-label="Link to this section">#</a>
|
|
What <em>is</em> a hash function?</h2>
|
|
<p>Hash functions are functions that take an input, usually a string, and produce a
|
|
number. If you were to call a hash function multiple times with the same input,
|
|
it will always return the same number, and that number returned will always be
|
|
within a promised range. What that range is will depend on the hash function,
|
|
some use 32-bit integers (so 0 to 4 billion), others go much larger.</p>
|
|
<p>If we were to write a dummy hash function in JavaScript, it might look like
|
|
this:</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">function</span><span class="z-5"> hash</span><span>(</span><span class="z-14 z-i">input</span><span>) {</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span class="z-6"> 0</span><span>;</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>Even without knowing <em>how</em> hash functions are used, it's probably no surprise
|
|
that this hash function is useless. Let's see how we can measure how good a
|
|
hash function is, and after that we'll do a deep dive on how they're used
|
|
within hash maps.</p>
|
|
<h2 id="what-makes-a-hash-function-good"><a class="anchor" href="#what-makes-a-hash-function-good" aria-label="Link to this section">#</a>
|
|
What makes a hash function good?</h2>
|
|
<p>Because <code>input</code> can be any string, but the number returned is within some
|
|
promised range, it's possible that two different inputs can return the same
|
|
number. This is called a "collision," and good hash functions try to minimise
|
|
how many collisions they produce.</p>
|
|
<p>It's not possible to completely eliminate collisions, though. If we wrote a hash
|
|
function that returned a number in the range 0 to 7, and we gave it 9 unique
|
|
inputs, we're guaranteed at least 1 collision.</p>
|
|
<div class="hash-examples">
|
|
<div>
|
|
<code>hash("to") == 3</code>
|
|
<code>hash("the") == 2</code>
|
|
<code>hash("café") == 0</code>
|
|
<code>hash("de") == 6</code>
|
|
<code>hash("versailles") == 4</code>
|
|
<code>hash("for") == 5</code>
|
|
<code>hash("coffee") == 0</code>
|
|
<code>hash("we") == 7</code>
|
|
<code>hash("had") == 1</code>
|
|
</div>
|
|
<p>
|
|
Output values from a well-known hash function, modulo 8. No matter what 9
|
|
values we pass, there are only 8 unique numbers and so collisions are
|
|
inevitable. The goal is to have as few as possible.
|
|
</p>
|
|
</div>
|
|
<p>To visualise collisions, I'm going to use a grid. Each square of the grid is
|
|
going to represent a number output by a hash function. Here's an example 8x2
|
|
grid. <span class="purple bold">Click</span> on the grid to increment the
|
|
example hash output value and see how we map it to a grid square. See what
|
|
happens when you get a number larger than the number of grid squares.</p>
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
let grid = document.getElementById("first-grid");
|
|
let hash = document.getElementById("grid-hash");
|
|
let modulo = document.getElementById("grid-modulo");
|
|
grid.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
let number = parseInt(hash.innerText) + 1;
|
|
hash.innerText = number.toString();
|
|
modulo.innerText = (number % 16).toString();
|
|
|
|
grid.querySelector(".grid-active").classList.remove("grid-active");
|
|
grid.children[number % 16].classList.add("grid-active");
|
|
return false;
|
|
});
|
|
});
|
|
</script>
|
|
<div class=above-grid>
|
|
<code style="color: #009E73; font-weight: bold;">
|
|
<span id="grid-hash">13</span> % 16 == <span id="grid-modulo">13</span>
|
|
</code>
|
|
</div>
|
|
<div class="grid" id="first-grid">
|
|
<div class="grid-item">0</div>
|
|
<div class="grid-item">1</div>
|
|
<div class="grid-item">2</div>
|
|
<div class="grid-item">3</div>
|
|
<div class="grid-item">4</div>
|
|
<div class="grid-item">5</div>
|
|
<div class="grid-item">6</div>
|
|
<div class="grid-item">7</div>
|
|
<div class="grid-item">8</div>
|
|
<div class="grid-item">9</div>
|
|
<div class="grid-item">10</div>
|
|
<div class="grid-item">11</div>
|
|
<div class="grid-item">12</div>
|
|
<div class="grid-item grid-active">13</div>
|
|
<div class="grid-item">14</div>
|
|
<div class="grid-item">15</div>
|
|
</div>
|
|
<p>Every time we hash a value, we're going to make its corresponding square on the
|
|
grid a bit darker. The idea is to create an easy way to see how well a hash
|
|
function avoids collisions. What we're looking for is a nice, even distribution.
|
|
We'll know that the hash function isn't good if we have clumps or patterns of
|
|
dark squares.</p>
|
|
<blockquote class="haskie">
|
|
<img src="/images/haskie-confused-200px.png" />
|
|
<p>
|
|
You said that when a hash function outputs the same value for 2 different
|
|
inputs, that's a collision. But if we have a hash function that outputs values
|
|
in a big range, and we mapped those to a small grid, aren't we going to create
|
|
lots of collisions on the grid that aren't actually collisions? On our 8x2
|
|
grid, 1 and 17 both map to the 2nd square.
|
|
</p>
|
|
</blockquote>
|
|
<p>This is a great observation. You're absolutely right, we're going to be creating
|
|
"pseudo-collisions" on our grid. It's okay, though, because if the hash function
|
|
is good we will still see an even distribution. Incrementing every square by 100
|
|
is just as good a distribution as incrementing every square by 1. If we have a
|
|
bad hash function that collides a lot, that will still stand out. We'll see
|
|
this shortly.</p>
|
|
<p>Let's take a larger grid and hash 1,000 randomly-generated strings. You can
|
|
<span class="purple bold">click</span> on the grid to hash a new set of random
|
|
inputs, and the grid will animate to show you each input being hashed and placed
|
|
on the grid.</p>
|
|
<p><heat-map
|
|
class="pct25"
|
|
iterations=1000
|
|
blockSize=20
|
|
valueFn=randomUUID
|
|
hashFn=murmur3
|
|
color=green>
|
|
</heat-map></p>
|
|
<p>The values are nice and evenly distributed because we're using a good,
|
|
well-known hash function called <code class="green">murmur3</code>. This hash
|
|
is widely used in the real-world because it has great distribution while also
|
|
being really, really fast.</p>
|
|
<p>What would our grid look like if we used a <em>bad</em> hash function?</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">function</span><span class="z-5"> hash</span><span>(</span><span class="z-14 z-i">input</span><span>) {</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> hash</span><span class="z-13"> =</span><span class="z-6"> 0</span><span>;</span></span>
|
|
<span class="giallo-l"><span class="z-13"> for</span><span> (</span><span class="z-1 z-i">let</span><span> c</span><span class="z-13"> of</span><span> input) {</span></span>
|
|
<span class="giallo-l"><span> hash</span><span class="z-13"> +=</span><span> c.</span><span class="z-5">charCodeAt</span><span>(</span><span class="z-6">0</span><span>);</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span> hash</span><span class="z-13"> %</span><span class="z-6"> 1000000</span><span>;</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>This hash function loops through the string that we're given and sums the
|
|
numeric values of each character. It then makes sure that the value is between 0
|
|
and 1000000 by using the modulus operator (<code>%</code>). Let's call this hash function
|
|
<code class="red">stringSum</code>.</p>
|
|
<p>Here it is on the grid. Reminder, this is 1,000 randomly generated strings that
|
|
we're hashing.</p>
|
|
<p><heat-map
|
|
class="pct25"
|
|
iterations=1000
|
|
blockSize=20
|
|
valueFn=randomUUID
|
|
hashFn=stringSum
|
|
color=red>
|
|
</heat-map></p>
|
|
<p>This doesn't look all that different from <code class="green">murmur3</code>.
|
|
What gives?</p>
|
|
<p>The problem is that the strings we're giving to be hashed are random. Let's see
|
|
how each function performs when given input that is not random: the numbers from
|
|
1 to 1000 converted to strings.</p>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: space-between; padding-top: 1rem; padding-bottom: 1rem">
|
|
<heat-map
|
|
style="flex-grow: 1; width: 40%; min-width: 200px; aspect-ratio: 2/1;"
|
|
iterations=1000
|
|
blockSize=10
|
|
valueFn=intToStr
|
|
hashFn=murmur3
|
|
color=green>
|
|
</heat-map>
|
|
<heat-map
|
|
style="flex-grow: 1; width: 40%; min-width: 200px; aspect-ratio: 2/1;"
|
|
iterations=1000
|
|
blockSize=10
|
|
valueFn=intToStr
|
|
hashFn=stringSum
|
|
color=red>
|
|
</heat-map>
|
|
</div>
|
|
<p>Now the problem is more clear. When the input isn't random, the output of <code
|
|
class="red">stringSum</code> forms a pattern. Our <code
|
|
class="green">murmur3</code> grid, however, looks the same as how it looked with
|
|
random values.</p>
|
|
<p>How about if we hash the <a rel="external" href="https://github.com/powerlanguage/word-lists/blob/master/1000-most-common-words.txt">top 1,000 most common English words</a>:</p>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: space-between; padding-top: 1rem; padding-bottom: 1rem">
|
|
<heat-map
|
|
style="flex-grow: 1; width: 40%; min-width: 200px; aspect-ratio: 2/1;"
|
|
iterations=1000
|
|
blockSize=10
|
|
valueFn=commonWords
|
|
hashFn=murmur3
|
|
color=green>
|
|
</heat-map>
|
|
<heat-map
|
|
style="flex-grow: 1; width: 40%; min-width: 200px; aspect-ratio: 2/1;"
|
|
iterations=1000
|
|
blockSize=10
|
|
valueFn=commonWords
|
|
hashFn=stringSum
|
|
color=red>
|
|
</heat-map>
|
|
</div>
|
|
<p>It's more subtle, but we do see a pattern on the <code class="bold
|
|
red">stringSum</code> grid. As usual, <code class="green bold">murmur3</code>
|
|
looks the same as it always does.</p>
|
|
<p>This is the power of a good hash function: no matter the input,
|
|
the output is evenly distributed. Let's talk about one more way to visualise
|
|
this and then talk about why it matters.</p>
|
|
<h3 id="the-avalanche-effect"><a class="anchor" href="#the-avalanche-effect" aria-label="Link to this section">#</a>
|
|
The avalanche effect</h3>
|
|
<p>Another way hash functions get evaluated is on something called the "avalanche
|
|
effect." This refers to how many bits in the output value change when just a
|
|
single bit of the input changes. To say that a hash function has a good
|
|
avalanche effect, a single bit flip in the input should result in an average of
|
|
50% the output bits flipping.</p>
|
|
<p>It's this property that helps hash functions avoid forming patterns in the grid.
|
|
If small changes in the input result in small changes in the output, you get
|
|
patterns. Patterns indicate poor distribution, and a higher rate of collisions.</p>
|
|
<p>Below, we are visualising the avalanche effect by showing two 8-bit binary
|
|
numbers. The top number is the input value, and the bottom number is the <code
|
|
class="green bold">murmur3</code> output value. <span class="purple
|
|
bold">Click</span> on it to <span class="purple bold">flip a single bit</span>
|
|
in the input. Bits that change in the output will be <span class="green
|
|
bold">green</span>, bits that stay the same will be <span class="red
|
|
bold">red</span>.</p>
|
|
<p><avalanche-effect
|
|
style="width: 100%; height: 200px;"
|
|
hashFn="murmur3">
|
|
</avalanche-effect></p>
|
|
<p><span class="green bold">murmur3</span> does well, though you will notice that
|
|
sometimes fewer than 50% of the bits flip and sometimes more. This is okay,
|
|
provided that it is 50% on average.</p>
|
|
<p>Let's see how <span class="bold red">stringSum</span> performs.</p>
|
|
<p><avalanche-effect
|
|
style="width: 100%; height: 200px;"
|
|
hashFn="stringSum">
|
|
</avalanche-effect></p>
|
|
<p>Well this is embarassing. The output is equal to the input, and so only a single
|
|
bit flips each time. This does make sense, because <span class="bold
|
|
red">stringSum</span> just sums the numeric value of each character in the
|
|
string. This example only hashes the equivalent of a single character, which
|
|
means the output will always be the same as the input.</p>
|
|
<h2 id="why-all-of-this-matters"><a class="anchor" href="#why-all-of-this-matters" aria-label="Link to this section">#</a>
|
|
Why all of this matters</h2>
|
|
<p>We've taken the time to understand some of the ways to determine if a hash
|
|
function is good, but we've not spent any time talking about why it matters.
|
|
Let's fix that by talking about hash maps.</p>
|
|
<p>To understand hash maps, we first must understand what a map is. A map is a data
|
|
structure that allows you to store key-value pairs. Here's an example in
|
|
JavaScript:</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">let</span><span> map</span><span class="z-13"> = new</span><span class="z-5"> Map</span><span>();</span></span>
|
|
<span class="giallo-l"><span>map.</span><span class="z-5">set</span><span>(</span><span class="z-11">"hello"</span><span>,</span><span class="z-11"> "world"</span><span>);</span></span>
|
|
<span class="giallo-l"><span>console.</span><span class="z-5">log</span><span>(map.</span><span class="z-5">get</span><span>(</span><span class="z-11">"hello"</span><span>));</span></span></code></pre>
|
|
<p>Here we take a key-value pair (<code>"hello"</code> → <code>"world"</code>) and store it in the map.
|
|
Then we print out the value associated with the key <code>"hello"</code>, which will be
|
|
<code>"world"</code>.</p>
|
|
<p>A more fun real-world use-case would be to find anagrams. An anagram is when two
|
|
different words contain the same letters, for example "antlers" and "rentals"
|
|
or "article" and "recital." If you have a list of words and you want to find
|
|
all of the anagrams, you can sort the letters in each word alphabetically and
|
|
use that as a key in a map.</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">let</span><span> words</span><span class="z-13"> =</span><span> [</span></span>
|
|
<span class="giallo-l"><span class="z-11"> "antlers"</span><span>,</span></span>
|
|
<span class="giallo-l"><span class="z-11"> "rentals"</span><span>,</span></span>
|
|
<span class="giallo-l"><span class="z-11"> "sternal"</span><span>,</span></span>
|
|
<span class="giallo-l"><span class="z-11"> "article"</span><span>,</span></span>
|
|
<span class="giallo-l"><span class="z-11"> "recital"</span><span>,</span></span>
|
|
<span class="giallo-l"><span class="z-11"> "flamboyant"</span><span>,</span></span>
|
|
<span class="giallo-l"><span>];</span></span>
|
|
<span class="giallo-l"></span>
|
|
<span class="giallo-l"><span class="z-1 z-i">let</span><span> map</span><span class="z-13"> = new</span><span class="z-5"> Map</span><span>();</span></span>
|
|
<span class="giallo-l"></span>
|
|
<span class="giallo-l"><span class="z-13">for</span><span> (</span><span class="z-1 z-i">let</span><span> word</span><span class="z-13"> of</span><span> words) {</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> key</span><span class="z-13"> =</span><span> word.</span><span class="z-5">split</span><span>(</span><span class="z-11">""</span><span>).</span><span class="z-5">sort</span><span>().</span><span class="z-5">join</span><span>(</span><span class="z-11">""</span><span>);</span></span>
|
|
<span class="giallo-l"></span>
|
|
<span class="giallo-l"><span class="z-13"> if</span><span> (</span><span class="z-13">!</span><span>map.</span><span class="z-5">has</span><span>(key)) {</span></span>
|
|
<span class="giallo-l"><span> map.</span><span class="z-5">set</span><span>(key, []);</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span> map.</span><span class="z-5">get</span><span>(key).</span><span class="z-5">push</span><span>(word);</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>This code results in a map with the following structure:</p>
|
|
<pre class="giallo z-code" ><code data-lang="json"><span class="giallo-l"><span>{</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> "aelnrst"</span><span>: [</span><span class="z-10">"antlers"</span><span>,</span><span class="z-10"> "rentals"</span><span>,</span><span class="z-10"> "sternal"</span><span>],</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> "aceilrt"</span><span>: [</span><span class="z-10">"article"</span><span>,</span><span class="z-10"> "recital"</span><span>],</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> "aabflmnoty"</span><span>: [</span><span class="z-10">"flamboyant"</span><span>]</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre><h3 id="implementing-our-own-simple-hash-map"><a class="anchor" href="#implementing-our-own-simple-hash-map" aria-label="Link to this section">#</a>
|
|
Implementing our own simple hash map</h3>
|
|
<p>Hash maps are one of many map implementations, and there are many ways to
|
|
implement hash maps. The simplest way, and the way we're going to demonstrate,
|
|
is to use a list of lists. The inner lists are often referred to as "buckets" in
|
|
the real-world, so that's what we'll call them here. A hash function is used on
|
|
the key to determine which bucket to store the key-value pair in, then the
|
|
key-value pair is added to that bucket.</p>
|
|
<p>Let's walk through a simple hash map implementation in JavaScript. We're going
|
|
to go through it bottom-up, so we'll see some utility methods before getting to
|
|
the <code>set</code> and <code>get</code> implementations.</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">class</span><span> </span><span class="z-5 z-u">HashMap</span><span> {</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> constructor</span><span>() {</span></span>
|
|
<span class="giallo-l"><span class="z-14"> this</span><span>.bs</span><span class="z-13"> =</span><span> [[], [], []];</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>We start off by creating a <code>HashMap</code> class with a constructor that sets up 3
|
|
buckets. We use 3 buckets and the short variable name <code>bs</code> so that this code
|
|
displays nicely on devices with smaller screens. In reality, you could have
|
|
however many buckets you want (and better variable names).</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">class</span><span> </span><span class="z-5 z-u">HashMap</span><span> {</span></span>
|
|
<span class="giallo-l"><span class="z-4"> // ...</span></span>
|
|
<span class="giallo-l"><span class="z-5"> bucket</span><span>(</span><span class="z-14 z-i">key</span><span>) {</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> h</span><span class="z-13"> =</span><span class="z-5"> murmur3</span><span>(key);</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span class="z-14"> this</span><span>.bs[h</span><span class="z-13"> %</span><span class="z-14"> this</span><span>.bs.length];</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>The <code>bucket</code> method uses <code class="bold green">murmur3</code> on the <code>key</code>
|
|
passed in to find a bucket to use. This is the only place in our hash map code
|
|
that a hash function is used.</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">class</span><span> </span><span class="z-5 z-u">HashMap</span><span> {</span></span>
|
|
<span class="giallo-l"><span class="z-4"> // ...</span></span>
|
|
<span class="giallo-l"><span class="z-5"> entry</span><span>(</span><span class="z-14 z-i">bucket</span><span>,</span><span class="z-14 z-i"> key</span><span>) {</span></span>
|
|
<span class="giallo-l"><span class="z-13"> for</span><span> (</span><span class="z-1 z-i">let</span><span> e</span><span class="z-13"> of</span><span> bucket) {</span></span>
|
|
<span class="giallo-l"><span class="z-13"> if</span><span> (e.key</span><span class="z-13"> ===</span><span> key) {</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span> e;</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span class="z-6"> null</span><span>;</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>The <code>entry</code> method takes a <code>bucket</code> and a <code>key</code> and scans the bucket until it
|
|
finds an entry with the given key. If no entry is found, <code>null</code> is returned.</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">class</span><span> </span><span class="z-5 z-u">HashMap</span><span> {</span></span>
|
|
<span class="giallo-l"><span class="z-4"> // ...</span></span>
|
|
<span class="giallo-l"><span class="z-5"> set</span><span>(</span><span class="z-14 z-i">key</span><span>,</span><span class="z-14 z-i"> value</span><span>) {</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> b</span><span class="z-13"> =</span><span class="z-14"> this</span><span>.</span><span class="z-5">bucket</span><span>(key);</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> e</span><span class="z-13"> =</span><span class="z-14"> this</span><span>.</span><span class="z-5">entry</span><span>(b, key);</span></span>
|
|
<span class="giallo-l"><span class="z-13"> if</span><span> (e) {</span></span>
|
|
<span class="giallo-l"><span> e.value</span><span class="z-13"> =</span><span> value;</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span>;</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span> b.</span><span class="z-5">push</span><span>({ key, value });</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>The <code>set</code> method is the first one we should recognise from our earlier
|
|
JavaScript <code>Map</code> examples. It takes a key-value pair and stores it in our hash
|
|
map. It does this by using the <code>bucket</code> and <code>entry</code> methods we created earlier.
|
|
If an entry is found, its value is overwritten. If no entry is found, the
|
|
key-value pair is added to the map. In JavaScript, <code>{ key, value }</code> is
|
|
shorthand for <code>{ key: key, value: value }</code>.</p>
|
|
<pre class="giallo z-code" ><code data-lang="javascript"><span class="giallo-l"><span class="z-1 z-i">class</span><span> </span><span class="z-5 z-u">HashMap</span><span> {</span></span>
|
|
<span class="giallo-l"><span class="z-4"> // ...</span></span>
|
|
<span class="giallo-l"><span class="z-5"> get</span><span>(</span><span class="z-14 z-i">key</span><span>) {</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> b</span><span class="z-13"> =</span><span class="z-14"> this</span><span>.</span><span class="z-5">bucket</span><span>(key);</span></span>
|
|
<span class="giallo-l"><span class="z-1 z-i"> let</span><span> e</span><span class="z-13"> =</span><span class="z-14"> this</span><span>.</span><span class="z-5">entry</span><span>(b, key);</span></span>
|
|
<span class="giallo-l"><span class="z-13"> if</span><span> (e) {</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span> e.value;</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span class="z-13"> return</span><span class="z-6"> null</span><span>;</span></span>
|
|
<span class="giallo-l"><span> }</span></span>
|
|
<span class="giallo-l"><span>}</span></span></code></pre>
|
|
<p>The <code>get</code> method is very similar to <code>set</code>. It uses <code>bucket</code> and <code>entry</code> to find
|
|
the entry related to the <code>key</code> passed in, just like <code>set</code> does. If an entry is
|
|
found, its <code>value</code> is returned. If one isn't found, <code>null</code> is returned.</p>
|
|
<p>That was quite a lot of code. What you should take away from it is that our
|
|
hash map is a list of lists, and a hash function is used to know which of the
|
|
lists to store and retrieve a given key from.</p>
|
|
<p>Here's a visual representation of this hash map in action. <span class="purple
|
|
bold">Click</span> anywhere on the buckets to add a new key-value pair using our
|
|
<code>set</code> method. To keep the visualisation simple, if a bucket were to "overflow",
|
|
the buckets are all reset.</p>
|
|
<p><hash-map hashFn=murmur3 valueFn=intToStr></hash-map></p>
|
|
<p>Because we're using <code class="green bold">murmur3</code> as our hash
|
|
function, you should see good distribution between the buckets. It's expected
|
|
you'll see <em>some</em> imbalance, but it should generally be quite even.</p>
|
|
<p>To get a value out of the hash map, we first hash the key to figure out which
|
|
bucket the value will be in. Then we have to compare the key we're searching for
|
|
against all of the keys in the bucket.</p>
|
|
<p>It's this search step that we minimise through hashing, and why <code
|
|
class="bold green">murmur3</code> is optimised for speed. The faster the hash
|
|
function, the faster we find the right bucket to search, the faster our hash
|
|
map is overall.</p>
|
|
<p>This is also why reducing collisions is so crucial. If we did decide to use that
|
|
dummy hash function from all the way at the start of this article, the one that
|
|
returns 0 all the time, we'll put all of our key-value pairs into the first
|
|
bucket. Finding anything could mean we have to check all of the values in the
|
|
hash map. With a good hash function, with good distribution, we reduce the
|
|
amount of searching we have to do to 1/N, where N is the number of buckets.</p>
|
|
<p>Let's see how <code class="red bold">stringSum</code> does.</p>
|
|
<p><hash-map hashFn=stringSum valueFn=intToStr></hash-map></p>
|
|
<p>Interestingly, <code class="bold red">stringSum</code> seems to distribute
|
|
values quite well. You notice a pattern, but the overall distribution looks
|
|
good.</p>
|
|
<blockquote class="haskie">
|
|
<img src="/images/haskie-triumphant-200px.png" />
|
|
<p>
|
|
Finally! A win for <code class="bold red">stringSum</code>. I knew it would
|
|
be good for something.
|
|
</p>
|
|
</blockquote>
|
|
<p>Not so fast, Haskie. We need to talk about a serious problem. The distribution
|
|
looks okay on these sequential numbers, but we've seen that <code class="bold
|
|
red">stringSum</code> doesn't have a good avalanche effect. This doesn't end
|
|
well.</p>
|
|
<h2 id="real-world-collisions"><a class="anchor" href="#real-world-collisions" aria-label="Link to this section">#</a>
|
|
Real-world collisions</h2>
|
|
<p>Let's look at 2 real-world data sets: IP addresses and English words. What I'm
|
|
going to do is take 100,000,000 random IP addresses and <a rel="external" href="https://github.com/dwyl/english-words">466,550 English
|
|
words</a>, hash all of them with both <code class="bold green">murmur3</code>
|
|
and <code class="red bold">stringSum</code>, and see how many collisions we get.</p>
|
|
<p><b>IP Addresses</b></p>
|
|
<table fixed>
|
|
<thead>
|
|
<th>
|
|
</th>
|
|
<th>
|
|
<code class="bold green">murmur3</code>
|
|
</th>
|
|
<th>
|
|
<code class="bold red">stringSum</code>
|
|
</th>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Collisions</td>
|
|
<td>1,156,959</td>
|
|
<td>99,999,566</td>
|
|
</tr>
|
|
<tr>
|
|
<td></td>
|
|
<td>1.157%</td>
|
|
<td>99.999%</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p><b>English words</b></p>
|
|
<table fixed>
|
|
<thead>
|
|
<th>
|
|
</th>
|
|
<th>
|
|
<code class="bold green">murmur3</code>
|
|
</th>
|
|
<th>
|
|
<code class="bold red">stringSum</code>
|
|
</th>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Collisions</td>
|
|
<td>25</td>
|
|
<td>464,220</td>
|
|
</tr>
|
|
<tr>
|
|
<td></td>
|
|
<td>0.005%</td>
|
|
<td>99.5%</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p>When we use hash maps for real, we aren't usually storing random values in them.
|
|
We can imagine counting the number of times we've seen an IP address in rate
|
|
limiting code for a server. Or code that counts the occurrences of words in
|
|
books throughout history to track their origin and popularity. <code class="bold
|
|
red">stringSum</code> sucks for these applications because of it's extremely
|
|
high collision rate.</p>
|
|
<h2 id="manufactured-collisions"><a class="anchor" href="#manufactured-collisions" aria-label="Link to this section">#</a>
|
|
Manufactured collisions</h2>
|
|
<p>Now it's <code class="bold green">murmur3</code>'s turn for some bad news.
|
|
It's not just collisions caused by similarity in the input we have to worry
|
|
about. Check this out.</p>
|
|
<p><hash-map hashFn=murmur3 valueFn=murmur3Collisions></hash-map></p>
|
|
<p>What's happening here? Why do all of these jibberish strings hash to the same
|
|
number?</p>
|
|
<p>I hashed 141 trillion random strings to find values that hash to the number
|
|
<code>1228476406</code> when using <code class="green bold">murmur3</code>. Hash functions
|
|
have to always return the same output for a specific input, so it's possible to
|
|
find collisions by brute force.</p>
|
|
<blockquote class="haskie">
|
|
<img src="/images/haskie-concerned-200px.png" />
|
|
<p>
|
|
I'm sorry, 141 <b>trillion</b>? Like... 141 and then 12 zeroes?
|
|
</p>
|
|
</blockquote>
|
|
<p>Yes, and it only took me 25 minutes. <a rel="external" href="https://computers-are-fast.github.io/">Computers are fast</a>.</p>
|
|
<p>Bad actors having easy access to collisions can be devastating if your software
|
|
builds hash maps out of user input. Take HTTP headers, for example. An HTTP
|
|
request looks like this:</p>
|
|
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>GET / HTTP/1.1</span></span>
|
|
<span class="giallo-l"><span>Accept: */*</span></span>
|
|
<span class="giallo-l"><span>Accept-Encoding: gzip, deflate</span></span>
|
|
<span class="giallo-l"><span>Connection: keep-alive</span></span>
|
|
<span class="giallo-l"><span>Host: google.com</span></span></code></pre>
|
|
<p>You don't have to understand all of the words, just that the first line is the
|
|
path being requested and all of the other lines are headers. Headers are <code>Key: Value</code> pairs, so HTTP servers tend to use maps to store them. Nothing stops us
|
|
from passing any headers we want, so we can be really mean and pass headers we
|
|
know will cause collisions. This can significantly slow down the server.</p>
|
|
<p><a rel="external" href="https://cryptanalysis.eu/blog/2011/12/28/effective-dos-attacks-against-web-application-plattforms-hashdos/">This isn't
|
|
theoretical, either</a>.
|
|
If you search "HashDoS" you'll find a lot more examples of this. It was a really
|
|
big deal in the mid-2000s.</p>
|
|
<p>There are a few ways to mitigate this specific to HTTP servers: ignoring
|
|
jibberish header keys and limiting the number of headers you store, for
|
|
example. But modern hash functions like <code class="bold green">murmur3</code>
|
|
offer a more generalised solution: randomisation.</p>
|
|
<p>Earlier in this post we showed some examples of hash function implementations.
|
|
Those implementations took a single argument: <code>input</code>. Lots of modern hash
|
|
functions take a 2nd parameter: <code>seed</code> (sometimes called <code>salt</code>). In the case
|
|
of <code class="green bold">murmur3</code>, this seed is a number.</p>
|
|
<p>So far, we've been using 0 as the seed. Let's see what happens with the
|
|
collisions I've collected when we use a seed of 1.</p>
|
|
<p><hash-map hashFn=murmur3 valueFn=murmur3Collisions seed=1></hash-map></p>
|
|
<p>Just like that, 0 to 1, the collisions are gone. This is the purpose of the
|
|
seed: it randomises the output of the hash function in an unpredictable way.
|
|
How it achieves this is beyond the scope of this article, all hash functions do
|
|
this in their own way.</p>
|
|
<p>The hash function still returns the same output for the same input, it's just
|
|
that the input is a combination of <code>input</code> and <code>seed</code>. Things that collide with
|
|
one seed shouldn't collide when using another. Programming languages often
|
|
generate a random number to use as the seed when the process starts, so that
|
|
every time you run your program the seed is different. As a bad guy, not knowing
|
|
the seed, it is now impossible for me to reliably cause harm.</p>
|
|
<p>If you look closely in the above visualisation and the one before it, they're
|
|
the same values being hashed but they produce different hash values. The
|
|
implication of this is that if you hash a value with one seed, and want to be
|
|
able to compare against it in the future, you need to make sure you use the same
|
|
seed.</p>
|
|
<p>Having different values for different seeds doesn't affect the hash map
|
|
use-case, because hash maps only live for the duration the program is running.
|
|
Provided you use the same seed for the lifetime of the program, your hash maps
|
|
will continue to work just fine. If you ever store hash values outside of your
|
|
program, in a file for example, you need to be careful you know what seed has
|
|
been used.</p>
|
|
<h2 id="playground"><a class="anchor" href="#playground" aria-label="Link to this section">#</a>
|
|
Playground</h2>
|
|
<p>As is tradition, I've made a playground for you to write your own hash functions
|
|
and see them visualised with the grids seen in this article. Click
|
|
<a href="/hashing-playground/">here</a> to try it!</p>
|
|
<h2 id="conclusion"><a class="anchor" href="#conclusion" aria-label="Link to this section">#</a>
|
|
Conclusion</h2>
|
|
<p>We've covered what a hash function is, some ways to measure how good it is,
|
|
what happens when it's not good, and some of the ways they can be broken by
|
|
bad actors.</p>
|
|
<p>The universe of hash functions is a large one, and we've really only scratched
|
|
the surface in this post. We haven't spoken about cryptographic vs
|
|
non-cryptographic hashing, we've touched on only 1 of the thousands of use-cases
|
|
for hash functions, and we haven't talked about how exactly modern hash
|
|
functions actually work.</p>
|
|
<p>Some further reading I recommend if you're really enthusiastic about this topic
|
|
and want to learn more:</p>
|
|
<ul>
|
|
<li><a rel="external" href="https://github.com/rurban/smhasher">https://github.com/rurban/smhasher</a> this repository is the gold standard
|
|
for testing how good hash functions are. They run a tonne of tests against
|
|
a wide number of hash functions and present the results in a big table. It
|
|
will be difficult to understand what all of the tests are for, but this is
|
|
where the state of the art of hash testing lives.</li>
|
|
<li><a rel="external" href="https://djhworld.github.io/hyperloglog/">https://djhworld.github.io/hyperloglog/</a> this is an interactive piece
|
|
on a data structure called HyperLogLog. It's used to efficiently count the
|
|
number of unique elements in very, very large sets. It uses
|
|
hashing to do it in a really clever way.</li>
|
|
<li><a rel="external" href="https://www.gnu.org/software/gperf/">https://www.gnu.org/software/gperf/</a> is a piece of software that, when given
|
|
the expected set of things you want to hash, can generate a "perfect" hash
|
|
function automatically.</li>
|
|
</ul>
|
|
<p>Feel free to join the discussion on <a rel="external" href="https://news.ycombinator.com/item?id=36401747">Hacker News</a>!</p>
|
|
<h2 id="acknowledgements"><a class="anchor" href="#acknowledgements" aria-label="Link to this section">#</a>
|
|
Acknowledgements</h2>
|
|
<p>Thanks to everyone who read early drafts and provided invaluable feedback.</p>
|
|
<ul>
|
|
<li><a rel="external" href="https://delroth.net/">delroth</a>, <a rel="external" href="https://twitter.com/Manon_Lef/">Manon</a>, <a rel="external" href="https://medium.com/@AaronKalair">Aaron</a>, <a rel="external" href="https://twitter.com/TheCoppinger">Charlie</a></li>
|
|
</ul>
|
|
<p>And everyone who helped me find <code class="green bold">murmur3</code> hash
|
|
collisions:</p>
|
|
<ul>
|
|
<li><a rel="external" href="https://indy.codes/">Indy</a>, <a rel="external" href="https://medium.com/@AaronKalair">Aaron</a>, Max</li>
|
|
</ul>
|
|
|
|
</div>
|
|
</article>
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<footer class="newsletter-footer">
|
|
<div>
|
|
Made with ❤️ by Sam Rose
|
|
</div>
|
|
<svg
|
|
id="sam-rose-signature"
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="50 154 640.1 157"
|
|
style="color: var(--color); width: 150px; display: block"
|
|
>
|
|
<style>
|
|
#sam-rose-signature path {
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 6;
|
|
stroke-linecap: round;
|
|
stroke-dasharray: 1;
|
|
stroke-dashoffset: 1;
|
|
opacity: 0;
|
|
}
|
|
|
|
#sam-rose-signature.in-view path:nth-of-type(1) {
|
|
animation: draw 0.9s ease forwards;
|
|
}
|
|
|
|
#sam-rose-signature.in-view path:nth-of-type(2) {
|
|
animation: draw 1.5s ease-out forwards;
|
|
animation-delay: 0.9s;
|
|
}
|
|
|
|
#sam-rose-signature.in-view path:nth-of-type(3) {
|
|
animation: draw 0.4s ease forwards;
|
|
animation-delay: 2.4s;
|
|
}
|
|
|
|
@keyframes draw {
|
|
0% {
|
|
stroke-dashoffset: 1;
|
|
opacity: 0;
|
|
}
|
|
1% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
stroke-dashoffset: 0;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
#sam-rose-signature path,
|
|
#sam-rose-signature.in-view path {
|
|
animation: none;
|
|
opacity: 1;
|
|
stroke-dashoffset: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<path
|
|
pathLength="1"
|
|
d="M279 180h-6 0-4 0-4 0-4 0-4 0-4 0-4.1 0l-4 .5h0l-4 .6h0l-4 .6h0l-3.9.7h0l-4 .6h0l-3.9.8h0l-3.9.8h0l-4 .9h0l-3.9.8h0l-3.9.8h0l-4 .8h0l-3.9.8h0l-3.9.8h0q-2 .4-3.8 1h0L190 192h0l-3.7 1.5h0l-3.7 1.5h0q-1.9.8-3.5 1.9h0q-1.6 1-2.2 3h0q-.6 1.8-.4 3.8h0q.2 2 1.5 3.1h0q1.4 1.3 3.2 2h0l3.7 1.7h0q1.8.8 3.7 1.4h0l3.8 1.4h0l3.8 1.3h0l3.8 1.4h0l3.8 1.3h0l3.8 1.2h0l3.8 1.3h0q1.9.6 3.7 1.4h0l3.7 1.7h0l3.6 1.8h0l3.5 1.8h0q1.8 1 3.4 2.2h0l3.2 2.3h0q1.6 1.3 2.3 3.1h0q.8 1.9.6 3.8h0q-.1 2-1.2 3.5h0q-1.2 1.6-2.6 3h0q-1.4 1.4-3 2.5h0l-3.6 2h0l-3.6 1.6h0l-3.6 1.7h0l-3.8 1.5h0l-3.8 1.2h0l-3.8 1.3h0l-3.8 1.3h0l-3.8 1.2h0l-3.9 1.2h0l-3.8 1h0l-3.9 1.2h0l-3.8 1h0l-3.9 1.2h0l-3.9 1h0l-3.9.9h0l-4 .9h0l-3.8.9h0l-4 .9h0l-3.9.8h0l-3.9.9h0l-4 .7h0l-3.9.8h0l-4 .7h0l-3.9.8h0l-4 .6h0l-3.9.7h0l-4 .6h0l-4 .6h0l-3.9.7h0l-4 .5h0l-4 .5h0l-4 .4h0l-4 .5h0l-3.9.5h0l-4 .6h0l-4 .6h0l-4 .6h0l-3.9.4h0l-4 .4h0l-4 .6h0q-2 .3-3.8 1l-1.9.6"
|
|
></path>
|
|
|
|
<path
|
|
pathLength="1"
|
|
d="m54 219 7-6.2q7-6.3 15.8-10.2h0q8.8-4 18-7.4h0l18.2-6.6h0q9.1-3.3 18.4-6.1h0l18.6-5.6h0q9.3-2.7 18.7-5.1h0q9.4-2.5 19-4h0l19.2-3.1h0l19.1-3h0q9.6-1.6 19.3-2.3h0q9.7-.7 19.4-1h0q9.7-.4 19.4-.4h19.4q9.7 0 19.4.7h0q9.6.7 19.1 2.8h0q9.5 2 16.7 7.3h0q7.3 5.3 1 11.7h0q-6.2 6.4-15.2 10h0q-9 3.5-18.4 6.3h0l-18.5 5.7h0L289 208h0l-18.6 5.5h0q-9.4 2.7-15.1 5.1h0q-5.7 2.4 4 2.4h0q9.7 0 19.2-1.5h0q9.6-1.5 19.1-3.6h0q9.5-2 19-3.6h0q9.7-1.5 19.3-2.6h0q9.7-1 19.3-2.4h0q9.6-1.3 19.2-2.9h0q9.6-1.5 19.2-2.6h0q9.7-1 15.8-3.1h0q6-2.1-3.6-3h0q-9.6-.7-19.3-.5h0q-9.7.3-19.4 1.4h0q-9.6 1-18.2 5.2h0q-8.5 4.1-2.7 8.8h0q5.9 4.6 15.6 5.3h0q9.6.8 19.3 1h0q9.7.1 19.4-.6h0q9.7-.7 19.1-2.9h0q9.5-2.1 19-4.5h0q9.3-2.4 18.2-6.2h0q8.8-3.8 5.2-8.9h0q-3.7-5-13.3-6.4h0q-9.5-1.4-19.2-1.9h0q-9.7-.5-19.4-.5h0q-9.7 0-12.5 3.1h0q-2.8 3.2 6.9 3.5h0q9.6.4 19.4.4h97q9.8 0 14-1.2h0q4.2-1.3-5.4-2h0q-9.7-.8-19.4-.8h0-19.4 0q-9.8 0-19.4.4h0q-9.7.3-3.4 2.2h0q6.4 2 16 2.5h0l19.5 1.2h0l19.4 1.2h0q9.6.6 19.3 1.5h0q9.7 1 5.2 5.1h0q-4.4 4.1-14 5.6h0q-9.6 1.5-19.3 2.2h0l-19.4 1.4h0q-9.6.7-4.5 1.7h0q5.2 1 15 1.4h0q9.6.5 19.3.5h0l19.4.4h0l19.5.5h0l19.4.2h37.6q8.5-.1-1.2-1h0q-9.6-.8-19.4-.9h0l-19.4-.1h0H561h0q-9.7 0-9.6 1.6h0q.2 1.6 9.8 2.8h0q9.6 1.2 19.3 1.8h0q9.7.6 19.3 1.8h0l19.3 2.3h0l19.3 2.1h0l19.3 2.4h0q9.6 1.3 19 3.3l9.4 1.9"
|
|
></path>
|
|
|
|
<path
|
|
pathLength="1"
|
|
d="M132 307l2-1.7q2.2-1.6 4.7-2.1h0q2.6-.5 5.2-.7h0l5.4-.5h0l5.3-.4h0l5.3-.5h0l5.3-.6h0l5.4-.5h0l5.3-.8h0l5.2-.8h0l5.3-.8h0l5.3-.8h0l5.3-.9h0l5.3-.8h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.8h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.8h0l5.3-.7h0l5.3-.7h0l5.3-.6h0l5.3-.7h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.4-.6h0l5.3-.6h0l5.3-.5h0l5.3-.4h0l5.4-.4h0l5.3-.4h0l5.3-.4h0l5.4-.3h0l5.3-.4h0l5.3-.4h0l5.4-.4h0l5.3-.4h0l5.3-.3h0l5.4-.1h0l5.3-.1h0l5.4-.1h0l5.3-.1h0l5.4-.1h5.3l5.3-.2h5.4l5.3-.2h5.4l5.3-.1h42.8q2.7 0 5.3.2h0l5.4.2h0l5.3.3h0l5.4.3h0l5.3.2h0l5.3.3h0l5.4.3h0l5.3.3h0l5.3.5h0l5.4.5h0l5.3.5h0l5.3.5h0l5.3.6h0q2.7.2 5.3.6h0l5.3.7h0l5.3.7h0l5.3.7h0l5.3.8h0l5.3.8h0l5.3 1h0q2.6.4 5 1.2l2.5.8"
|
|
></path>
|
|
</svg>
|
|
|
|
<nav class="social-links" aria-label="Sam Rose on social media">
|
|
<a
|
|
href="https://twitter.com/samwhoo"
|
|
target="_blank"
|
|
aria-label="Follow Sam Rose on X (opens in a new tab)"
|
|
>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden="true"
|
|
>
|
|
<g clip-path="url(#clip0_260_538)">
|
|
<path
|
|
d="M23.2245 5.78963C22.4373 6.13838 21.591 6.37087 20.6981 6.48075C21.606 5.94187 22.3042 5.08238 22.632 4.06688C21.7826 4.56488 20.8417 4.9305 19.8405 5.12325C19.041 4.27575 17.9006 3.75 16.6372 3.75C14.2102 3.75 12.2433 5.7 12.2433 8.10413C12.2433 8.44425 12.2831 8.77575 12.3581 9.0975C8.70708 8.91412 5.46895 7.17863 3.30258 4.54463C2.92195 5.18813 2.70783 5.94187 2.70783 6.73725C2.70783 8.24887 3.48183 9.57937 4.66195 10.3635C3.94158 10.3414 3.26358 10.1411 2.66995 9.81975C2.66995 9.83288 2.66995 9.85238 2.66995 9.87075C2.66995 11.9831 4.18495 13.7419 6.19308 14.1424C5.82595 14.2429 5.4382 14.2987 5.03845 14.2987C4.75458 14.2987 4.4782 14.2665 4.21045 14.2185C4.76958 15.9446 6.39183 17.2065 8.3137 17.2436C6.80995 18.4095 4.9162 19.1077 2.8567 19.1077C2.50083 19.1077 2.1532 19.0875 1.80933 19.0459C3.75445 20.2778 6.06483 21 8.5447 21C16.6256 21 21.0461 14.364 21.0461 8.60737C21.0461 8.41875 21.0401 8.23088 21.0311 8.04525C21.894 7.43625 22.6372 6.6675 23.2245 5.78963Z"
|
|
fill="#1DA1F2"
|
|
/>
|
|
</g>
|
|
<defs>
|
|
<clipPath id="clip0_260_538">
|
|
<rect width="24" height="24" fill="white" />
|
|
</clipPath>
|
|
</defs>
|
|
</svg>
|
|
</a>
|
|
<a
|
|
href="https://bsky.app/profile/samwho.dev"
|
|
target="_blank"
|
|
>
|
|
<img
|
|
src="/images/bluesky.svg"
|
|
alt="Follow Sam Rose on Bluesky (opens in a new tab)"
|
|
width="20"
|
|
height="20"
|
|
style="transform: translateY(2px)"
|
|
/>
|
|
</a>
|
|
<a href="/rss.xml">
|
|
<img src="/images/rss.svg" alt="RSS feed" width="24" height="24" />
|
|
</a>
|
|
</nav>
|
|
|
|
<p style="font-size: 0.8rem">
|
|
If you want, get updates about new posts by email. No spam, ever.
|
|
</p>
|
|
|
|
<form
|
|
action="https://buttondown.email/api/emails/embed-subscribe/samwho"
|
|
method="post"
|
|
class="embeddable-buttondown-form"
|
|
>
|
|
<label class="visually-hidden" for="bd-email">Email address</label>
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
id="bd-email"
|
|
autocomplete="email"
|
|
placeholder="your@email.address"
|
|
/>
|
|
<input type="submit" value="Subscribe" />
|
|
<p>
|
|
<a href="https://buttondown.email/refer/samwho" target="_blank"
|
|
>powered by buttondown</a
|
|
>
|
|
</p>
|
|
</form>
|
|
</footer>
|
|
|
|
<script>
|
|
const signature = document.getElementById('sam-rose-signature');
|
|
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting && entry.intersectionRatio === 1) {
|
|
signature.classList.add('in-view');
|
|
observer.unobserve(signature);
|
|
}
|
|
},
|
|
{
|
|
threshold: 1.0
|
|
}
|
|
);
|
|
|
|
observer.observe(signature);
|
|
</script>
|
|
|
|
|
|
</body>
|