2024-03-14T00:00:00.000Zhttps://jameshfisher.com/feed.xmlJim Fisher’s blogJim FisherHow can I capture all crashes in a web page?2024-03-14T00:00:00.000Zhttps://jameshfisher.com/2024/03/14/how-can-i-capture-all-crashes-in-a-web-page/<p>Services like <a href="https://sentry.io/">Sentry</a>
have JS libraries that capture things that go wrong in your web page.
How do they work?</p>
<p>There are many kinds of “things that can go wrong”,
and they must be captured in different ways:</p>
<ul>
<li><code>window.addEventListener("error", handler)</code>
handles errors that are <code>throw</code>n by JS but not caught by <code>try</code>/<code>catch</code>.
This is because if no <code>catch</code> block is found,
an <code>ErrorEvent</code> is created and dispatched directly on the <code>window</code> object.</li>
<li><code>window.addEventListener("error", handler, true)</code>
handles <code>error</code> events in their <em>capture</em> phase.
This is needed to capture events that are dispatched on DOM elements.
For example, if an <code>img</code> source fails to load,
an “error” event is dispatched on that <code>img</code> element.
Note that this event does not <em>bubble</em>, but it does have a <em>capture</em> phase.</li>
<li><code>window.addEventListener("unhandledrejection", handler)</code>
handles unhandled <code>Promise</code> rejections.
(Note that, since Promises can be implemented in JS,
this depends on the implementation.
For example, <a href="http://bluebirdjs.com/docs/api/error-management-configuration.html">Bluebird dispatches this event</a>.)</li>
<li><code>window.addEventListener("securitypolicyviolation", handler)</code>
handles <code>Content-Security-Policy</code> violations.</li>
<li><code>console.error</code> can be overridden.</li>
</ul>
<p>Note there is also <code>window.onerror</code>,
an old and non-standard API
which seems to capture the same errors as <code>window.addEventListener("error")</code>.
I don’t see a reason to use it.</p>
<p>Below is a small playground demonstrating these different kinds of crashes.</p>
<style>
#crashlog > div {
background: #eee;
margin: 0.5em;
border-radius: 0.5em;
}
</style>
<div>
<button onclick="causeUncaughtError()">Cause uncaught error</button>
<button onclick="causeUnhandledRejection()">Cause unhandled exception</button>
<button onclick="causeImageFailure()">Cause image failure</button>
<button onclick="causeCSPViolation()">Cause CSP violation</button>
<button onclick="causeConsoleError()">Cause console error</button>
<div>
<div id="crashlog" style="font-family: monospace; font-size: 0.8em;"></div>
<div id="resources">
</div>
<script>
// https://stackoverflow.com/a/18391400/229792
if (!('toJSON' in Error.prototype)) {
Object.defineProperty(Error.prototype, 'toJSON', {
value: function () {
var alt = {};
Object.getOwnPropertyNames(this).forEach(function (key) {
alt[key] = this[key];
}, this);
return alt;
},
configurable: true,
writable: true
});
}
const cspMetaEl = document.createElement('meta');
cspMetaEl.setAttribute("http-equiv", "Content-Security-Policy");
cspMetaEl.setAttribute("content", "img-src 'self';");
document.head.appendChild(cspMetaEl);
const crashlogEl = document.getElementById("crashlog");
const resourcesEl = document.getElementById("resources");
function report(source, data) {
const crashEl = document.createElement("div");
crashEl.innerText = `${source}: ${JSON.stringify(data)}`;
crashlogEl.appendChild(crashEl);
}
window.addEventListener("error", (errorEvent) => {
const { filename, lineno, colno, error, message } = errorEvent;
report("window.addEventListener('error')", { filename, lineno, colno, error, message });
});
window.addEventListener("error", (errorEvent) => {
report("window.addEventListener('error', ..., true)", errorEvent);
}, true);
window.addEventListener("unhandledrejection", (ev) => {
report("window.addEventListener('unhandledrejection')", ev)
});
window.addEventListener('securitypolicyviolation', (event) => {
const { blockedURI, violatedDirective, originalPolicy } = event;
report("window.addEventListener('securitypolicyviolation')", { blockedURI, violatedDirective, originalPolicy });
});
let originalConsoleError = console.error;
console.error = function() {
report("console.error", arguments);
originalConsoleError.apply(console, arguments);
};
function clear() {
crashlogEl.innerText = '';
}
function causeUncaughtError() {
clear();
throw new Error('An error from the button');
}
function causeUnhandledRejection() {
clear();
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('rejected!'));
}, 100);
});
}
function causeImageFailure() {
clear();
const imgEl = document.createElement("img");
imgEl.setAttribute("src", "/fakeimage/" + Math.random().toString());
resourcesEl.appendChild(imgEl);
}
function causeCSPViolation() {
clear();
const imgEl = document.createElement("img");
imgEl.setAttribute("src", "https://example.com/csp-violation.png");
resourcesEl.appendChild(imgEl);
}
function causeConsoleError() {
clear();
console.error("A console error");
}
</script>
How do errors in a web page reach the dev console?2024-03-13T00:00:00.000Zhttps://jameshfisher.com/2024/03/13/how-do-thrown-errors-reach-the-console/<p>When you write <code>throw new Error("blah")</code> in JavaScript,
this often results in a message in the console.
What are the steps that lead to this message?</p>
<p>First, we go up the call stack until the nearest <code>catch</code> block.
If there is no <code>catch</code> block, an <code>ErrorEvent</code> is created,
and <code>window.dispatchEvent</code> is called with it.
These errors can be observed by adding an event listener:</p>
<pre><code class="language-js">window.addEventListener("error", (errorEvent) => {
// ...
});
</code></pre>
<p>The browser’s console is one such event listener.
We can see this by cancelling the events before they reach the console output:</p>
<pre><code class="language-js">window.addEventListener("error", (errorEvent) => {
errorEvent.preventDefault();
});
</code></pre>
<p>Despite GPT-4’s beliefs, <code>throw new Error()</code> does <em>not</em> propagate through the DOM tree.
If you throw an error in a button click event listener,
the error does is not dispatched on the button element,
and it does not bubble up to the button’s ancestor elements.
The error event is dispatched directly on the <code>window</code> object.</p>
<p>However, other events <em>do</em> propagate through the DOM.
If an image fails to load,
an “error” event is dispatched on the image element.
This event does not bubble, but it does have a capture phase.
We can capture these resource errors in their capture phase using:</p>
<pre><code class="language-js">window.addEventListener("error", (errorEvent) => {
// ...
}, true);
</code></pre>
A formula for responsive font-size2024-03-12T00:00:00.000Zhttps://jameshfisher.com/2024/03/12/a-formula-for-responsive-font-size/<p>This CSS is now part of most websites I make:</p>
<pre><code class="language-css">:root {
font-size: calc(1rem + 0.25vw);
}
</code></pre>
<p>This rule is an alternative to <code>@media</code> rules like this from nytimes.com:</p>
<pre><code class="language-css">p { font-size: 1.125rem; }
@media (min-width: 740px) {
p { font-size: 1.25rem; }
}
</code></pre>
<p>This pattern is the norm:
a small font size, a large font size, and a breakpoint.
Here’s a sample of common sites:</p>
<table>
<thead>
<tr>
<th></th>
<th>Small font-size</th>
<th>Large font-size</th>
<th>Breakpoint</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Medium.com</strong></td>
<td><code>1.125rem</code></td>
<td><code>1.25rem</code></td>
<td><code>728px</code></td>
</tr>
<tr>
<td><strong>Substack.com</strong></td>
<td><code>1.125rem</code></td>
<td><code>1.25rem</code></td>
<td><code>768px</code></td>
</tr>
<tr>
<td><strong>Nytimes.com</strong></td>
<td><code>1.125rem</code></td>
<td><code>1.25rem</code></td>
<td><code>740px</code></td>
</tr>
</tbody>
</table>
<p>But breakpoints are mathematically ugly!
Instead of defining <code>font-size</code> piecewise,
can’t we use one linear function?
Here’s the line I believe they’re trying to approximate:</p>
<p><img src="/assets/2024-03-12/chart.png" style="border: none; max-width: 30em; margin: 0 auto; display: block;" /></p>
<p>With modern CSS, we can just write that function!
It’s <code>calc(1.0625rem + 0.2604vw)</code>.
I round this to <code>1rem + 0.25vw</code>.</p>
<p>Sharp-eyed readers might wonder:
doesn’t my CSS have circular reasoning?
If <code>rem</code> is defined as “Font size of the root element”,
how can we use <code>1rem</code> in the definition of <code>font-size</code> on the root element?!
It turns out <a href="https://www.w3.org/TR/css-values-3/#rem">it’s a special case</a>:</p>
<blockquote>
<p>When specified in the <code>font-size</code> property of the root element,
or in a document with no root element,
<code>1rem</code> is equal to the initial value of the <code>font-size</code> property.</p>
</blockquote>
Setting font-size based on viewing distance2024-03-11T00:00:00.000Zhttps://jameshfisher.com/2024/03/11/setting-font-size-based-on-viewing-distance/<p>I want to set the font-size at the most comfortable size for reading.
But this depends on the distance from the reader to the screen.</p>
<p>Can we estimate this distance from screen size?
I measured my viewing distance for four devices:</p>
<table>
<thead>
<tr>
<th></th>
<th>Mobile (portrait)</th>
<th>Mobile (landscape)</th>
<th>Laptop</th>
<th>Desktop</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Width</strong></td>
<td>7</td>
<td>15</td>
<td>29</td>
<td>62</td>
</tr>
<tr>
<td><strong>Weight</strong></td>
<td>15</td>
<td>7</td>
<td>18</td>
<td>34</td>
</tr>
<tr>
<td><strong>Distance</strong></td>
<td>25</td>
<td>30</td>
<td>50</td>
<td>80</td>
</tr>
</tbody>
</table>
<p>I found that <code>17cm + width</code> decently predicts the viewing distance.</p>
<p>Now given an estimated viewing distance,
we can translate desired an <em>angular</em> size (like 1 degree) into an <em>absolute</em> size with
<code>dist * tan(angle)</code>.</p>
<p>We can express that in CSS with: <code>calc((17cm + 100vw) * tan(1deg))</code>.
This red box is 1 degree of your field of view:</p>
<div style="background: red; width: calc((17cm + 100vw) * tan(1deg)); aspect-ratio: 1 / 1">
</div>
<p>And this text is <code>font-size: 0.5deg</code> (estimated):</p>
<div style="background: #eee; font-size: calc((17cm + 100vw) * tan(0.5deg));">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
<p>Result: I’m not convinced that the optimum font-size is purely an angular size.
There are other considerations, like:</p>
<ul>
<li>If the user is <strong>on-the-go</strong>, as they often are with a mobile, movement makes everything harder to read.</li>
<li>If the user is <strong>long-sighted</strong>, their mobile screen may be further away than estimated.</li>
<li>If the user is <strong>short-sighted</strong>, text on their desktop may need to be a larger angular size.
(Myopia is not just a uniform blurring of the visual field!)</li>
</ul>
How does HotJar record your screen?2024-03-09T00:00:00.000Zhttps://jameshfisher.com/2024/03/09/how-does-hotjar-record-your-screen/<p>I was blown away when I first saw tools like HotJar.
You could see everything your users were doing!
The only thing missing was a secret recording of their webcam!</p>
<p>How does it work?
Capturing mouse and keyboard events is easy,
but how can you record exactly what the user sees?
Browsers have a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API">Screen Capture API</a>,
but these tools sure don’t use that.
They need to work efficiently in the background without extra permissions.</p>
<p><a href="https://posthog.com/session-replay">PostHog session replay</a> is a modern, open-source implementation.
It uses <a href="https://github.com/rrweb-io/rrweb">rrweb</a>,
a library to “record and replay the web”.
All the magic is in there.</p>
<p>Here’s a first attempt,
which sends the DOM as HTML to your recording endpoint once per second:</p>
<pre><code class="language-js">const sessionId = Math.random();
function snapshot() {
return (new XMLSerializer()).serializeToString(document);
}
function sendSnapshot() {
fetch(`/recordings/${sessionId}`, {
method: 'POST',
body: snapshot()
})
}
setInterval(sendSnapshot, 1000)
</code></pre>
<p>Problems with this naive implementation:</p>
<ol>
<li>It doesn’t capture everything.</li>
<li>It misses changes, and captures them too late.</li>
<li>It’s very inefficient (in CPU, network, and storage).</li>
</ol>
<p>What other state is there to capture?
The HTML has references to external resources, like images and stylesheets.
To capture the image data, <a href="https://github.com/rrweb-io/rrweb/blob/e607e83b21d45131a56c1ff606e9519a5b475fc1/packages/rrweb-snapshot/src/snapshot.ts#L744">rrweb draws the image to a canvas</a>.
And to capture a stylesheet,
we can consult <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/styleSheets"><code>Document.styleSheets</code></a>.
We also need the <code>window.innerWidth</code> and <code>window.innerHeight</code>,
and the scroll offsets for anything with a scrollbar.</p>
<p>To capture all changes instantly,
we can use <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">the <code>MutationObserver</code> API</a>.
This lets us replace <code>setInterval</code> with something like:</p>
<pre><code class="language-js">const observer = new MutationObserver(sendSnapshot);
observer.observe(
document.documentElement,
{ attributes: true, childList: true, subtree: true }
);
</code></pre>
<p>Finally, we can make this more efficient
by capturing <em>changes</em> rather than <em>snapshots</em>.
The <code>MutationObserver</code> callback gets a list of <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord"><code>MutationRecord</code>s</a>.
In theory they can be <em>applied</em> to a snapshot to get an updated DOM.
We can send deltas these to our recording API.
We’ll need to also send any external resources that the updated nodes refer to.</p>
The golden rule of PR reviews2023-10-07T00:00:00.000Zhttps://jameshfisher.com/2023/10/07/the-golden-rule-of-code-review/<p>You’re reviewing Jane’s pull request.
It fixes a bug that stops many customers using your software.
Unfortunately, her variable names look strange.
Do you “Approve” her PR, or do you “Request changes”?
Your choice here is between two product variants:</p>
<div>
<table>
<thead>
<tr>
<th></th>
<th>Product A</th>
<th>Product B</th>
</tr>
</thead>
<tbody>
<tr>
<th>Usability</th>
<td>❌ Some customers can't use it</td>
<td>🎉 Customers can use it</td>
</tr>
<tr>
<th>Variable names</th>
<td>😐 No changes</td>
<td>🤔 Some questionable names</td>
</tr>
</tbody>
</table>
</div>
<p>If you had to choose between Product A and Product B,
which would you choose?
The bugfix clearly outweighs the variable name choices.
Therefore, you should approve Jane’s pull request.
This is The Golden Rule of PR Review:
<strong>If It’s An Improvement, Approve It.</strong></p>
<p>With the decision framed this way,
no reasonable person would choose Product A.
And yet in my experience, many reviewers would not approve Jane’s PR!
This is usually because they’re not following the Golden Rule,
but instead following some other incorrect rule.
Let’s see some examples.</p>
<p>John hit “Request changes” on this PR, writing:
“I would have written this loop functionally. You can use a <code>map</code> function here.”
In doing so, John was implicitly answering the question:
<strong>“Is this a change <em>I</em> would have made?”</strong>
But this is the wrong question:
you’re different people, and there’s more than one way to do it!
Perhaps John was influenced by the possibility that he might be blamed for approving Jane’s review.
(And perhaps John was influenced by the unfortunate name “<em>code</em> review”,
which wrongly emphasizes “code” as the thing that matters.)
As a response to John’s review policy,
Jane will instead request review from team members
that she believes would have implemented the change similarly.</p>
<p>Alex hit “Request changes”, writing:
“All PRs should maintain or improve coverage. Please add unit tests before merging.”
In doing so, Alex was implicitly answering the question:
<strong>“Is <em>every aspect</em> of this PR an improvement?”</strong>
But this too is a wrong question:
almost everything is an engineering trade-off!
Perhaps Alex was influenced by all those CI metrics (coverage, code size, bundle size, build time),
which wrongly emphasize aspects that are easily measurable,
and which imply that there is no trade-off amongst them.
As a response to Alex’s review policy,
Jane will not bother submitting future improvements,
because the cost of submitting a change is too high.</p>
<p>Bob hit “Request changes”, writing:
“The page looks bad on mobile. Please use a responsive design.”
In doing so, Bob is implicitly answering the question:
<strong>“Is this the ideal implementation?”</strong>
But this is yet another wrong question:
product development is iterative.
Perhaps Bob was influenced by <a href="https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow">Gitflow</a>,
an antiquated style in which
“developers create a feature branch and delay merging it to the main trunk branch until the feature is complete.”
As a response to Bob’s review policy,
changes will pile up on long-running “feature” branches,
leading to <a href="https://cloud.google.com/architecture/devops/devops-tech-trunk-based-development">slower software delivery</a>.</p>
<p>Sometimes people complain about reviewers being “petty tyrants”,
drunk on the small amount of power that this task gives them.
I don’t think this is the main reason for overly strict reviews.
Rather, I think the primary cause is that reviewers are not taught the Golden Rule:
<strong>If It’s An Improvement, Approve It.</strong></p>
Let's get calibrated! A workshop2023-09-13T00:00:00.000Zhttps://jameshfisher.com/2023/09/13/lets-get-calibrated-a-workshop/<p>I ran <a href="https://docs.google.com/presentation/d/1-nTZsbPRk_TQs4U9-bVi9xfCUdT_BfCCXocBxywSljw/edit?usp=sharing">a workshop called <em>Let’s Get Calibrated!</em></a>.
It was designed to show:</p>
<ul>
<li>Why estimations and calibration are important, and what they even mean</li>
<li>How to calibrate yourself, while learning some facts about the company</li>
</ul>
<p>Here are the Google Slides.
You can run this workshop in your own company.
Just fill it in with facts from your company.</p>
<div>
<div style="position:relative;padding-top:56.25%;">
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSbtKaDD52JGMwRF94cz8bXjTL5HjDHkVAnbr9nnOx1Sak-4MGmO6g7yM3KMAjxW839L1r5kXV1z8Qe/embed?start=false&loop=false&delayms=3000" frameborder="0" allowfullscreen
style="position:absolute;top:0;left:0;width:100%;height:100%;"></iframe>
</div>
</div>
How ForumMagnum builds communities of inquiry2023-09-04T00:00:00.000Zhttps://jameshfisher.com/2023/09/04/how-forummagnum-builds-communities-of-inquiry/Build norms, not features2023-08-28T00:00:00.000Zhttps://jameshfisher.com/2023/08/28/build-norms-not-features/<p>If you’re building a multi-user product, you’re not just selling social <em>features</em>.
You’re selling social <em>norms</em>.
In this post, I analyze LessWrong as a great example of product design,
carefully designed to build strong social norms.</p>
<p><a href="https://www.lesswrong.com/">LessWrong</a> is a forum with various features.
But the developers don’t see themselves as building social <em>features</em> in a <em>product</em>.
Instead, they’re building social <a href="https://en.wikipedia.org/wiki/Social_norm"><em>norms</em></a> in a <em>community</em>.
Social norms are shared expectations of how to work together.
The LessWrong community has strong social norms,
such as <em>long-form writing</em> and <em>rationality</em>.
The forum developers can’t build those norms directly,
but they can help build them indirectly.
Let’s see how they’ve achieved this.</p>
<p><strong>Technique 1: state your norms.</strong>
LessWrong’s community guidelines are below each comment box:</p>
<blockquote>
<p>Aim to explain, not persuade.
Try to offer concrete models and predictions.
If you disagree, try getting curious about what your partner is thinking.
Don’t be afraid to say ‘oops’ and change your mind.</p>
</blockquote>
<p>This technique is extremely simple and powerful,
but surprisingly under-used!
I bet your enterprise Slack or Notion has no such norms stated anywhere.</p>
<p><strong>Technique 2: defaults.</strong>
Consider the behavior of the <code>⏎Enter</code> button in a text box.
If <code>⏎Enter</code> sends my message, I’ll write short <em>messages</em>.
But if creates a new paragraph, I’ll write longer <em>comments</em>.
Then, by posting my longer comment,
I help build the social norm that <em>this is a place for long-form</em>.
<a href="https://en.wikipedia.org/wiki/Nudge_theory">Nudge theory</a> tells us that defaults are powerful.
But in multi-user platforms, defaults are all-powerful:
first they determine individual behavior,
then this determines group behavior,
and then this sets off a positive feedback loop.</p>
<p>(Aside: I bet you’ve experienced what I call “Shift-Enter anxiety.”
Visiting a new app, will the <code>⏎Enter</code> key create a new paragraph,
or will it prematurely send my message?
With this micro-stress, I must make a guess:
does it look like this app wants long-form writing?
Is the input box large?
Are other people posting multiple paragraphs?
If so, I hit <code>⏎Enter</code>, and pray for a new paragraph ...)</p>
<p><strong>Technique 3: friction.</strong>
The word “friction” in design is often used negatively,
but friction is a powerful way to steer users towards desired behavior.
Consider how LessWrong builds the social norm of <em>long-form, async comms</em>.
In a LessWrong comment, <code>⏎Enter</code> creates a new paragraph; there is no keyboard shortcut for “Submit”.
There are no realtime notifications.
Timestamps are only accurate to the hour, not the minute.
There’s a separate feature called “shortform”.</p>
<p><strong>Technique 4: moderation.</strong>
Each user on LessWrong is also a moderator.
They can write their own “moderation guidelines”.
On that user’s posts,
their moderation guidelines are shown alongside the standard community guidelines below the comment box.</p>
<p><em>Voting</em> lets users moderate each other, reinforcing established norms.
Each post and comment on LessWrong has an up/down vote box.</p>
<p>However, voting isn’t a way to establish norms in the first place:
if bad behavior becomes a community norm,
it will also be reinforced by voting systems.
LessWrong guards against this with <em>reactions</em>.
In most apps, you can react to stuff with <em>emojis</em>.
But emojis can be very ambiguous (<a href="https://tidbits.com/2023/07/22/the-unbearable-ambiguity-of-emoji/">even causing legal issues!</a>.
On LessWrong, reactions have labels, like:
“Changed My Mind”, “Insightful”, or “Good Facilitation”.
LessWrong’s word-based reactions show users what’s valued in this community..</p>
<p><strong>Technique 5: containment.</strong>
Counter-intuitively, one way to build a norm is to build a feature for its <em>opposite norm</em>.
I call these “norm containment” features.
LessWrong has two containment features.</p>
<p>One containment feature is called <em>Shortform</em>.
“Exploratory, draft-stage, rough, and rambly thoughts are all welcome on Shortform.”
Implicit in this description is that such content is <em>not</em> generally welcome elsewhere.
The “shortform” feature says: “Normal posts are long-form and carefully edited.”</p>
<p>Another containment feature is called “agreement voting”.
Each comment on LessWrong has <em>two</em> voting axes.
The first axis is the traditional “How much do you like this?”.
The second axis is: “How much do you agree with this, separate from whether you think it’s a good comment?”.
By explicitly moving “agreement” onto a separate axis,
the forum tries to <em>unbuild</em> the “groupthink” social norm that plagues so many other forums.
The “agreement voting” feature says: “Normal voting should not consider agreement.”</p>
<p>So if you’re trying to <em>unbuild</em> a social norm,
try <em>containing</em> it by building a separate feature for it.</p>
<p>If you’re selling a collaboration product, you’re not selling features.
You’re selling norms.
Remember <a href="https://en.wikipedia.org/wiki/Google_Wave">Google Wave</a>,
where the limit was your imagination,
but no one knew what they were expected to do?
It’s justifiably dead.
When I use Slack,
I need the <em>other</em> people using Slack to use Slack how I expect people to use Slack.
If the servers go down, <code>#ops</code> is not async!</p>
<p>Instead of building features, try building norms.
“In Q3, we’re building a chat-less-after-work norm.
In Q4, we’ll deprecate the default-private-chat norm.”
Which social norms are you selling?
Do your defaults nudge people towards those norms?
Which tiny tweaks can radically change the culture?
Examine your defaults, and build your norms!</p>
Proving 1+1=2 and other advanced theorems2023-06-14T00:00:00.000Zhttps://jameshfisher.com/2023/06/14/proving-112-and-other-advanced-theorems/<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.css" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/contrib/auto-render.min.js" crossorigin="anonymous" onload="renderMath()"></script>
<script>
function renderMath() {
renderMathInElement(document.body,{
delimiters: [
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
]
});
}
</script>
<p><strong>What are numbers?
What does it mean to add them?
And why does a+b equal b+a?
In this interactive session we’ll play <a href="https://adam.math.hhu.de/#/g/hhu-adam/NNG4">the Natural Number Game</a>
to explore the foundations of math in <a href="https://leanprover.github.io/">Lean</a>.
Prerequisites: ability to count.</strong></p>
<h2>Motivation 1: can you convince a simpleton that a+b = b+a?</h2>
<p>Imagine this dialogue between you and Simplicius the simpleton:</p>
<blockquote>
<p><strong>You:</strong> <code>2+3=3+2</code>, wouldn’t you agree?</p>
<p><strong>Simplicius:</strong> Hmm, I don’t see it.</p>
<p><strong>You:</strong> Okay, <code>2+3=5</code> by calculation. And <code>3+2=5</code> by calculation. So both sides equal <code>5</code>.</p>
<p><strong>Simplicius:</strong> Okay, I see that. And it seems to work for some other examples.</p>
<p><strong>You:</strong> Great. So you can see that, for any <code>a</code> and <code>b</code>, <code>a+b=b+a</code>?</p>
<p><strong>Simplicius:</strong> Hmm, no, I don’t see that. How do you know?</p>
<p><strong>You:</strong> ... C’mon, it’s obvious!</p>
</blockquote>
<p>What can you say to Simplicius?
Can you construct an argument that would convince him?
That is, a proof?</p>
<p>But can’t we just say, “Simplicius is a rare idiot; let’s move on”?
Let’s see a second example, which might be more familiar.</p>
<h2>Motivation 2: convincing the computer your code is correct</h2>
<p>You’ve written some code that gets the first number in an array:</p>
<pre><code class="language-ts">function head(arr: number[]): number {
if (arr.length === 0) {
return -1;
} else {
return arr[0];
}
}
</code></pre>
<p>Your type <code>: number</code> claims that this function always returns a number.
I’m convinced.
But annoyingly, the computer complains:</p>
<p><img src="/assets/2023-06-14/complaint.png" alt="Type 'number | undefined' is not assignable to type 'number'" /></p>
<p>Stupid computer!
Apparently it can’t see that,
if the length of the array is not zero,
there must be a value at the first index.
Why not, and how can we prove it to the computer?</p>
<p>Our computers are like Simplicius.
We need a formal argument to convince it that <code>arr[0]</code> really is a number.</p>
<h2>Lean</h2>
<p>Enter <a href="https://leanprover.github.io/">Lean</a>.
In its own words:</p>
<blockquote>
<p>Lean is a functional programming language [... and] a theorem prover.</p>
</blockquote>
<p>How could this help us solve our problems above?</p>
<ul>
<li>
<p>To help us show that addition is commutative, Lean gives us:</p>
<ul>
<li>A language to define what <code>a+b = b+a</code> really means</li>
<li>A language to prove that claim</li>
<li>A computer program to check that proof</li>
</ul>
</li>
<li>
<p>To help us show that our <code>head</code> function is correct, Lean gives us:</p>
<ul>
<li>A language to write our <code>head</code> function</li>
<li>A language to prove that <code>head</code> really does always return a number</li>
<li>A computer program to check that proof</li>
<li>A computer program to run or compile our <code>head</code> function</li>
</ul>
</li>
</ul>
<h2><a href="https://adam.math.hhu.de/#/g/hhu-adam/NNG4">The natural number game</a></h2>
<p>In this session we’ll play <a href="https://adam.math.hhu.de/#/g/hhu-adam/NNG4">The Natural Number Game</a>.
Click it and you should see:</p>
<p><img src="/assets/2023-06-14/natural_number_game.png" alt="Natural number game" /></p>
<p>We’ll play the first 8 levels.
At the end, we will have proved that <code>a+b=b+a</code>.</p>
<h2>1.1: <code>rfl</code>, reflection, “anything is equal to itself”</h2>
<p>The most fundamental tactic, <code>rfl</code>!
Sounds useless, but it’s almost a definition of equality!</p>
<pre><code class="language-lean">example ( x y z : ℕ ) :
x * y + z = x * y + z
:= by
rfl
</code></pre>
<h2>1.2: <code>rw</code>, rewriting, “If <code>a=b</code>, then you can replace <code>a</code> with <code>b</code>”</h2>
<p>A problem from primary school:</p>
<div>
\[
\begin{aligned}
a &= 3 \\
x &= 2 \times a
\end{aligned}
\]
</div>
<p>What is the value of <em>x</em>? You might solve it by substituting:</p>
<div>
\[
\begin{aligned}
x &= 2 \times a \\
&= 2 \times 3 \\
&= 6
\end{aligned}
\]
</div>
<p>This is exactly what the <code>rw</code> tactic does.</p>
<pre><code class="language-lean">example ( x y : ℕ ) ( h : y = x + 7 ) :
2 * y = 2 * ( x + 7 )
:= by
rw [h]
rfl
</code></pre>
<h2>1.3: <code>0</code> and <code>succ</code>, defining the natural numbers</h2>
<p>Above we vaguely referred to “numbers”,
but for the rest of this session,
we’re only going to be using the “natural numbers”,
0, 1, 2, 3, ..., denoted ℕ.
Other kinds of “number” (e.g. negatives, fractions, reals, complex, ...)
don’t exist in this session!</p>
<p>We’ve been writing numbers in decimal, e.g. <code>12</code>.
But decimal is kinda complex,
so here we’ll work with a simpler notation:
125 = 1*(10^2) + 2*(10^1) + 5*(10^0)</p>
<ul>
<li><code>0</code> is a natural number.</li>
<li>If <code>n</code> is a natural number, <code>succ(n)</code> is a natural number.</li>
</ul>
<p><code>succ</code> stands for “successor”.
You can interpret it as “The number after <code>n</code>”.
So instead of writing “3”, we instead write <code>succ(succ(succ(0)))</code>.</p>
<p><strong>Exercise:</strong> how would you write “7” in this notation?</p>
<p><strong>Exercise:</strong> what number does <code>succ(succ(succ(succ(0))))</code> represent?</p>
<p>Coders — you can think of the natural numbers as a linked list structure:</p>
<pre><code class="language-ts">type Nat = 0 | { succ: Nat };
const three = { succ: { succ: { succ: 0 } } };
</code></pre>
<p>Onto the exercise:</p>
<pre><code class="language-lean">example ( a b : ℕ ) ( h : ( succ a ) = b ) :
succ ( succ a ) = succ b
:= by
rw [h]
rfl
</code></pre>
<h2>1.4: introducing addition</h2>
<p>To convince anyone that “addition is commutative”,
we first need to say what we mean by “addition”!</p>
<p>Here is how the Natural Number Game defines addition:</p>
<pre><code class="language-lean">add_zero (a : ℕ) : a + 0 = a
add_succ (a b : ℕ) : a + (succ b) = succ (a + b)
</code></pre>
<p>Or in English: “To calculate <code>a+b</code>,
repeatedly move the <code>succ</code>s from <code>b</code> onto the outside,
until there are none left on <code>b</code>.
When there are none left, you have the total.”</p>
<p>Seeing this in action:</p>
<pre><code> succ(0) + succ(succ(0))
= succ( succ(0) + succ(0) ) // add_succ
= succ(succ( succ(0) + 0 )) // add_succ
= succ(succ( succ(0) )) // add_zero
</code></pre>
<p><strong>Exercise:</strong> what numbers are we adding? And what is the result?</p>
<p>For the coders, here’s a rough equivalent in TypeScript:</p>
<pre><code class="language-ts">function add(a: Nat, b: Nat): Nat {
if (b === 0) {
return a;
} else {
return { succ: add(a, b.succ) };
}
}
</code></pre>
<pre><code class="language-lean">example :
a + succ 0 = succ a
:= by
rw [add_succ]
rw [add_zero]
rfl
</code></pre>
<h2>Interlude: induction</h2>
<p>Imagine a chess board with a single bishop on one of the black squares.
I then repeatedly move the bishop around (following normal rules: diagonal moves).
What color square will the bishop be on after move 846,245?</p>
<p>Here’s an argument that it will be on a black square:</p>
<ul>
<li>At time <code>0</code>, the bishop is on a black square.</li>
<li>If the bishop is on a black square at time <code>t</code>,
it must be on a black square at time <code>t+1</code>.
This is because a diagonal move preserves the square color.</li>
<li>Therefore, the bishop is on a black square at all times <code>t</code>.</li>
<li>In particular, it’s on a black square at time <code>846,245</code>.</li>
</ul>
<p>This is an argument by induction:</p>
<ul>
<li><code>P(0)</code></li>
<li>If <code>P(n)</code>, then <code>P(n+1)</code></li>
<li>Therefore, <code>P(n)</code> for all natural numbers <code>n</code></li>
</ul>
<h2>2.1: <code>zero_add</code> and <code>induction n</code></h2>
<p>So, we have <code>a + 0 = a</code>.
We have it because we assumed it as an axiom that we called <code>add_zero</code>.</p>
<p>But what about <code>0 + a = a</code>?
Looks very similar — but is it the same?</p>
<p>No!
It’s a separate claim, and we’ll call it <code>zero_add</code>.
Our task here is to prove it.</p>
<p>You could try using <code>rfl</code> or <code>rw [h]</code>.
But you won’t get very far.
The final tactic you need is induction,
written <code>induction n</code> in Lean.</p>
<p>Here’s the induction argument in English:</p>
<ul>
<li>
<p>If <code>a = 0</code>, then <code>0 + a = a</code>.</p>
<ul>
<li>Substitute and simplify to get <code>0 = 0</code>, true by reflection.</li>
</ul>
</li>
<li>
<p>Otherwise, assume <code>0 + a = a</code> (the “induction hypothesis”).</p>
<ul>
<li>Then we want to show <code>0 + succ(a) = succ(a)</code>.</li>
<li>Use <code>add_succ</code> to get <code>succ(0 + a) = succ(a)</code>.</li>
<li>Substitute the induction hypothesis to get <code>succ(a) = succ(a)</code>, true by reflection.</li>
</ul>
</li>
<li>
<p>Therefore, <code>0 + a = a</code> for all natural numbers <code>a</code>.</p>
</li>
</ul>
<p>Here it is in Lean:</p>
<pre><code class="language-lean">theorem MyNat.zero_add ( n : ℕ ) :
0 + n = n
:= by
induction n
rw [add_zero]
rfl
rw [add_succ]
rw [n_ih]
rfl
</code></pre>
<h2>2.2: <code>add_assoc</code></h2>
<p>From here, you don’t need to learn any new concepts!
The last three levels can all be solved with combinations of <code>rfl</code>, <code>rw</code>, and <code>induction</code>.
Good luck!</p>
<pre><code class="language-lean">theorem MyNat.add_assoc ( a b c : ℕ ) :
( a + b ) + c = a + ( b + c )
:= by
induction c
rw [add_zero]
rw [add_zero]
rfl
rw [add_succ]
rw [add_succ]
rw [add_succ]
rw [n_ih]
rfl
</code></pre>
<h2>2.3: <code>succ_add</code></h2>
<pre><code class="language-lean">theorem MyNat.succ_add ( a b : ℕ ) :
succ a + b = succ ( a + b )
:= by
induction b
rw [add_zero]
rw [add_zero]
rfl
rw [add_succ]
rw [n_ih]
rw [add_succ]
rfl
</code></pre>
<h2>2.4: <code>add_comm</code></h2>
<pre><code class="language-lean">theorem MyNat.add_comm ( a b : ℕ ) :
a + b = b + a
:= by
induction b
rw [add_zero]
rw [zero_add]
rfl
rw [add_succ]
rw [n_ih]
rw [succ_add]
rfl
</code></pre>