emacs/var/elfeed/db/data/64/64b7593bec35f3b6804479295de682d65471c75f
2022-01-03 12:49:32 -06:00

195 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<meta name="author" content="By Pierre Neidhardt" />
<title>Typed, customizable hooks</title>
<style type="text/css">
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
span.underline{text-decoration: underline;}
div.column{display: inline-block; vertical-align: top; width: 50%;}
</style>
<style type="text/css">
a.sourceLine { display: inline-block; line-height: 1.25; }
a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; }
a.sourceLine:empty { height: 1.2em; position: absolute; }
.sourceCode { overflow: visible; }
code.sourceCode { white-space: pre; position: relative; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
code.sourceCode { white-space: pre-wrap; }
a.sourceLine { text-indent: -1em; padding-left: 1em; }
}
pre.numberSource a.sourceLine
{ position: relative; }
pre.numberSource a.sourceLine:empty
{ position: absolute; }
pre.numberSource a.sourceLine::before
{ content: attr(data-line-number);
position: absolute; left: -5em; text-align: right; vertical-align: baseline;
border: none; pointer-events: all;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
a.sourceLine::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
</head>
<body>
<header>
<h1 class="title">Typed, customizable hooks</h1>
<p class="author">By Pierre Neidhardt</p>
</header>
<p>Below, we share what we've learned about hook design, extensibility, and how we've improved upon legacy hook systems in Nyxt.</p>
<p>Note: For an introduction to hooks in Nyxt, see our <a href="https://nyxt.atlas.engineer/article/hooks.org">other article on hooks</a>.</p>
<p>Definitions:</p>
<ul>
<li><p>Hook: a place in the code where we can add code dynamically.</p></li>
<li><p>Hook handler: a piece of code that can be evaluated by a hook.</p></li>
</ul>
<p>During the development of Nyxt we quickly felt dissatisfied with our initial hook implementation (based on Emacs, built with cl-hooks (<a href="https://github.com/scymtym/architecture.hooks" class="uri">https://github.com/scymtym/architecture.hooks</a>)). Hooks are an important extension feature. They need to be powerful, reliable, and easy to use.</p>
<p>No existing implementations satisfied our needs, so we decided to write our own with the following enhancements/concepts:</p>
<ul>
<li><p>Hooks are first-class objects not just any list so that we can tell if something evaluates to a hook.</p></li>
<li><p>We can <code>disable</code> hook handlers without deleting them, so that they can be toggled on and off.</p></li>
<li><p>In Emacs, handlers can be any function, including lambdas which make hooks hard to manipulate (e.g. you can't remove a lambda by its name since it has no name).</p>
<ul>
<li><p>Lambdas are effectively blackboxes once added to the hook.</p></li>
<li><p>Lambdas don't compare, so adding the same lambda twice will unintentionally stack it.</p></li>
</ul>
<p>To overcome this limitation while still allowing the use of lambdas, we created a <code>handler</code> type with a <code>name</code> slot. This allows us to fix both issues: The handler can be associated to a lambda and handlers can be compared.</p></li>
<li><p>In Emacs, hook handlers are always run sequentially and do not return anything. There is no way to customize how they are run and what return value we expect. In particular, they do now allow for composing handlers which would in effect turn the hook into a pipeline!</p>
<p>Our hook class has a <code>combination</code> slot which accepts a function that schedules the handler execution and collects the return value(s).</p></li>
<li><p>In Emacs, hooks cannot be typed which leads to &quot;hard to catch&quot; errors. E.g., when the user tries to add a function of the wrong type to a hook. Therefore, we've added macros to help define <em>typed hooks</em>.</p></li>
<li><p>Emacs hooks are simple lists, they can't be <em>attached</em> to a given object. In our hook system we've added support for globally-accessible hooks, as well as object-bound hooks.</p></li>
</ul>
<p>Our work has now been <a href="https://github.com/ruricolist/serapeum/issues/41">merged in Serapeum</a> and can be accessed from the <code>serapeum/contrib/hooks</code> package.</p>
<p>Let's have a look at the implementation details!</p>
<h1 id="declaring-new-hook-types">Declaring new hook types</h1>
<p>We provide a <code>define-hook-type</code> macro. For instance</p>
<div class="sourceCode" id="cb1" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb1-1" data-line-number="1">(define-hook-type string-&gt;string (<span class="kw">function</span> (<span class="kw">string</span>) <span class="kw">string</span>))</a></code></pre></div>
<p>will generate</p>
<ul>
<li><p>a <code>handler-string-&gt;string</code> class,</p></li>
<li><p>a <code>hook-string-&gt;string</code> class,</p></li>
<li><p>a <code>make-handler-string-&gt;string</code> function,</p></li>
<li><p>a <code>make-hook-string-&gt;string</code> function,</p></li>
<li><p>a <code>add-hook</code> method specialized over <code>hook-string-&gt;string</code> and <code>handler-string-&gt;string</code>.</p></li>
</ul>
<p>Say we've got a <code>#'my-downcase</code> function of type <code>(function (string) string)</code>. Now we can create a hook and add <code>#'my-downcase</code> to it.</p>
<div class="sourceCode" id="cb2" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb2-1" data-line-number="1">(<span class="kw">defvar</span><span class="fu"> test-hook </span>(hooks:make-hook-string-&gt;string))</a>
<a class="sourceLine" id="cb2-2" data-line-number="2"></a>
<a class="sourceLine" id="cb2-3" data-line-number="3">(hooks:add-hook test-hook</a>
<a class="sourceLine" id="cb2-4" data-line-number="4"> (make-handler-string-&gt;string #&#39;my-downcase))</a></code></pre></div>
<p>The library comes with the following predefined hook types:</p>
<div class="sourceCode" id="cb3" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb3-1" data-line-number="1">(define-hook-type void (<span class="kw">function</span> ()))</a>
<a class="sourceLine" id="cb3-2" data-line-number="2">(define-hook-type string-&gt;string (<span class="kw">function</span> (<span class="kw">string</span>) <span class="kw">string</span>))</a>
<a class="sourceLine" id="cb3-3" data-line-number="3">(define-hook-type number-&gt;number (<span class="kw">function</span> (<span class="kw">number</span>) <span class="kw">number</span>))</a>
<a class="sourceLine" id="cb3-4" data-line-number="4">(define-hook-type any (<span class="kw">function</span> (&amp;<span class="kw">rest</span> <span class="kw">t</span>)))</a></code></pre></div>
<ul>
<li>The <code>void</code> hook type is for handlers that don't return anything. This is useful when we want to use handlers for their side effects.</li>
<li>The <code>any</code> hook type accepts any handler type.</li>
</ul>
<h1 id="lambdas-as-handlers">Lambdas as handlers</h1>
<p>You don't always want to declare top-level functions before adding a handler to a hook. So the above example could be replaced with the following:</p>
<div class="sourceCode" id="cb4" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb4-1" data-line-number="1">(<span class="kw">defvar</span><span class="fu"> test-hook </span>(hooks:make-hook-string-&gt;string))</a>
<a class="sourceLine" id="cb4-2" data-line-number="2"></a>
<a class="sourceLine" id="cb4-3" data-line-number="3">(hooks:add-hook test-hook</a>
<a class="sourceLine" id="cb4-4" data-line-number="4"> (make-handler-string-&gt;string (<span class="kw">lambda</span> (s) (<span class="kw">string-downcase</span> s))</a>
<a class="sourceLine" id="cb4-5" data-line-number="5"> <span class="bu">:name</span> &#39;my-downcase))</a></code></pre></div>
<h1 id="disabling-handlers">Disabling handlers</h1>
<p>See the <code>disable-hook</code> and <code>enable-hook</code> methods which accept multiple handler names as argument.</p>
<div class="sourceCode" id="cb5" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb5-1" data-line-number="1">(disable-hook test-hook &#39;my-downcase)</a>
<a class="sourceLine" id="cb5-2" data-line-number="2">(run-hook test-hook <span class="st">&quot;FOO&quot;</span>)</a>
<a class="sourceLine" id="cb5-3" data-line-number="3"><span class="co">; =&gt; &quot;FOO&quot;</span></a>
<a class="sourceLine" id="cb5-4" data-line-number="4">(enable-hook test-hook &#39;my-downcase)</a>
<a class="sourceLine" id="cb5-5" data-line-number="5">(run-hook test-hook <span class="st">&quot;FOO&quot;</span>)</a>
<a class="sourceLine" id="cb5-6" data-line-number="6"><span class="co">; =&gt; &quot;foo&quot;</span></a></code></pre></div>
<p>Not passing any handler name is equivalent to selecting all handlers.</p>
<p>Disabling a handler and re-enabling it moves it to the front of the handler list, which may change the handler order. Keep this in mind if execution order matters!</p>
<h1 id="handler-combinations">Handler combinations</h1>
<p>A hook can be configured in how it runs its handlers. Example:</p>
<div class="sourceCode" id="cb6" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb6-1" data-line-number="1">(<span class="kw">defvar</span><span class="fu"> test-hook </span>(hooks:make-hook-number-&gt;number</a>
<a class="sourceLine" id="cb6-2" data-line-number="2"> :handlers (<span class="kw">list</span> #&#39;<span class="dv">1</span>+ #&#39;square)</a>
<a class="sourceLine" id="cb6-3" data-line-number="3"> :combination #&#39;hooks:combine-composed-hook))</a>
<a class="sourceLine" id="cb6-4" data-line-number="4"></a>
<a class="sourceLine" id="cb6-5" data-line-number="5">(run-hook test-hook <span class="dv">2</span>)</a>
<a class="sourceLine" id="cb6-6" data-line-number="6"><span class="co">;; =&gt; 9</span></a></code></pre></div>
<p>In the above the result of the first handler is passed as the input to the second and so on. The final result is the output of the last handler.</p>
<p>The library provides a few default combination functions:</p>
<ul>
<li><p><code>default-combine-hook</code>: Return the list of the results of the HOOK handlers applied to the arguments.</p></li>
<li><p><code>combine-hook-until-failure</code>: As above but stop the list at the first handler that returns nil.</p></li>
<li><p><code>combine-hook-until-success</code>: As above but return the first non-nil result.</p></li>
<li><p><code>combine-composed-hook</code>: This is the handler from the above example.</p></li>
</ul>
<h1 id="typing">Typing</h1>
<p>A common pitfall that keeps tripping Emacs users is when a handler is added to a hook that takes an argument of an unexpected type. This kind of error is usually only caught at runtime.</p>
<p>This is why we've introduced typing in our library. In the [[Declaring new hook types]] section we saw that defining a hook type generates a new <code>add-hook</code> method that's specialized over the specified types. Since there is only one such method, it's only possible to call <code>add-hook</code> over the right handler object, which is created by the associated typed handler constructor (e.g. <code>handler-string-&gt;string</code>).</p>
<p>Common Lisp compilers like <a href="http://www.sbcl.org/">SBCL</a> perform function type-checking at compile time, which allows us to catch errors early when the user tries to create a handler over a function of the wrong type.</p>
<h1 id="global-hooks-and-object-bound-hooks">Global hooks and object-bound hooks</h1>
<p>The <code>define-hook</code> function allows for registering hooks globally without binding them to global variables.</p>
<p>With just a type and a name, it defines a global hook which can then be accessed with the <code>find-hook</code>:</p>
<div class="sourceCode" id="cb7" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb7-1" data-line-number="1">(hooks:define-hook &#39;hooks:hook-number-&gt;number &#39;foo)</a>
<a class="sourceLine" id="cb7-2" data-line-number="2">(hooks:find-hook &#39;foo)</a>
<a class="sourceLine" id="cb7-3" data-line-number="3"><span class="co">;; #&lt;HOOKS:HOOK-NUMBER-&gt;NUMBER {1007537C83}&gt;</span></a>
<a class="sourceLine" id="cb7-4" data-line-number="4"><span class="co">;; T</span></a></code></pre></div>
<p>You can also bind a hook over an object. This hook is unique to the object.</p>
<div class="sourceCode" id="cb8" data-org-language="lisp"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><a class="sourceLine" id="cb8-1" data-line-number="1">(hooks:define-hook &#39;hooks:hook-number-&gt;number &#39;foo</a>
<a class="sourceLine" id="cb8-2" data-line-number="2"> :object #&#39;mul2)</a>
<a class="sourceLine" id="cb8-3" data-line-number="3">(hooks:find-hook &#39;foo #&#39;mul2)</a>
<a class="sourceLine" id="cb8-4" data-line-number="4"><span class="co">;; #&lt;HOOKS:HOOK-NUMBER-&gt;NUMBER {100757FC43}&gt;</span></a>
<a class="sourceLine" id="cb8-5" data-line-number="5"><span class="co">;; T</span></a></code></pre></div>
<h1 id="conclusion">Conclusion</h1>
<p>We've been using our novel hook system in Nyxt for a while now and it's proven both robust and flexible. It has removed a whole class of errors from user configurations!</p>
<p>We hope these design decisions will be met with success. It'd be great to see this kind of sophistication in Emacs and other extensible programs!</p>
<p>Thanks for reading :-)</p>
</body>
</html>