<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <title>Technomancy</title>
  <id>tag:technomancy.us,2007:blog/</id>
  <link href="https://technomancy.us/atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://technomancy.us/" rel="alternate" type="text/html"/>
  <updated>2026-05-20T15:47:31+00:00</updated>



        <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:204</id>
    <published>2026-04-25T17:08:11Z</published>
    <updated>2026-04-25T17:08:11Z</updated>

    <link href="https://technomancy.us/204" rel="alternate" type="text/html"/>
    <title>in which more paths are charted towards code independence</title>
    <content type="html">


&lt;p&gt;As the ninth anniversary approaches
  of &lt;a href="https://github.com/technomancy/circleci.el"&gt;the last
  project repository I created on Github&lt;/a&gt;, &lt;!-- 12 May 2017 --&gt; it's
  been so encouraging to see more and more projects migrate away from
  Microsoft Github.  I love seeing the rise
  of &lt;a href="https://codeberg.org"&gt;Codeberg&lt;/a&gt;, not the least
  because it's &lt;a href="https://join.codeberg.org/"&gt;a
  democratically-run non-profit&lt;/a&gt; which isn't subject to the whims
  of the extractive arm of an unaccountable exploitative mega-corporation.&lt;/p&gt;

&lt;p id="rfn1"&gt;If you're currently hosting a project on Github and you recognize
  the &lt;a href="https://www.theatlantic.com/technology/archive/2020/01/ice-contract-github-sparks-developer-protests/604339/"&gt;harm&lt;/a&gt;
  of &lt;a href="https://www.theregister.com/2023/06/09/github_copilot_lawsuit/"&gt;Microsoft's&lt;/a&gt; &lt;a href="https://www.theverge.com/2024/6/28/24188391/microsoft-ai-suleyman-social-contract-freeware"&gt;monopoly&lt;/a&gt;,
  it's undeniable that Codeberg is your quickest and easiest offramp to an
  empowering and pro-user place.  But at the same time, I don't see
  Codeberg as the be-all and end-all of Github replacements, for a
  couple reasons. Most obviously, putting all your eggs in one basket
  isn't ideal, even if it's a much better basket than the one we used
  to have. We shouldn't replace Github with &lt;em&gt;one&lt;/em&gt; site at all;
  we need the strength and resilience that only comes with
  diversity&lt;sup&gt;&lt;a href="#fn1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p id="rfn2"&gt;But beyond that, I'm reminded me of the way that some dominant
  ideas can become so dominant that it becomes difficult to even
  imagine alternatives to them. Forgejo (the software behind Codeberg)
  is unapologetically a Github clone. For most people in software
  development, Github has been synonymous with development workflows
  for so long that other ways of thinking have not only languished,
  but have started to feel nearly inconceivable. Even for people who
  want very much to get away from Microsoft, the design decisions made
  for Github follow you around from Gitlab to Bitbucket to
  Codeberg/Forgejo&lt;sup&gt;&lt;a href="#fn2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;This is a pattern you see over and over with open source
  alternatives to dominant systems; for a long time GNOME and KDE were
  just trying really hard to build the exact same thing as Windows,
  but without Microsoft. Mastodon is trying really hard to be Twitter
  without Twitter. When a company dominates the market, they capture
  not only their own users but even the imaginations of people
  actively resisting them. It usually takes a great deal of time and
  effort to carve out new patterns of thinking.&lt;/p&gt;

&lt;p&gt;If you just want to get off Github, that's fine; you should do that
  first, and you don't need to tackle the job of learning a new flow
  right away. But I want to make space for people to think about
  alternatives. What does a post-github world look like?  What other
  flows could we use to support project development and contribution?
  When people talk about moving off Github, the first objection is
  always "but &lt;em&gt;everyone&lt;/em&gt; has an account there already!" And
  this is a fair point; no one wants to create Yet Another
  Account. I'm only really willing to consider alternatives that don't
  require creating another account, especially not in a distributed
  system where each project might be on its own server.&lt;/p&gt;

&lt;img src="/i/telephones.jpg" alt="bright neon sign saying 'telephones'" /&gt;

&lt;p&gt;Let's start by going back to the basics. Simon Tatham
  has &lt;a href="https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/git-no-forge/"&gt;written
  up a guide&lt;/a&gt; describing how he uses Git without &lt;em&gt;any&lt;/em&gt;
  forge-like software involved. He keeps his repositories on his own
  server with nothing but a read-only &lt;kbd&gt;gitweb&lt;/kbd&gt; rendering
  their contents as HTML. If you want to contribute a patch, you can
  publish your own clone on your host of choice and let him know (via
  email, chat, social media, or whatever) which branch to pull
  from. This method works best for projects with a single maintainer
  and a small community. It's a good starting place for most
  projects. Then when you have more going on, you might want to add a
  little more structure to help keep track of things.&lt;/p&gt;

&lt;p&gt;And then there's projects
  like &lt;a href="https://radicle.xyz/"&gt;Radicle&lt;/a&gt; offer a flow that
  manages to work in a peer-to-peer way with no server involved. Being
  fully peer-to-peer, your machine gets all the data synced locally,
  so it works great when offline too, and you don't have to create yet
  another account on another system. This is a bold vision of how the
  future of development might look.&lt;/p&gt;

&lt;p&gt;But when I was searching for an alternative
  for &lt;a href="https://fennel-lang.org"&gt;Fennel&lt;/a&gt; I went looking to
  the past instead. Back when I was fresh out of university and an
  enthusiastic &lt;a href="https://subversion.apache.org/"&gt;Subversion&lt;/a&gt;
  user, I set
  up &lt;a href="https://web.archive.org/web/20070721082835/http://dev.technomancy.us/"&gt;an
  instance of Trac&lt;/a&gt;, a web-based version control, ticket, and wiki
  system. I used it for all my projects, but it was mostly just a
  personal organization tool as at the time I did not have any
  projects that got more than one or two patches from other
  contributors.  Then in 2007 I &lt;a href="/92"&gt;switched from Subversion
  to Git&lt;/a&gt;, and had to drop Trac. A few months later
  in &lt;a href="https://mako.cc/writing/hill-free_tools.html"&gt;a moment
  of weakness&lt;/a&gt;, I started &lt;a href="/105"&gt;putting my projects on
  Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Over the years I lost track of Trac, largely due to the fact that
  since it was made by fans of Subversion, it took
  them &lt;a href="https://trac.edgewall.org/wiki/TracGit"&gt;several years
  to add support for Git&lt;/a&gt;, by which time most people had switched
  away from it. But Trac offered something really special. The
  individual features around ticket reporting and wiki pages are
  impressive, but most of all, I see Trac as the pinnacle of web app
  design from the golden age of progressive enhancement.&lt;/p&gt;

&lt;p id="rfn3"&gt;What does that mean exactly? Well, a few years after Trac dropped
  off my radar, we saw the rise of React, GraphQL, and the Single-Page
  App (SPA). While there are certain type of sites for which the SPA
  interaction style makes sense, these technologies saw &lt;em&gt;mass&lt;/em&gt;
  adoption across every single kind of web app, regardless of whether
  they were a good fit. As the software industry is largely driven by
  trends and fashion, React's "cool factor" led to a mass amnesia
  regarding how to make normal, well-designed web apps made of HTML
  with a sprinkling of scripting on top and everything became one big
  tangled, brittle mass of Javascript that completely keeled over when
  scripting wasn't available. But Trac got it right: the server
  generates HTML (quickly&lt;sup&gt;&lt;a href="#fn3"&gt;3&lt;/a&gt;&lt;/sup&gt;!)  and sends
  it to you, and in a few cases uses some &lt;em&gt;optional&lt;/em&gt; scripting
  to get things like live previews. You can use it
  with &lt;a href="https://www.netsurf-browser.org/"&gt;Netsurf&lt;/a&gt; or lynx!&lt;/p&gt;

&lt;img src="/i/charlotte-bike-lane.jpg" alt="bike lane" class="right" /&gt;

&lt;p&gt;Anyway, when I set up &lt;a href="https://dev.fennel-lang.org"&gt;Fennel's
    Trac&lt;/a&gt;, I was mostly impressed with how easy it was to get
  going, but there were a handful of things that were weirdly off. The
  most glaring is that even in 2026, the default setup is still to use
  Subversion instead of Git. Support for Git is now built-in at least,
  but for some reason you still have to enable it in the config. An
  easy change, but a strange one.&lt;/p&gt;

&lt;p id="rfn4"&gt;Secondly,
  the &lt;a href="https://trac.edgewall.org/wiki/WikiFormatting"&gt;format
    used for the wiki pages&lt;/a&gt; is ... somewhat dated. It's
  not &lt;em&gt;bad&lt;/em&gt; or anything, but in 2026 using anything other
  than Markdown just feels pointlessly
  contrarian&lt;sup&gt;&lt;a href="#fn4"&gt;4&lt;/a&gt;&lt;/sup&gt;. There does
  exist &lt;a href="https://trac-hacks.org/wiki/MarkdownMacro"&gt;a plugin
    that lets you write in Markdown&lt;/a&gt;, but it only lets you create
  Markdown sections of pages that otherwise use the standard
  formatting. There's no way to use Markdown as the default
  formatter for wiki pages, issues, and comments. At least, there
  wasn't until &lt;a href="https://git.sr.ht/~technomancy/oops-all-markdown"&gt;I
    wrote my own plugin to do it&lt;/a&gt; based on the existing plugin.&lt;/p&gt;

&lt;p&gt;The third problem is maybe the weirdest: the default setup has no
  way to create user accounts!
  It &lt;a href="https://trac.edgewall.org/wiki/TracAuthenticationIntroduction"&gt;delegates
  login completely to Apache&lt;/a&gt;. In order to add a user, the admin
  has to SSH into the machine and edit an &lt;kbd&gt;htpasswd&lt;/kbd&gt; file in
  a text editor. Needless to say, if you're using this for any kind of
  community project, (or if you don't use Apache) this is borderline
  useless. There are some plugins that add workable authentication
  systems, but all the existing ones either require all users to sign
  up for (you guessed it) Yet Another Account or require you to tie
  your logins to a huge company like Google or Github. Neither of
  those paths were acceptable to me.&lt;/p&gt;

&lt;p id="rfn5"&gt;When I set this up, I had just recently been working
    on &lt;a href="https://fedibot.club"&gt;Fedibot club&lt;/a&gt;, an unrelated
    project. For that, I had implemented a Fediverse-based OAuth
    system that allows users to specify their server where they
    already have an account, and use that existing
    account&lt;sup&gt;&lt;a href="#fn5"&gt;5&lt;/a&gt;&lt;/sup&gt; to log in. Rather than
    having a fixed set of servers that you're allowed to authenticate
    with, it dynamically registers an OAuth client for any new server
    it's never seen before&lt;sup&gt;&lt;a href="#fn6"&gt;6&lt;/a&gt;&lt;/sup&gt;. So I took
    this exact flow
    and &lt;a href="https://git.sr.ht/~technomancy/trac-fedi/"&gt;ported it
    to Trac&lt;/a&gt;. Despite somehow not really ever having written any
    Python before in my life, I found it surprisingly easy to
    implement, using an existing hard-coded OAuth plugin as a
    guide. It clocks in at 124 lines of code.&lt;/p&gt;

&lt;p&gt;When I'm trying out a new system, sometimes I run into some issues
  and think, "OK, I can put some elbow grease into this and get
  something working" but I usually expect it'll be a little slipshod
  and require putting up with things getting janky or ongoing tweaks.
  I encountered a twofold surprise here: one being simply that the
  fixes were so &lt;em&gt;easy&lt;/em&gt; for me to make as someone who knows
  nothing about either Trac or Python, and the second being that once
  those few fixes landed, that pretty much took care of everything,
  and the remaining things worked basically flawlessly. It's a little
  disappointing that these things couldn't be addressed upstream in
  the project, but hey, I'll take the win.&lt;/p&gt;

&lt;p&gt;Anyway, whether you end up using Trac or not, I hope you
  can find something that works for your project. Good luck!&lt;/p&gt;

&lt;hr&gt;

&lt;p id="fn1"&gt;[&lt;a href="#rfn1"&gt;1&lt;/a&gt;] I've seen a lot of my friends
  stand up their own little Forgejo servers, and I love this. However,
  I believe there's a big missed opportunity here too. If you don't
  have an account on Alex's Random Forgejo, you can use OAuth to log
  in with an account on another server, which is great.&lt;/p&gt;

&lt;p&gt;However, users are limited just to servers where the &lt;em&gt;site's
  admins&lt;/em&gt; have manually registered OAuth clients! Alex might allow
  users to log in using their Github account, and Alex might allow
  users to log in using their Codeberg account, but it's very unlikely
  Alex's Random Forgejo will allow users to log in with their account
  on Mel's Random Forgejo, so there is still some centralizing gravity
  at play. Fortunately this is very easy to
  fix! &lt;a href="https://git.sr.ht/~technomancy/min-web-fennel/#fediverse-specific-oauth-flow"&gt;My
  implementation of OAuth login using dynamic client registration&lt;/a&gt;
  clocks in at around a hundred lines of code. If it weren't for
  Forgejo being implemented in Golang, I'd be inclined to give a shot
  at implementing it. Perhaps some enterprising reader would be up for
  it!&lt;/p&gt;

&lt;p id="fn2"&gt;[&lt;a href="#rfn2"&gt;2&lt;/a&gt;] But
  not &lt;a href="https://git.sr.ht"&gt;Sourcehut&lt;/a&gt;! And I do like
  Sourcehut overall; they have the best CI of any system I've used,
  and page load times are unparalleled. Its email-based contribution
  flow can be kind of awkward, but I have to applaud them for trying
  something different..&lt;/p&gt;

&lt;p id="fn3"&gt;[&lt;a href="#rfn3"&gt;3&lt;/a&gt;] I run my Trac instance on my home
  server, which is a Core 2 Duo Thinkpad from 2010. Trac uses about
  160MB of RAM, and loads pages in consistently less than 200
  milliseconds; dramatically faster than Github. (Your experience
  might be slower than this due to more network hops, but outside
  extreme cases it should still be a good deal better than Github.)
  When I first got it set up, it felt slow due to an attack of scraper
  bots from LLM companies, but once I
  installed &lt;a href="https://iocaine.madhouse-project.org/"&gt;Iocaine&lt;/a&gt;
  that took care of it.&lt;/p&gt;

&lt;p id="fn4"&gt;[&lt;a href="#rfn4"&gt;4&lt;/a&gt;] Don't get me wrong; I'm not saying
  Markdown is &lt;em&gt;good&lt;/em&gt;. It's a lot like English: I'm not using it
  because it's good; I'm using it because it's both tolerable and
  ubiquitous.&lt;/p&gt;

&lt;p id="fn5"&gt;[&lt;a href="#rfn5"&gt;5&lt;/a&gt;] As a grudging compromise, I've
  installed &lt;a href="https://github.com/trac-hacks/trac-github/"&gt;a
  Github OAuth plugin&lt;/a&gt; for users who don't have a Fediverse
  account, but like ... come on, people.&lt;/p&gt;

&lt;p id="fn6"&gt;[&lt;a href="#rfn5"&gt;6&lt;/a&gt;] If you're interested in reading
  more about how this works, I've got a
  &lt;a href="https://git.sr.ht/~technomancy/min-web-fennel/#fediverse-specific-oauth-flow"&gt;fairly
    thorough write-up&lt;/a&gt; that should be useful to anyone hoping to
  build a similar system. It's easier than it sounds! I'd love to see
  more people using this strategy to break away from corporate monopolies.&lt;/p&gt;

</content></entry>


        <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:203</id>
    <published>2025-04-08T15:33:17Z</published>
    <updated>2025-04-08T15:33:17Z</updated>

    <link href="https://technomancy.us/203" rel="alternate" type="text/html"/>
    <title>in which web pages are served but also fetched</title>
    <content type="html">


&lt;p&gt;I'm not much of a "web programmer". But when I do make web sites, I
  mostly like to write programs that spit out HTML files, and then put
  them on my web server. It's simple, fast, and gets the job done for
  most of the web sites I've needed. Every so often I come
  across &lt;a href="/196"&gt;something that needs a little more
  functionality&lt;/a&gt; than static pages can offer, and in those cases
  I'd put up a 15-line CGI script to save off some data for later
  analysis or whatever. Works great, and I highly recommend it when it
  fits the problem at hand.&lt;/p&gt;

&lt;p&gt;But what about sites that are &lt;em&gt;mostly dynamic&lt;/em&gt;? Using CGI in
  that case might not be a great fit. Or what if you need to use
  a server that doesn't support CGI at all? If you're already using
  &lt;a href="https://nginx.org/"&gt;nginx&lt;/a&gt; for your site,
  then &lt;a href="https://openresty.org/en/"&gt;OpenResty&lt;/a&gt; looks like a
  compelling choice. But I haven't used nginx in ages. I was curious
  about more standalone alternatives. That's when I
  discovered &lt;a href="https://github.com/bakpakin/moonmint/"&gt;moonmint&lt;/a&gt;.&lt;/p&gt;

&lt;img src="/i/brinchang.jpg" alt="fog covered mountains in early morning"&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: since writing this post, I've switched
  over to using &lt;a href="https://daurnimator.github.io/lua-http/"&gt;lua-http&lt;/a&gt;
  instead of moonmint as it is receiving ongoing maintenance. I gave
  &lt;a href="https://conf.fennel-lang.org/2025#video1"&gt;a
    talk on this&lt;/a&gt; at FennelConf 2025.&lt;/p&gt;

&lt;p id="rfn1"&gt;The moonmint web framework was created
  by &lt;a href="https://bakpakin.com/"&gt;Calvin Rose&lt;/a&gt;, who also created
  the first version of Fennel&lt;sup&gt;&lt;a href="#fn1"&gt;1&lt;/a&gt;&lt;/sup&gt;. It is
  written in Lua, but it's very easy to use from Fennel. Now of course
  Lua does not have any functionality built-in from which you can make
  a web server. You can't open a socket without &lt;em&gt;some&lt;/em&gt;
  third-party code. Often people
  use &lt;a href="https://github.com/lunarmodules/luasocket"&gt;luasocket&lt;/a&gt;
  for this, but moonmint takes a different approach and
  uses &lt;a href="https://github.com/luvit/luv"&gt;luv&lt;/a&gt; instead, which
  is a library that provides bindings
  to &lt;a href="https://docs.libuv.org/en/v1.x/"&gt;libuv&lt;/a&gt;, a
  multi-platform asynchronous I/O framework.&lt;/p&gt;

&lt;p&gt;My goal was to build &lt;a href="https://search.technomancy.us"&gt;a
    hyper-personalized search engine&lt;/a&gt; that indexed just the links
  I've posted. This felt like a good fit for moonmint. I found
  that &lt;a href="https://www.sqlite.org/fts5.html"&gt;SQLite contains a
    very capable full-text search engine called fts5&lt;/a&gt; and decided
  to use that to store the pages. For the final piece of the puzzle,
  I used &lt;a href="https://pandoc.org/"&gt;pandoc&lt;/a&gt; to convert HTML
  into plain text suitable for indexing.&lt;/p&gt;

&lt;p&gt;Once I had all the pieces put together, the first working version
  of the site came together very quickly. I fed it a list of URLs from
  my bookmarks file and was able to index and query them with ease in
  around 160 lines of code. But the problem with this first version is
  that I was feeding the URLs directly to pandoc, which was very
  convenient when the pages loaded, but pandoc would index them
  regardless of the response code, so I got a &lt;em&gt;lot&lt;/em&gt; of 404
  pages in the index. So I needed to start making these HTTP requests
  myself in order to ensure that only successful requests got their
  pages included.&lt;/p&gt;

&lt;p&gt;And this is where things started to get tricky, because making
  HTTPS requests on the Lua runtime is ... unfortunately a bit of an
  achilles' heel at this point. There are a lot of options, but none
  of them are without significant problems.&lt;/p&gt;

&lt;ul&gt;
  &lt;li id="rfn2"&gt;The moonmint framework actually came with a module for making
    HTTPS requests, but its relied on a luarocks dependency for its
    TLS functionality, and that dependency has since introduced
    incompatible changes. Due to luarocks version philosophy of "just
    give me whatever version you feel like", it has stopped working
    with moonmint. These kinds of problems are predictably somewhat
    common with luarocks, so I try to use other ways to get libraries
    when possible&lt;sup&gt;&lt;a href="#fn2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;

  &lt;li&gt;Installing via apt-get instead of luarocks will tend to get you
    more stable results. There are two Lua HTTP clients available in
    apt-get on debian stable, which is what I run on my machines:
    lua-http and lua-curl. Neither of these are packaged for use with
    Lua 5.4.&lt;/li&gt;

  &lt;li&gt;I've used luasocket for projects in the past; mostly for IRC
    stuff. It's pretty good for that! It doesn't do TLS on its own
    tho. There is a luasec library that offers a similar API but with
    added TLS capabilities. It also wraps luasocket's HTTP client in
    an HTTPS variant. However, luasec &lt;em&gt;does not perform any
    certificate verification by default!&lt;/em&gt; This makes its
    out-of-the-box behavior very unsafe, and I have to recommend
    against using it for anything. While it is possible to enable
    certificate verification, leaving such an unsafe default shows
    very poor decision-making, and I would not trust this library to
    do other things right if they get such a basic thing wrong.&lt;/li&gt;

  &lt;li id="rfn3"&gt;The latest one I've seen
    is &lt;a href="https://github.com/love2d/lua-https"&gt;lua-https&lt;/a&gt;,
    created by the folks behind &lt;a href="https://love2d.org"&gt;LÖVE&lt;/a&gt;,
    the 2D game framework. My project has nothing to do with games,
    but the LÖVE folks tend to do a good job with their APIs, and I
    figured it would be worth giving it a shot. It's not available in
    apt-get yet, but unlike most Lua libraries that have C code
    included, it's pretty easy to build yourself, so I added it as
    a git submodule
    and &lt;a href="https://git.sr.ht/~technomancy/search/tree/main/item/Makefile#L16"&gt;threw
    a handful of lines into my Makefile to compile
    it&lt;/a&gt;&lt;sup&gt;&lt;a href="#fn3"&gt;3&lt;/a&gt;&lt;/sup&gt;.  The only downside I've
    seen so far with this library is that it does not support
    streaming the responses. For my indexer, it would be nice if we
    could look at the HTTP headers before fetching the whole request,
    so I could skip known-unusable mime types like videos. However,
    given the alternatives, this seemed like the best choice for
    me.&lt;/li&gt;
&lt;/ul&gt;

&lt;img src="/i/kl-monorail.jpg" alt="monorail with neon skyscraper"
     class="right" /&gt;

&lt;p id="rfn4"&gt;My situation is a bit unusual. I have indexing runs
  which happen in batch jobs completely disconnected from the web
  app. They populate the SQLite database, which is only ever queried
  from the web app. The web app doesn't make any of its own HTTPS
  requests. If it did, using lua-https would mean that those requests
  would block until they completed, during which the server could not
  process any other requests. The site I'm building is a
  hyper-personalized search engine. You're welcome to use it too, of
  course, but it's really most useful to me. It has a target audience
  of one. So I'm not at all concerned about problems like blocking the
  main thread. If I were, I would need to replace it with a
  non-blocking equivalent, ideally tied into the event loop of
  luv.&lt;sup&gt;&lt;a href="#fn4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;My search engine is also unusual because its indexer does not
  really "crawl" at all; that is, it fetches a list of pages but does
  not recursively branch out to the pages that those pages link
  to. That means the index remains pretty small. For a general-purpose
  search engine, this would render it more or less useless, but for a
  hyper-personalized search engine, it's honestly not that bad, and it
  makes it &lt;em&gt;much&lt;/em&gt; simpler to
  code. I've &lt;a href="https://search.technomancy.us/why"&gt;gone into
    more detail on the site about why I built my own search engine in
    this unusual way&lt;/a&gt; so I won't get too much more into that here,
  but all my URLs so far come either from my bookmarks file, or from
  links I have posted &lt;a href="https://hey.hagelb.org/@technomancy"&gt;on
    my social
    media&lt;/a&gt; &lt;a href="https://icosahedron.website/@technomancy"&gt;accounts&lt;/a&gt;.
  My next planned step is to start indexing links that are posted by
  accounts that I follow.&lt;/p&gt;

&lt;p id="rfn5"&gt;Requests right now are &lt;em&gt;very&lt;/em&gt; fast; between 20 and 80
  milliseconds&lt;sup&gt;&lt;a href="#fn5"&gt;5&lt;/a&gt;&lt;/sup&gt; depending on how many
  results there are. I would expect that hundreds of concurrent users
  would be supported easily without much noticeable slowdown. But if I
  was interested in scaling this up beyond that, I would put several
  different lua processes behind a load balancer. I put the server
  behind &lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt; in order to do
  the TLS termination and to let me have multiple sites on the same
  server, but I only run one application server for the search
  engine. If you needed to handle more traffic, balancing across many
  different server processes across a few different ports is easy to
  do with a small tweak to the Caddy config.&lt;/p&gt;

&lt;p&gt;Anyway, if you are interested in how to build a simple web app in
  Fennel, &lt;a href="https://git.sr.ht/~technomancy/search/tree/main/item/main.fnl"&gt;take
  a look at the code&lt;/a&gt;! At just under 350 lines, it's pretty
  straightforward and readable. I'm not sure I would necessarily
  recommend Moonmint over Openresty for web applications in general, but
  if you're doing something with simple I/O requirements and want a
  solution with fewer moving parts, give it a try.&lt;/p&gt;

&lt;hr&gt;

&lt;p id="fn1"&gt;[&lt;a href="#rfn1"&gt;1&lt;/a&gt;] When I first looked at moonmint, I
  had an eerie sense of deja-vu. It was a project written in Lua by
  Calvin Rose in 2016 which had not seen any further development since
  2016. Those exact same circumstances could also
  describe &lt;a href="https://github.com/bakpakin/Fennel/graphs/contributors"&gt;Fennel&lt;/a&gt;
  when I first found it! It's about 1500 lines of
  code. &lt;a href="https://github.com/technomancy/moonmint/"&gt;My own fork
    of moonmint&lt;/a&gt; removes the non-functioning OpenSSL stuff.&lt;/p&gt;

&lt;p id="fn2"&gt;[&lt;a href="#rfn2"&gt;2&lt;/a&gt;] There's a newer project
  called &lt;a href="https://gitlab.com/andreyorst/deps.fnl"&gt;deps.fnl&lt;/a&gt;
  which tries to work around the problems inherent in the luarocks
  model. I did not try it for this because I needed a few dependencies
  which it couldn't handle like pandoc and a lua library for
  robots.txt that was kept in svn. (yes, really!) But if you need a
  library that's hard to use without luarocks but want an actual
  reliable build, you should give it a look.&lt;/p&gt;

&lt;p id="fn3"&gt;[&lt;a href="#rfn3"&gt;3&lt;/a&gt;] The advantage of this approach is
  it makes it pretty easy to build the dependency with static
  linking. When you're writing a CLI tool to distribute, this can be
  really handy. I used this technique
  for &lt;a href="https://git.sr.ht/~technomancy/squirtleci"&gt;another
    Fennel project&lt;/a&gt; of mine in the past, but it wasn't relevant for
  a web application.&lt;/p&gt;

&lt;p id="fn4"&gt;[&lt;a href="#rfn4"&gt;4&lt;/a&gt;] This is one place where Openresty
  might be a better choice; it's already got non-blocking
  functionality for HTTPS requests and database queries and whatever
  you'd want to do. And the non-blocking calls can
  be &lt;a href="https://leafo.net/posts/itchio-and-coroutines.html"&gt;nicely
  abstracted away using coroutines&lt;/a&gt; so that the code reads linearly
  and doesn't fall prey to the callback-soup style that is common
  among async programs.&lt;/p&gt;

&lt;p id="fn5"&gt;[&lt;a href="#rfn5"&gt;5&lt;/a&gt;] These response times are coming
  from my home server, which is a 4-core Thinkpad from 2010 on my home
  DSL. This machine also hosts my two social media servers. The search
  engine process consumes around 20 MB of resident RAM.&lt;/p&gt;

</content></entry>


    
  <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:202</id>
    <published>2024-12-31T19:28:41Z</published>
    <updated>2024-12-31T19:28:41Z</updated>

    <link href="https://technomancy.us/202" rel="alternate" type="text/html"/>
    <title>in which things once suspended are resumed</title>
    <content type="html">


&lt;p&gt;One common question I get about coding
  in &lt;a href="https://fennel-lang.org"&gt;Fennel&lt;/a&gt; is about how to
  effectively use coroutines. Fennel runs on the Lua runtime, and Lua
  is famous for having very few features, but having carefully
  selected &lt;em&gt;just&lt;/em&gt; the right features to include, and coroutines
  are one of those features that give a remarkable bang for their
  buck. But because most languages don't have them, they're often seen
  as an advanced or confusing feature, but they're not difficult, just
  different. Once you get a grasp on them,
  they provide a great deal of flexibility and in many cases mitigate
  the problem of not having OS threads on the Lua runtime.&lt;/p&gt;

&lt;h4&gt;What even is a coroutine?&lt;/h4&gt;

&lt;p&gt;In order to create a coroutine, you need a function to
  put inside it. Here's an example in Fennel&lt;sup&gt;&lt;a href="#fn1"
  name="rfn1"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;aside&gt;&lt;p&gt;This is a written version
  of &lt;a href="https://conf.fennel-lang.org/2024#video1"&gt;the talk I
    gave at FennelConf 2024&lt;/a&gt;. If you prefer video to reading, give
    that a watch; it's about 20 minutes long.&lt;/p&gt;&lt;/aside&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;c&lt;/span&gt; (&lt;span class="type"&gt;coroutine.create&lt;/span&gt; (&lt;span class="keyword"&gt;fn&lt;/span&gt; [] 5)))
c &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; #&amp;lt;thread: 0x56546f8fba88&amp;gt;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;Lua calls them "threads" which is confusing, but they're not OS
  threads. Coroutines are a form of &lt;em&gt;cooperative multitasking&lt;/em&gt;
  while OS threads are a form of &lt;em&gt;preemptive multitasking&lt;/em&gt;. That
  means that there's a scheduler which decides when to run or pause an
  OS thread. With coroutines, it's up to you to schedule and pause
  them. Despite some similarity to OS threads, you really don't use
  them in the same way, so I'd hesitate to lean on that analogy too
  hard.&lt;/p&gt;

&lt;p&gt;In order to run a coroutine, you use &lt;kbd&gt;coroutine.resume&lt;/kbd&gt;:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true 5&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;In the trivial case, it behaves like a function. However, if
  it were a function, we could call it again, but:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; false "cannot resume dead coroutine"&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;That's because each coroutine represents a single run of a function.
  So far it just sounds worse; a function you can only call once. Great.
  In order for it to be interesting, we need a function that yields:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;f&lt;/span&gt; []
  (&lt;span class="type"&gt;coroutine.yield&lt;/span&gt; 1)
  (&lt;span class="type"&gt;coroutine.yield&lt;/span&gt; 2)
  &lt;span class="builtin"&gt;:done&lt;/span&gt;)

(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;c&lt;/span&gt; (&lt;span class="type"&gt;coroutine.create&lt;/span&gt; f))

(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true 1
&lt;/span&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true 2
&lt;/span&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true "done"
&lt;/span&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; false "cannot resume dead coroutine"
&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;Yielding is like returning, but resuming starts you back where you
  were.  That's it! That's the whole thing. You now understand
  coroutines; thanks for reading!&lt;/p&gt;

&lt;p&gt;Just kidding; there is one more thing. You can pass values into the
  coroutine when resuming, and they will be returned by the call
  to &lt;kbd&gt;coroutine.yield&lt;/kbd&gt;. To show this, let's make a recursive
  function that uses that return value:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;f&lt;/span&gt; [x] (f (&lt;span class="keyword"&gt;+&lt;/span&gt; x (&lt;span class="type"&gt;coroutine.yield&lt;/span&gt; x))))
(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;c&lt;/span&gt; (&lt;span class="type"&gt;coroutine.create&lt;/span&gt; f))

(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c 1) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true    1
&lt;/span&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c 1) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true    2
&lt;/span&gt;(&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; c 6) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; true    8
&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;You'll notice the function never actually needs to return.
  It can loop forever and that's fine as long as it yields.&lt;/p&gt;

&lt;p&gt;OK, that's it now for real this time; that's the whole
  thing. Deceptively simple, but what can you do with it?&lt;/p&gt;

&lt;h4&gt;Workers working together&lt;/h4&gt;

&lt;!-- &lt;img src="/i/coroutines.jpg" class="right" --&gt;
&lt;!--      alt="Rey is faced by enemy ships labeled: continuations are confusing, --&gt;
&lt;!--      callbacks are spaghetti, no threads on my runtime. she hits them --&gt;
&lt;!--      all with a single blast labeled 'coroutines'"&gt; --&gt;

&lt;p&gt;Coroutines allow you to write non-blocking code that
  still &lt;em&gt;looks&lt;/em&gt; linear. Without coroutines, writing
  non-blocking code means leaning on callbacks. Here's a simplified
  example from a game I made
  with &lt;a href="https://lyonheart.us/"&gt;Matthew Lyon&lt;/a&gt;
  called &lt;a href="https://technomancy.itch.io/tower-institute-of-linguistics"&gt;Tower
  Institute of Linguistics&lt;/a&gt;. You
  can &lt;a href="https://p.hagelb.org/worker.fnl.html"&gt;see the whole
  example here&lt;/a&gt;, but let's start by looking at one function:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;move&lt;/span&gt; [self target]
  (&lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="keyword"&gt;=&lt;/span&gt; &lt;span class="type"&gt;self.y&lt;/span&gt; &lt;span class="type"&gt;target.y&lt;/span&gt;)
      (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.x&lt;/span&gt; (&lt;span class="keyword"&gt;+&lt;/span&gt; &lt;span class="type"&gt;self.x&lt;/span&gt; (&lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="keyword"&gt;&amp;lt;&lt;/span&gt; &lt;span class="type"&gt;self.x&lt;/span&gt; &lt;span class="type"&gt;target.x&lt;/span&gt;) 1 -1)))
      (&lt;span class="keyword"&gt;=&lt;/span&gt; &lt;span class="builtin"&gt;:stairs&lt;/span&gt; (state.map&lt;span class="builtin"&gt;:tile-type&lt;/span&gt; &lt;span class="type"&gt;self.x&lt;/span&gt; &lt;span class="type"&gt;self.y&lt;/span&gt;))
      (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.y&lt;/span&gt; (&lt;span class="keyword"&gt;+&lt;/span&gt; &lt;span class="type"&gt;self.y&lt;/span&gt; (&lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="keyword"&gt;&amp;lt;&lt;/span&gt; &lt;span class="type"&gt;self.y&lt;/span&gt; &lt;span class="type"&gt;target.y&lt;/span&gt;) 1 -1)))
      (move self (state.map&lt;span class="builtin"&gt;:find-on-level&lt;/span&gt; &lt;span class="type"&gt;self.x&lt;/span&gt; &lt;span class="type"&gt;self.y&lt;/span&gt; &lt;span class="builtin"&gt;:stairs&lt;/span&gt;)))
  (&lt;span class="type"&gt;coroutine.yield&lt;/span&gt;)
  (&lt;span class="keyword"&gt;when&lt;/span&gt; (&lt;span class="keyword"&gt;not&lt;/span&gt; (self&lt;span class="builtin"&gt;:at?&lt;/span&gt; target))
    (move self target)))&lt;/pre&gt;

&lt;p&gt;Workers move with this &lt;kbd&gt;move&lt;/kbd&gt; function which A) figures
  out which direction to go, B) takes a single step in that direction,
  C) yields, and then D) recurses if it's not done. This looks like a pretty normal recursive loop; the only difference is it's got a yield in the middle of it.&lt;/p&gt;

&lt;p&gt;So &lt;kbd&gt;move&lt;/kbd&gt; on its own could be done not too badly with
  callbacks. Our approach really shines as part of a more complex
  flow.  For context, this is from a game you give orders that get
  picked up by the workers in the game. Orders require two workers to
  work together to accomplish them. Without blocking the main loop,
  workers need to decide what to do next and coordinate on their
  work. Let's look down at the &lt;kbd&gt;start&lt;/kbd&gt; function that kicks
  off a worker's logic:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;start&lt;/span&gt; [self]
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;first check the queue of incoming orders to take
&lt;/span&gt;  (&lt;span class="keyword"&gt;case&lt;/span&gt; (&lt;span class="type"&gt;table.remove&lt;/span&gt; &lt;span class="type"&gt;state.order-queue&lt;/span&gt; 1)
    order (take-order self order))
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;next see if any other workers need help
&lt;/span&gt;  (&lt;span class="keyword"&gt;case&lt;/span&gt; (&lt;span class="type"&gt;table.remove&lt;/span&gt; &lt;span class="type"&gt;state.help-queue&lt;/span&gt; 1)
    other-worker (give-help self other-worker))
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;if you're too frustrated, leave
&lt;/span&gt;  (&lt;span class="keyword"&gt;when&lt;/span&gt; (&lt;span class="keyword"&gt;&amp;lt;&lt;/span&gt; &lt;span class="type"&gt;state.threshold&lt;/span&gt; &lt;span class="type"&gt;self.frustration&lt;/span&gt;)
    (self&lt;span class="builtin"&gt;:leave&lt;/span&gt; [&lt;span class="string"&gt;"no one understands me.\nI'm tired of this."&lt;/span&gt;]))
  (&lt;span class="type"&gt;coroutine.yield&lt;/span&gt;)
  (start self))&lt;/pre&gt;

&lt;p&gt;What does it mean to take an order?&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;take-order&lt;/span&gt; [self order]
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;add yourself to the help queue and set yourself to waiting
&lt;/span&gt;  (&lt;span class="type"&gt;table.insert&lt;/span&gt; &lt;span class="type"&gt;state.help-queue&lt;/span&gt; self)
  (&lt;span class="keyword"&gt;set&lt;/span&gt; (&lt;span class="type"&gt;self.order&lt;/span&gt; &lt;span class="type"&gt;self.waiting?&lt;/span&gt;) (&lt;span class="keyword"&gt;values&lt;/span&gt; order true))
  (move self order) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;remember, move yields every step of the way
&lt;/span&gt;  (&lt;span class="keyword"&gt;while&lt;/span&gt; &lt;span class="type"&gt;self.waiting?&lt;/span&gt;
    (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.frustration&lt;/span&gt; (&lt;span class="keyword"&gt;+&lt;/span&gt; &lt;span class="type"&gt;self.frustration&lt;/span&gt; 1))
    (&lt;span class="type"&gt;coroutine.yield&lt;/span&gt;))
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;once we have someone helping us, we can build
&lt;/span&gt;  (build-order self order false))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;build-order&lt;/span&gt; [self order helper?]
  (&lt;span class="keyword"&gt;each&lt;/span&gt; [tx ty (&lt;span class="type"&gt;order.tiles&lt;/span&gt; helper?)]
    (move self {&lt;span class="builtin"&gt;:x&lt;/span&gt; tx &lt;span class="builtin"&gt;:y&lt;/span&gt; ty})
    (self&lt;span class="builtin"&gt;:build&lt;/span&gt; (&lt;span class="keyword"&gt;.&lt;/span&gt; &lt;span class="type"&gt;order.blueprint&lt;/span&gt; ty tx))))&lt;/pre&gt;

&lt;p&gt;And how about helping?&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;give-help&lt;/span&gt; [self other-worker]
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;go to the worker you're helping (yielding the whole way)
&lt;/span&gt;  (move self other-worker)
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;unblock them
&lt;/span&gt;  (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;other-worker.waiting?&lt;/span&gt; false)
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;get to building!
&lt;/span&gt;  (build-order self &lt;span class="type"&gt;other-worker.order&lt;/span&gt; true))&lt;/pre&gt;

&lt;p&gt;There's a little hand-waving here, like what &lt;kbd&gt;self.build&lt;/kbd&gt;
  does.  But the point is the overall flow is very clear. Now try to
  imagine what this would look like using callbacks. You would have to
  turn the whole thing inside out. No recursion, no loops allowed.  It
  would not read smoothly because you'd constantly be distracted by
  trivialities.&lt;/p&gt;

&lt;h4&gt;The Importance of Being Stackful&lt;/h4&gt;

&lt;p&gt;Other languages have similar constructs: Python and JavaScript have
  "generator functions" which can also yield. However, generator
  functions can only yield directly from their body. Other normal
  functions that they call normally can't yield&lt;sup&gt;&lt;a href="#fn2"
  name="rfn2"&gt;2&lt;/a&gt;&lt;/sup&gt;. They can call other generator functions to
  let them do a nested yield.&lt;/p&gt;

&lt;p&gt;Generators are a bit of a lower-level construct from
  coroutines. However, they can serve as building blocks to implement
  "stackless coroutines". Using macro-like tricks, you can implement
  stackless coroutines by splitting functions into generators at their
  yield boundaries. The advantage of this approach is that it can be
  done at compile-time without needing any support from the underlying
  virtual machine.&lt;/p&gt;

&lt;p&gt;But going stackless has one critical limitation: you can only yield
  from a generator function. This would be fine for the worker example
  above; we wrote all code that yielded, so we could change it to use
  generators. If we wanted to yield from existing code that was not
  yield-aware, on the other hand...&lt;/p&gt;

&lt;h4&gt;You may now resume REPLing&lt;/h4&gt;

&lt;p&gt;Imagine you are creating a program with a GUI toolkit and the GUI
  library provides you with an event loop. You can't block the event
  loop, but you want to integrate a REPL so the user can run code from
  within the program. But when you call &lt;kbd&gt;fennel.repl&lt;/kbd&gt; it
  makes a blocking call to read from standard in. That's no
  good—you're writing a GUI program! You can't do a blocking read, and
  you're not using console input anyway.&lt;/p&gt;

&lt;p&gt;You can indicate for Fennel's repl to get its input from another
  source without too much trouble, since it accepts an options
  table. Let's put some canned input lines into a table and give it a
  function that pops those lines. Once the table is empty, we'll
  return &lt;kbd&gt;nil&lt;/kbd&gt; which will get treated like an EOF, ending
  the REPL session.&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;lines&lt;/span&gt; [&lt;span class="string"&gt;"(+ 54 "&lt;/span&gt; &lt;span class="string"&gt;" 50 "&lt;/span&gt; &lt;span class="string"&gt;")"&lt;/span&gt;])

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;next-line&lt;/span&gt; [] (&lt;span class="type"&gt;table.remove&lt;/span&gt; lines 1))

(&lt;span class="type"&gt;fennel.repl&lt;/span&gt; {&lt;span class="builtin"&gt;:readChunk&lt;/span&gt; next-line}) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;-&amp;gt; 104&lt;/pre&gt;

&lt;p&gt;But how would we wire this into a GUI? Here's an example that uses
  &lt;a href="https://love2d.org"&gt;LÖVE&lt;/a&gt;, a game framework. It starts
  out by defining one table each for input and output, then declaring
  a drawing function, which you can just skim because isn't that
  important for our purposes.&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;local&lt;/span&gt; [lines input] [[] []])

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;love.draw&lt;/span&gt; []
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [(w h) (&lt;span class="type"&gt;love.window.getMode&lt;/span&gt;)
        fh (&lt;span class="keyword"&gt;+&lt;/span&gt; (&lt;span class="keyword"&gt;:&lt;/span&gt; (&lt;span class="type"&gt;love.graphics.getFont&lt;/span&gt;) &lt;span class="builtin"&gt;:getHeight&lt;/span&gt;) 12)]
    (&lt;span class="keyword"&gt;each&lt;/span&gt; [i line (&lt;span class="builtin"&gt;ipairs&lt;/span&gt; lines)]
      (&lt;span class="type"&gt;love.graphics.print&lt;/span&gt; line 2 (&lt;span class="keyword"&gt;*&lt;/span&gt; i (&lt;span class="keyword"&gt;+&lt;/span&gt; fh 2))))
    (&lt;span class="type"&gt;love.graphics.line&lt;/span&gt; 0 (&lt;span class="keyword"&gt;-&lt;/span&gt; h fh 4) w (&lt;span class="keyword"&gt;-&lt;/span&gt; h fh 4)) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;draw input
&lt;/span&gt;    (&lt;span class="type"&gt;love.graphics.print&lt;/span&gt; (&lt;span class="type"&gt;table.concat&lt;/span&gt; input) 9 (&lt;span class="keyword"&gt;-&lt;/span&gt; h fh))))&lt;/pre&gt;

&lt;p&gt;Output puts things into the &lt;kbd&gt;lines&lt;/kbd&gt; table; input gets
  handled by tossing text into &lt;kbd&gt;input&lt;/kbd&gt;, plus we've got a
  little helper function to clear out &lt;kbd&gt;input&lt;/kbd&gt; once we're
  ready to use it:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;out&lt;/span&gt; [xs] (&lt;span class="keyword"&gt;icollect&lt;/span&gt; [_ x (&lt;span class="builtin"&gt;ipairs&lt;/span&gt; xs) &lt;span class="builtin"&gt;:into&lt;/span&gt; lines] x))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;love.textinput&lt;/span&gt; [text] (&lt;span class="type"&gt;table.insert&lt;/span&gt; input text))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;pop-input&lt;/span&gt; []
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [text (&lt;span class="type"&gt;table.concat&lt;/span&gt; input)]
    (&lt;span class="keyword"&gt;while&lt;/span&gt; (&lt;span class="type"&gt;table.remove&lt;/span&gt; input) nil) &lt;span class="comment-delimiter"&gt;; &lt;/span&gt;&lt;span class="comment"&gt;clear input
&lt;/span&gt;    (&lt;span class="keyword"&gt;..&lt;/span&gt; text &lt;span class="string"&gt;"\n"&lt;/span&gt;)))&lt;/pre&gt;

&lt;p&gt;Finally we wire it all together with a coroutine that runs our repl
  function; we override the
  output function to put things in the table, but instead of creating
  an input function to read from our GUI elements, we just pass
  in &lt;kbd&gt;coroutine.resume&lt;/kbd&gt; directly! Finally
  the &lt;kbd&gt;love.keypressed&lt;/kbd&gt; handler responds to the user
  pressing &lt;kbd&gt;enter&lt;/kbd&gt; by &lt;em&gt;resuming the repl coroutine&lt;/em&gt;
  with the string from the input table! The repl coroutine runs until
  it needs input, at which point it yields until the user indicates
  the input is ready by pressing enter, resuming the coroutine with
  the input.&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;options&lt;/span&gt; {&lt;span class="builtin"&gt;:onValues&lt;/span&gt; out &lt;span class="builtin"&gt;:readChunk&lt;/span&gt; &lt;span class="type"&gt;coroutine.yield&lt;/span&gt;})
(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;repl&lt;/span&gt; (&lt;span class="type"&gt;coroutine.create&lt;/span&gt; (&lt;span class="keyword"&gt;partial&lt;/span&gt; &lt;span class="type"&gt;fennel.repl&lt;/span&gt; options)))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;love.keypressed&lt;/span&gt; [key]
  (&lt;span class="keyword"&gt;case&lt;/span&gt; key
    &lt;span class="builtin"&gt;:return&lt;/span&gt; (&lt;span class="type"&gt;coroutine.resume&lt;/span&gt; repl (pop-input))
    &lt;span class="builtin"&gt;:backspace&lt;/span&gt; (&lt;span class="type"&gt;table.remove&lt;/span&gt; input)))&lt;/pre&gt;

&lt;p&gt;So what makes this different from the previous example? Well, in
  some senses it's simpler; it's just one loop instead of a whole
  series of actions. But the important thing
  is: &lt;kbd&gt;fennel.repl&lt;/kbd&gt; doesn't know it's running in a coroutine!
  All it has to do is accept whatever function we want as the "read
  input" function&lt;sup&gt;&lt;a href="#fn3" name="rfn3"&gt;3&lt;/a&gt;&lt;/sup&gt; replacing
  reading from standard in. For the rest of the repl code, it's
  completely irrelevant that it's going to yield waiting for
  input.&lt;/p&gt;

&lt;p&gt;This technique is not possible with stackless coroutines.  So it
  wouldn't work in Python or JavaScript, but it works great in Lua and
  Fennel! You can turn any code into non-blocking code regardless of
  whether it was originally written with that in mind or not. It's a
  really powerful technique&lt;sup&gt;&lt;a href="#fn4" name="rfn4"&gt;4&lt;/a&gt;&lt;/sup&gt;
  whenever you need some flow control that goes a little outside the
  usual. Give it a try some time!&lt;/p&gt;

&lt;hr&gt;
&lt;div class="footnotes"&gt;

  &lt;p&gt;[&lt;a name="fn1"&gt;1&lt;/a&gt;] I'm using Fennel for my examples
    because... well, it's my favorite programming language! If you
    prefer Lua, you can take any little snippet of Fennel code and
    put it into &lt;a href="https://fennel-lang.org/see"&gt;See Fennel&lt;/a&gt;
    to find out what the equivalent Lua looks like. All the code in
    this post is very straightforward to translate; mostly you just
    move the parens. Remember that Lua and Fennel
    have &lt;a href="https://en.wikipedia.org/wiki/Tail_call"&gt;tail-call
    optimization&lt;/a&gt;, which our code here will use heavily.&lt;/p&gt;

  &lt;p&gt;[&lt;a name="fn2"&gt;2&lt;/a&gt;] If you've heard of "function coloring";
    this is the same problem. See the
    article &lt;a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/"&gt;What
    Color is your Function?&lt;/a&gt;.&lt;/p&gt;

  &lt;p&gt;[&lt;a name="fn3"&gt;3&lt;/a&gt;] For this to work, it's important to note
    that &lt;kbd&gt;coroutine.yield&lt;/kbd&gt; is &lt;strong&gt;not&lt;/strong&gt; special
    syntax. It's a first-class function that can be passed around as a
    function argument, placed in a table, etc. That's why we can pass it
    right in with the options table; no wrapper needed.&lt;/p&gt;

  &lt;p&gt;[&lt;a name="fn4"&gt;4&lt;/a&gt;] If you've ever used the
    site &lt;a href="https://itch.io"&gt;itch.io&lt;/a&gt;, you've benefited from
    this! Because itch.io is written
    in &lt;a href="https://moonscript.org/"&gt;MoonScript&lt;/a&gt;, which compiles
    to Lua. They
    have &lt;a href="https://leafo.net/posts/itchio-and-coroutines.html"&gt;a
      great write-up&lt;/a&gt; of how their I/O uses coroutines to make
    linear-looking code run in a non-blocking way, so check that
    out.&lt;/p&gt;

&lt;/div&gt;

</content></entry>


      <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:201</id>
    <published>2024-09-13T01:48:12Z</published>
    <updated>2024-09-13T01:48:12Z</updated>

    <link href="https://technomancy.us/201" rel="alternate" type="text/html"/>
    <title>in which social media can be put in your own hands</title>
    <content type="html">


&lt;p&gt;I've
  been &lt;a href="https://icosahedron.website/@technomancy"&gt;on
  Mastodon&lt;/a&gt; since early 2017 and have really enjoyed it. It's been
  great to see the Fediverse grow as a user-owned network that can
  function without a corporate overlord calling the shots, exploiting
  the user base, and ultimately squeezing it to death for
  monetization. There are plenty of problems that remain, but one of
  the biggest ones is that new users have to find an instance to sign
  up on, and this can be tricky. Installing and administering a
  Mastodon instance is a lot of work. The liberatory function of the
  network is only as good as peoples' ability to make use of it, which
  requires running servers. If running a server is hard, fewer people
  are going to do it, and the power is going to be concentrated in the
  hands of a technical elite. A healthy network must make it easy to
  avoid this kind of centralization; to do that we have to look beyond
  Mastodon.&lt;/p&gt;

&lt;p id="rfn1"&gt;In 2019 I &lt;a href="/191"&gt;ran my own fediverse server out
    of my home&lt;/a&gt; on Pleroma for about a year; eventually shutting it
    down for a few different
    reasons&lt;sup&gt;&lt;a href="#fn1"&gt;1&lt;/a&gt;&lt;/sup&gt;. Then I started hearing
    more and more about this
    new &lt;a href="https://docs.gotosocial.org/en/latest/"&gt;GotoSocial&lt;/a&gt;
    server whose goal was to make it easy to run your own instance,
    and, well, I liked what I saw! I've
    been &lt;a href="https://hey.hagelb.org"&gt;running my own instance&lt;/a&gt;
    since August of 2022 and have made some documentation
    contributions to the project as well
    as &lt;a href="https://opencollective.com/gotosocial"&gt;funding it on
    OpenCollective&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So what's it like to set up your own GotoSocial server? Well, I can
  walk you thru what I used for my setup. Yours might be different;
  that's OK! I made these up-front choices to simplify the operational
  overhead because I didn't want this to be a hassle and I don't need
  the extra engineering that comes from trying for nine nines of uptime:&lt;/p&gt;

&lt;dl&gt;
  &lt;dt&gt;Running from my home DSL&lt;/dt&gt;
  &lt;dd&gt;I have an account on a server in a data center that this blog
    uses, but it's just for static files, email,
    and &lt;a href="/196"&gt;simple CGI scripts&lt;/a&gt;. It's not a full VPS
    because I don't want to be on the hook for all the updates and
    other sysadmin chores, but the downside is that it can't run
    programs like GotoSocial. But I have an Internet connection at
    home that I can use. The site will go down when the power or
    Internet at my home cuts out, but I don't mind. It's fine.&lt;/dd&gt;

  &lt;dt&gt;Using dynamic DNS on a subdomain&lt;/dt&gt;
  &lt;dd&gt;Similarly to the above, I've had &lt;kbd&gt;hagelb.org&lt;/kbd&gt;
    registered forever, so it's easy to add &lt;kbd&gt;hey.hagelb.org&lt;/kbd&gt;
    as a subdomain without doing additional registration. In fact this
    is a CNAME for &lt;kbd&gt;technomancy.mooo.com&lt;/kbd&gt; which is set up to
    accept dynamic DNS updates
    on &lt;a href="https://freedns.afraid.org/"&gt;freedns.afraid.org&lt;/a&gt;
    which I like a lot. It does mean that when my ISP changes my IP
    address, there is a
    window &lt;a href="https://jvns.ca/blog/2021/12/06/dns-doesn-t-propagate/"&gt;waiting
    for the old cached DNS records to expire&lt;/a&gt;, and message delivery
    will be delayed. It's fine.&lt;/dd&gt;

  &lt;dt&gt;Running on an old Thinkpad&lt;/dt&gt;
  &lt;dd&gt;Yep, I put it on an X260 that I got back in 2016. It uses about
    300MB of memory, and the CPU usage (for my single-user instance
    with ~750 followers and following ~100) has so far been
    imperceptible. Sure that machine is used for other stuff, but it's
    so efficient that you can't even tell. It's fine.&lt;/dd&gt;

  &lt;dt&gt;No containers&lt;/dt&gt;
  &lt;dd&gt;You &lt;em&gt;can&lt;/em&gt; run GotoSocial in Docker. Lots of people do
    this. However, from watching the GotoSocial help channel, I've
    observed that people who do this tend to end up running into weird
    problems that don't happen if you just ... run the program on the
    host directly, like people have been doing for thousands of
    years. I can understand the appeal of Docker if you have a very
    complex, error-prone installation process like Mastodon, but for GotoSocial
    it's just one standalone binary and zero dependencies. None of the
    problems Docker is meant to solve are present here, so there's no
    need to complicate things. I started running my server inside
    a &lt;kbd&gt;tmux&lt;/kbd&gt; session assuming that I would switch it over to
    being a proper &lt;kbd&gt;systemd&lt;/kbd&gt;-managed service once I got to
    the point where the limitations of &lt;kbd&gt;tmux&lt;/kbd&gt; started causing
    problems. That ... never actually happened. It's fine.&lt;/dd&gt;

  &lt;dt&gt;SQLite for the database&lt;/dt&gt;
  &lt;dd&gt;For some reason the default example config file in GotoSocial is
    set to use PostgreSQL. I like PostgreSQL for a lot of things. We
    use it at work and it's great. But it's absolutely overkill for
    this; none of those fancy features are going to be needed so just
    stick with SQLite which doesn't require a separate install and
    server process to be running. It's fine.&lt;/dd&gt;

  &lt;dt&gt;No proxy&lt;/dt&gt;
  &lt;dd&gt;GotoSocial can handle serving, registering, and renewing TLS
    certificates itself, so it can listen directly on port 443. If
    your machine is going to host multiple web sites, things need to
    get a little more complicated. Instead of putting your GotoSocial
    server directly on port 443, you put a different server like nginx
    or Caddy there to handle TLS termination, and GotoSocial listens
    on 8080 or something, with nginx dispatching requests to different
    downstream ports based on the contents of the &lt;kbd&gt;Host&lt;/kbd&gt;
    header. I'm not doing this, because I don't have any other sites
    here. If I wanted to add another site to this machine, it'd be a
    pain, but for now it's a lot simpler. &lt;strong&gt;Update&lt;/strong&gt;: I
    was wrong; it's actually incredibly easy to put a proxy up after
    the fact. Install &lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt; and
    put in 2 lines of configuration about which domain gets proxied to
    which port, and everything just works. It's fine.&lt;/dd&gt;

  &lt;dt&gt;No privileged ports&lt;/dt&gt;
  &lt;dd&gt;Unix was designed as a multi-user operating system. By default
    it will not allow normal users to listen on port numbers below
    1024, such as the HTTPS port 443. Nowadays this is pretty silly;
    rather than multiple users per computer we have multiple computers
    per user. I don't want my program to have to be root just to
    respond to HTTPS requests, so I ran &lt;kbd&gt;echo
    'net.ipv4.ip_unprivileged_port_start=0' &amp;gt;
    /etc/sysctl.d/50-unprivileged-ports.conf &amp;&amp; sysctl --system&lt;/kbd&gt;
    and now any user can do it. Someone will undoubtedly tell you
    that's unsafe. It's fine.&lt;/dd&gt;

  &lt;dt&gt;&lt;s&gt;Makefile&lt;/s&gt; apt for updates&lt;/dt&gt;
  &lt;dd id="rfn2"&gt;&lt;s&gt;Until about six months ago, every time a new
    GotoSocial version came out I'd back up my stuff, curl the
    tarball&lt;sup&gt;&lt;a href="#fn2"&gt;2&lt;/a&gt;&lt;/sup&gt;, extract it, shuffle some
    files around, and press &lt;kbd&gt;Ctrl-C ↑ Return&lt;/kbd&gt; to restart the
    server. Then I
    wrote &lt;a href="https://p.hagelb.org/Makefile-gts.html"&gt;makefile&lt;/a&gt;
    to handle the updates and backups for me. Longer-term I want to
    get an unofficial &lt;a href="https://apt.technomancy.us"&gt;Debian
    package&lt;/a&gt; for it that will handle service restarts for me, but
      that's fairly complicated, and I'm not there yet. It's fine.&lt;/s&gt;
    I have created &lt;a href="https://apt.technomancy.us"&gt;unofficial
      debian packaging&lt;/a&gt; to make it easier to install on
    apt-based systems.
  &lt;/dd&gt;

  &lt;dt&gt;Pinafore and Tusky for clients&lt;/dt&gt;
  &lt;dd id="rfn3"&gt;Unlike Mastodon and Pleroma, the GotoSocial team is focused on
    creating a backend and not a client. When you want to sign in,
    you'll need to use a 3rd-party client for that. On my laptop I use
    &lt;a href="https://pinafore.social"&gt;Pinafore&lt;/a&gt;, which is the best
    SPA-type web app I've ever used by a pretty large margin. It ran equally
    smoothly on my newer hardware as on my old Core 2 Duo laptop from
    2008 and has excellent keyboard controls. On mobile I
    use &lt;a href="https://tusky.app/"&gt;Tusky&lt;/a&gt;, which by a bizarre
    coincidence is &lt;em&gt;also&lt;/em&gt; the best app I've ever used on its
    platform (Android) by a significant margin. You can use GotoSocial
    with other clients as well; I've
    tried &lt;a href="https://phanpy.social/"&gt;Phanpy&lt;/a&gt; and I hear there are
    good iOS clients too. So yeah, there's no way to use it without
    third-party apps&lt;sup&gt;&lt;a href="#fn3"&gt;3&lt;/a&gt;&lt;/sup&gt;, but the apps are
    brilliant, so it's fine.&lt;/dd&gt;

  &lt;dt&gt;&lt;strong&gt;Update&lt;/strong&gt;: Imported blocklist&lt;/dt&gt;
  &lt;dd&gt;I forgot to mention this when I first published this, but the
    Fediverse &lt;em&gt;is&lt;/em&gt; an open network, and there are plenty of bad
    actors on it. You really don't want to run an instance without
    having a robust blocklist in place. Before my current instance, I
    used &lt;a href="https://icosahedron.website/about"&gt;icosahedron.website&lt;/a&gt;
    as my home, and I found that the admins there had done an
    excellent job at keeping the bad guys out. So I took their list as
    my starting point. There's no way to automate subscriptions to
    blocklists yet, but
    it's &lt;a href="https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md"&gt;on
      the roadmap&lt;/a&gt;. It's fine.&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;One huge selling point for GotoSocial over Mastodon for me is that
  posts on GotoSocial have web permalinks!  You can
  just &lt;a href="https://hey.hagelb.org/@technomancy/statuses/01J4SETAS5X1VQ0KXMMHGF37DR"&gt;link
  to a post&lt;/a&gt; and people can open it in their web browser
  or &lt;kbd&gt;curl&lt;/kbd&gt; or whatever, provided it is a public post. (The
  default post visibility is Unlisted, so if you want things to be
  visible in the browser, you might consider changing this to Public.)
  In Mastodon, when you send someone a link to a post, they can't read
  it without loading the entire Mastodon frontend app into their
  browser, which will is a monster React codebase that will break if
  they have scripting disabled and also can take up to 30 seconds on
  older hardware.&lt;/p&gt;

&lt;p&gt;There's
  a &lt;a href="https://docs.gotosocial.org/en/latest/user_guide/search/"&gt;search
  feature&lt;/a&gt; built-in with indexing, but it is limited to posts
  you've written and their replies. But it turns out that after
  running my server for a little over 2 years, the entire DB has only
  grown to 2.1GB. When it's that small, you don't need an index;
  the entire &lt;kbd&gt;statuses&lt;/kbd&gt; table can be scanned in half a
  second:&lt;/p&gt;

&lt;pre&gt;  $ sqlite3 sqlite.db "select uri, content from statuses where content like '%sqlite%'"&lt;/pre&gt;

&lt;p id="fn4"&gt;So is it all sunshine and roses? Well... yeah, kinda! When I
  first set it up I was expecting it to at least require a bit of
  regular admin work, but in fact the only thing I have to do is
  restart it for updates. Early on there were more missing
  features&lt;sup&gt;&lt;a href="#fn4"&gt;4&lt;/a&gt;&lt;/sup&gt;, but nowadays overall it's
  quite competitive and is even landing new stuff no one else has like
  a fully sandboxed, wasm-based thumbnail generation process
  and &lt;a href="https://docs.gotosocial.org/en/latest/federation/posts/#interaction-policy"&gt;interaction
  policies&lt;/a&gt; (aka Reply Guy Defense System) that allow you to
  describe in more detail who is allowed to see, reply, or boost a
  post.&lt;/p&gt;

&lt;p id="rfn5"&gt;I know running your own server is not for
  everyone&lt;sup&gt;&lt;a href="#fn5"&gt;5&lt;/a&gt; &lt;a href="#fn6"&gt;6&lt;/a&gt;&lt;/sup&gt;. We
  still have work to do to make this kind of thing even more
  accessible and to tear down the gatekeeping that prevents
  "non-nerds" from learning how to do this stuff. But we've come a
  long way, and I'm excited to see what the future brings as more
  people are able to participate in user-owned social media.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update January 2025&lt;/strong&gt;: I have a second GotoSocial
  server running on the same host. It's
  called &lt;a href="https://yyyo.online/"&gt;Yotosocial&lt;/a&gt; and
  it &lt;a href="https://git.sr.ht/~technomancy/gotosocial/commit/13da1340b98655f217a9802aa25e1952d08df779"&gt;only
    lets you post "yo"&lt;/a&gt;.&lt;/p&gt;

&lt;hr&gt;

&lt;p id="fn1"&gt;[&lt;a href="#rfn1"&gt;1&lt;/a&gt;] This was partly because I was
  running the server on a Raspberry Pi, at first on an SD card, and
  then later on an external USB hard drive. It turns out PostgreSQL
  gets really mad if you use it heavily from an SD card; disk errors
  caused frequent DB segfaults. But also the direction the project
  took caused me to rethink my involvement in it; some unpleasant
  bigots joined the team, and I wanted to distance myself from that.
&lt;/p&gt;

&lt;p id="fn2"&gt;[&lt;a href="#rfn2"&gt;2&lt;/a&gt;] Unfortunately, the release tarball
  is a tarbomb. Watch out! But the linked makefile does handle this
  problem for you.&lt;/p&gt;

&lt;p id="fn3"&gt;[&lt;a href="#rfn3"&gt;3&lt;/a&gt;] There is a GotoSocial web UI for
  settings, but you only use it to configure your account and
  instance, it's not something you open on a regular basis.&lt;/p&gt;

&lt;p id="fn4"&gt;[&lt;a href="#rfn4"&gt;4&lt;/a&gt;] When I started, it didn't have
  support for mp4 media, which is especially a pain because Mastodon
  doesn't support gifs, so when people upload gifs it converts them to
  mp4, so I was missing a lot of those. Hashtags, polls, mutes, and
  account migration have all landed in the past 18 months. One thing
  that's still missing is the edit button; for the time being you have
  to delete and redraft instead, but it's slated for the next
  release. Domain blocks are also currently all-or-nothing, so you
  can't set it to drop activity from an instance for all users that
  you don't currently follow, at least not yet.&lt;/p&gt;

&lt;p id="fn5"&gt;[&lt;a href="#rfn5"&gt;5&lt;/a&gt;] If you want to pay someone else to
  run a server for you, hosted Mastodon has been around for a while,
  but I've recently discovered
  that &lt;a href="https://www.knthost.com/gotosocial"&gt;hosted GotoSocial&lt;/a&gt;
  is now offered as well, currently for US$3.75 a month. This
  is an observation, not an endorsement, as I haven't used the
  service. &lt;a href="https://shellsharks.com/notes/2025/01/10/gotosocial-on-knt-host"&gt;This
  post&lt;/a&gt; discusses the process of setting up a hosted server.&lt;/p&gt;

&lt;p id="fn6"&gt;[&lt;a href="#rfn5"&gt;6&lt;/a&gt;] In this post I've focused on the
  technical hurdles, but the social aspect of moderating an instance
  brings a completely different set of challenges when you have other
  people on your server. Lots of people just don't; they set it up
  just for themselves, and that's fine too.&lt;/p&gt;

</content></entry>


      <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:200</id>
    <published>2023-12-31T22:07:31Z</published>
    <updated>2023-12-31T22:07:31Z</updated>

    <link href="https://technomancy.us/200" rel="alternate" type="text/html"/>
    <title>in which an escape route is discovered</title>
    <content type="html">

&lt;p&gt;I was initially something of a skeptic when I first read about the
  &lt;a href="https://langserver.org"&gt;Language Server Protocol&lt;/a&gt;. As a
  huge fan of REPL-based development, I thought it was a step back
  from the &lt;a href="https://nrepl.org"&gt;nREPL protocol&lt;/a&gt;, which had
  several years head start on the "editor-agnostic, language-agnostic
  tooling" approach&lt;sup&gt;&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;&lt;/sup&gt;. But
  after using it a while on Clojure and Fennel, I've come around; the
  features are compelling and worth using.&lt;/p&gt;

&lt;p&gt;The biggest difference between nREPL and LSP is their approach of
  what they do with the code in your project. nREPL is built around
  the assumption that the main thing you want to do is run your code;
  interact with it back and forth to see how it works. LSP is built
  around static analysis: "look but don't
  touch". For the most part they're complementary; LSP is never
  going to be able to show you something like test results or tracing,
  and because of greater overall investment of effort, LSP tends to do
  a better job of surfacing errors directly in your editor, provided
  they're compile-time errors.&lt;/p&gt;

&lt;img src="/i/yellow-tree.jpg" alt="a nice yellow tree" class="right"&gt;

&lt;p&gt;The difference between running code and just looking at it is
  pretty big, but some languages blur the distinction. Languages with
  metaprogramming tend to put up fundamental hurdles to static
  analysis, because once you allow code to be rewritten by macros, how
  can you even know what it is you should be analyzing without
  actually running the macro? And once you've run the macro, you're
  not doing static analysis any more&amp;mdash;a macro could run any
  arbitrary code, including stealing your SSH keys or wiping your
  disk.&lt;/p&gt;

&lt;p&gt;Here's an example: this code uses a macro which introduces a new
  local, but without running the macro, the Clojure LSP server can't
  actually know that, and mistakenly flags it as an error:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;deftest&lt;/span&gt; &lt;span class="function-name"&gt;test-all&lt;/span&gt;
  (&lt;span class="type"&gt;utils&lt;/span&gt;/with-system [{&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:keys&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [&lt;/span&gt;&lt;span class="flymake-error"&gt;&lt;span class="region"&gt;database&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;] &lt;/span&gt;&lt;span class="flymake-error"&gt;&lt;span class="region"&gt;&amp;amp;as&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="flymake-error"&gt;&lt;span class="region"&gt;system&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;} {&lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:start&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="constant"&gt;&lt;span class="region"&gt;true&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;}]
    (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;let&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [user (&lt;/span&gt;&lt;span class="type"&gt;&lt;span class="region"&gt;db&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;/insert-user &lt;/span&gt;&lt;span class="flymake-error"&gt;&lt;span class="region"&gt;database&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="type"&gt;&lt;span class="region"&gt;test-data&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;/generate-user))
          request {&lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:user-id&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:id&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; user) &lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:endpoint&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:get-user&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;}]
      (is (= (&lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:username&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; user)
             (&lt;/span&gt;&lt;span class="clojure-keyword"&gt;&lt;span class="region"&gt;:username&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="type"&gt;&lt;span class="region"&gt;grpc&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;/handle &lt;/span&gt;&lt;span class="flymake-error"&gt;&lt;span class="region"&gt;system&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; request)))))))&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;This is pretty annoying when you're coding. There are workarounds
  to some cases; you can spoon-feed the LSP server to just treat a
  macro as if for analysis purposes it works just like a built-in; in
  this case pretending to be &lt;code&gt;let&lt;/code&gt; would fix it. But it
  can't work gracefully out of the box because of this inherent
  incompatibility.&lt;/p&gt;

&lt;p&gt;However, &lt;em&gt;most&lt;/em&gt; macros don't actually do anything other than
  pure transformation of their arguments. It's almost always safe to
  run them during static analysis, except you don't know for
  sure&lt;sup&gt;&lt;a href="#fn2" name="rfn2"&gt;2&lt;/a&gt;&lt;/sup&gt;. That's why Fennel's
  macros can't access the outside world&lt;sup&gt;&lt;a href="#fn3"
  name="rfn3"&gt;3&lt;/a&gt;&lt;/sup&gt;. By introducing this one limitation, we
  supercharged our static analysis capabilities to where they're
  outperforming much more mature and better-funded languages like
  Clojure.&lt;/p&gt;

&lt;p&gt;Unfortunately, up until the 1.4.0 release, there was an issue with
  the macro sandbox, in particular around its interaction with the
  metadata system. Fennel allows you to attach metadata to functions
  and macros, so you can look up things like argument lists and
  docstrings in the REPL or other dynamic tooling. These get stored in
  a metadata table in the compiler.&lt;/p&gt;

&lt;p&gt;Now of course, the metadata itself is quite safe; it's just strings
  and lists of symbol names. There's nothing wrong with exposing this
  inside the compiler sandbox. You want to allow macro definitions to
  set metadata of macros. The problem is that the data is keyed
  not on the &lt;em&gt;name&lt;/em&gt; of the function&lt;sup&gt;&lt;a href="#fn4"
  name="rfn4"&gt;4&lt;/a&gt;&lt;/sup&gt; but on the function itself! So any function
  which had already been compiled could be run from the macro sandbox,
  making it easy to escape it.&lt;/p&gt;

&lt;p&gt;Anyway, once this was discovered, we quickly released Fennel 1.4.0
  with a fix that exposes a write-only proxy table for metadata to the
  sandbox. Thanks
  to &lt;a href="https://xerool.net/"&gt;XeroOl&lt;/a&gt;, author of the wonderful
  &lt;a href="https://git.sr.ht/~xerool/fennel-ls"&gt;fennel-ls&lt;/a&gt; language
  server for discovering this vulnerability and disclosing it.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;] Actually I still do think that
  as a protocol it's a big step back from nREPL, but it's not a hill
  I'm going to die on, because LSP has tons of momentum coming from
  very deep pockets, and nREPL is almost unheard of outside lisp
  communities. It's the tale as old as time of the technically
  superior alternative losing out to the clumsy behemoth. But as a
  user, you mostly don't care how the protocol is implemented, and
  the &lt;a href="https://fasterthanli.me/articles/the-bottom-emoji-breaks-rust-analyzer"&gt;nastiest
  of the design shortcomings in the protocol&lt;/a&gt; seem to have been
  addressed.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn2" href="#rfn2"&gt;2&lt;/a&gt;] I should note that while I fully
  agree with the approach of Clojure's LSP server to limit itself to
  static analysis for safety reasons, this is not a universally-held
  position. Other language servers don't make the same tradeoffs
  around safety. In particular I was surprised to learn
  that &lt;code&gt;rust-analyzer&lt;/code&gt; actually runs all macros
  and &lt;a href="https://rust-analyzer.github.io/manual.html#security"&gt;specifically
  documents that it is not safe to run on untrusted
  codebases&lt;/a&gt;. Unfortunately it's unclear whether most of its users
  are aware of this fact or not. Stay safe out there, folks!&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn3" href="#rfn3"&gt;3&lt;/a&gt;] That's a slight
  oversimplification; it's possible to disable the macro sandbox when
  it's really necessary, but this is something you wouldn't do unless
  you knew ahead of time it was safe, and it's certainly not something
  that the language server would do for you! That said, we
  have &lt;a href="https://wiki.fennel-lang.org/CompilerSandboxGranularity"&gt;plans
  to allow you to be more granular&lt;/a&gt; and grant specific macros
  limited access to filesystem capabilities.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn4" href="#rfn4"&gt;4&lt;/a&gt;] Functions in Fennel and Lua
  don't really have names in a conventional sense, other than the name
  of the local or table field they're stored in. Since there can be a
  multitude of these, none of them can be considered canonical or
  inherent. In a subtle but very real sense, every Lua function is an
  anonymous function. Stack traces in Lua don't tell you the name of
  the function as it was defined, but the name used to reference the
  function when it was &lt;em&gt;called&lt;/em&gt;.&lt;/p&gt;

</content></entry>


      <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:199</id>
    <published>2022-11-22 19:16:41</published>
    <updated>2022-11-22 19:16:41</updated>

    <link href="https://technomancy.us/199" rel="alternate" type="text/html"/>
    <title>in which legibility comes at a price</title>
    <content type="html">

&lt;p&gt;It's been a &lt;em&gt;weird&lt;/em&gt; few weeks for decentralized social
  media. The collapse of Twitter at the hands of an incompetent
  billionaire has led to a massive wave of users on the
  Fediverse&lt;sup&gt;&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;&lt;/sup&gt;, from under a million to
  over seven million now. And at the time of
  this writing Twitter hasn't even finished collapsing yet; the site
  is still hobbling along, albeit
  with &lt;a href="https://www.androidauthority.com/twitter-sms-2fa-3234698/"&gt;gaping
  holes&lt;/a&gt;. Though by the time I've finished writing this post, who
  knows. A lot can happen.&lt;/p&gt;

&lt;a href="https://datasci.social/@estebanmoro/109370591683609825"&gt;
  &lt;img src="/i/fedi-user-count.png" width="900"
       alt="a graph of new users per day overlaid with various events
            leading to Twitter's demise; each disaster causes a spike."&gt;&lt;/a&gt;

&lt;p&gt;As you might expect, the huge influx has caused some technical
  problems to the Fediverse. The mega-instances like mastodon.social
  saw slow page load times and hours of lag between something posted
  and it propagating out to other servers. More reasonably-sized
  servers have still seen some lesser propagation lag, but the main effect of
  this influx has been nontechnical. New users have had a hard time
  finding well-run servers that are accepting new members, and there
  has been a lot of complaining that the system is hard to understand
  or navigate due to its distributed nature.&lt;/p&gt;

&lt;blockquote&gt;whenever twitter makes a bunch of its users angry we get a
  new wave of activity on the [Fediverse] and all the new mastodon
  users engage in the primary hobby of a new mastodon user, which is
  listing all of the reasons why mastodon doesn't work. it's real fun
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://mastodon.social/@rootsworks/109263862932275455"&gt;- @rootsworks@mastodon.social&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some of this is simply a necessary difference between a single
  unified corporate system to a distributed network. In the 1990s we
  saw a similar type of confusion as people left silos like America
  OnLine (AOL) and CompuServ to explore the early days of the World
  Wide Web. It wasn't as polished, and there was no central view of
  the entire network. You had to do more exploring to find independent
  communities that ran their own sites. People who remember making
  this shift know now that the effort was worth it, but back then you couldn't
  fault people for wondering why things couldn't just be easy like it
  used to be.&lt;/p&gt;

&lt;p&gt;But I believe there's more to the situation with the Fediverse
  today than simply the differences that arise out of technical
  necessity.&lt;/p&gt;

&lt;p&gt;I recently finished reading the
  book &lt;a href="https://en.wikipedia.org/wiki/Seeing_Like_a_State"&gt;Seeing
  Like a State&lt;/a&gt; by James Scott. There's a lot to unpack in this
  book&lt;sup&gt;&lt;a href="#fn2" name="rfn2"&gt;2&lt;/a&gt;&lt;/sup&gt;,
  but I want to talk about the way it defines the
  term &lt;em&gt;legibility&lt;/em&gt;. Legibility describes the degree to
  which a place is understandable and navigable to an outsider.&lt;/p&gt;

&lt;img src="/i/mae-sot-street.jpg" class="right"
     alt="a street near the market in Mae Sot, Thailand"&gt;

&lt;p&gt;My favorite example of legibility is street addresses. When I
  lived in Thailand I was initially very surprised to learn that even
  long-time residents had a very rough grasp of the names of most of
  the streets in the city. People navigated by landmarks instead. As a
  newcomer to the city it was very disorienting; I had to set aside my
  ideas I brought in about how to navigate and learn to read the city
  like a local. In the end after living there two years I knew the
  names of four or five of the major streets, but when someone wanted
  to know where my house was, I didn't give them a street address; I
  told them it was a block east of the bus terminal.&lt;/p&gt;

&lt;p&gt;This attitude towards navigating looks
  different depending on whether you're an outsider or a resident. To
  a resident it's no big deal, but to someone who's new to the city
  it can be frustrating and bewildering. In some cases (for instance,
  cities that rely on tourism) there are incentives for the locals to
  increase legibility, but other factors can incentivize
  people to reduce their legibility.&lt;/p&gt;

&lt;p&gt;Legibility is particularly interesting when it lies at the crux of
  a power imbalance, and the book describes many such situations. As
  pre-modern states in Europe consolidated their power, they looked
  for ways to collect taxes and conscript soldiers more
  effectively. Customary naming made this very difficult.&lt;/p&gt;

&lt;blockquote&gt;
  Only wealthy aristocrats tended to have fixed surnames…Imagine the
  dilemma of a tithe or capitation-tax collector [in England] faced
  with a male population, 90% of whom bore just six Christian names
  (John, William, Thomas, Robert, Richard, and Henry).
&lt;/blockquote&gt;
&lt;p&gt;&lt;cite&gt;Seeing Like a State&lt;/cite&gt; by James Scott&lt;/p&gt;

&lt;p&gt;In order to consolidate their power over their citizens, states
  rolled out policies to increase the legibility of the population
  they ruled over; they started to require that everyone take a
  surname in order to uniquely identify them to the state in censuses
  and tax documents. As you can imagine, this didn't go over well; the
  historical record documents intense resistance, in some cases even
  open rebellion. The people themselves had no need for surnames; they
  got along fine with a given name supplemented when necessary with
  contextual specifiers. "John" might be "John the Baker" in some
  contexts and "Short John" in others and "John Underhill" to people
  in the next town. This system worked fine for everyone involved,
  except those wishing to exert their authority from the
  outside.&lt;sup&gt;&lt;a href="#fn3" name="rfn3"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;If this sounds familiar to you, it might be because you were paying
  attention to social media controversies of the last decade! Prominent
  advertising companies
  like &lt;a href="https://en.wikipedia.org/wiki/Facebook_real-name_policy_controversy"&gt;Facebook&lt;/a&gt;
  and &lt;a href="https://en.wikipedia.org/wiki/Nymwars"&gt;Google&lt;/a&gt;
  attempted to roll out tremendously unpopular policies requiring
  users to identify themselves using their legally-documented
  name&lt;sup&gt;&lt;a href="#fn4" name="rfn4"&gt;4&lt;/a&gt;&lt;/sup&gt;. Though these policies were always accompanied by some flimsy
  justification about "user safety", the real goal was to make their
  users more legible, primarily for advertising purposes, but also to
  law enforcement and other authorities.&lt;/p&gt;

&lt;p&gt;Well, it turns out just like villagers in premodern France, lots of
  users don't want to make themselves more legible! Even setting aside
  the impossibility of a company like Facebook accurately identifying
  the difference between a pseudonym and a legal name, there are a lot
  of great reasons to not want to be easy to find. Avoiding targeted
  advertising is just the tip of the iceberg; ask any member of a
  marginalized group and they'll tell you that being easy to find can
  have a high cost when it exposes you to targeted abuse.&lt;/p&gt;

&lt;p&gt;Twitter features full-text search of everything posted. This is
  fantastic for legibility; if you're curious about a topic, you can
  always see who's talking about it. One of the ways this gets used on
  Twitter is to pick fights. Trolls find a hot topic and barge uninvited
  into conversations to yell at people who disagree with their
  position. On the Fediverse, search is &lt;em&gt;opt-in&lt;/em&gt; instead of
  opt-out. Normal text doesn't get indexed, but hashtags do. If you
  want your post to become searchable under certain terms, a # is all
  it takes, but the randos won't show up unless you go out of your way
  to invite them.&lt;/p&gt;

&lt;p&gt;In some ways, people today coming to the Fediverse from corporate
  social media remind me of how I felt when I first moved to
  Thailand&amp;mdash;lost and bewildered by the lack of street
  signs. They've spent a lot of time on the highways of platforms that
  prioritize "engagement" at all cost, and it's taking a while for it
  to sink in that not all the Internet works that way. They've never
  considered that illegibility can be a defense mechanism..&lt;/p&gt;

&lt;p&gt;Become illegible.&lt;sup&gt;&lt;a href="#fn5" name="rfn5"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;]
  The &lt;a href="https://en.wikipedia.org/wiki/Fediverse"&gt;Fediverse&lt;/a&gt;
  is a network of interconnected social media servers that exchange
  posts. Most of its usage is based on a similar model to Twitter, but
  there are some systems on it that focus on photos or videos. Because
  the most common Fediverse server is called Mastodon, many people use
  the word Mastodon to talk about the network, similar to how in the
  1990s people said "Internet Explorer" when they meant the web.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn2" href="#rfn2"&gt;2&lt;/a&gt;] While the book is insightful, it is pretty
  long, and the two case studies in the middle drag on a bit. I
  strongly recommend reading the first three chapters and the last two
  chapters, but you can still get 90% of the insight by skipping the
  two very detailed case studies in the middle.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn3" href="#rfn3"&gt;3&lt;/a&gt;] The scary thing about this is
  how &lt;em&gt;absolute&lt;/em&gt; the normalization of surnames became over time
  despite the intense resistance. In the end, people accepted the
  greater control, and even tho surnames were rare in many
  societies only a few hundred years ago, nowadays they are thought of
  by most people as "natural".&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn4" href="#rfn4"&gt;4&lt;/a&gt;]
  These policies often misleadingly referred
  to the legal name as the "real name", as if legal process had some
  magical access to &lt;em&gt;reality itself&lt;/em&gt; which could not be
  achieved using the customary
  naming patterns people have been using for millennia.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn5" href="#rfn5"&gt;5&lt;/a&gt;]
  I have to admit to stretching this a bit for a punchy closing line, but
  of course it's not as simple as "legibility bad"; obviously there
  are many cases in which you want legibility. I'm trying to
  convey the fact that legibility does have downsides, and if you've never
  even considered this, then it's impossible for you to have a productive
  conversation about social software in today's context.&lt;/p&gt;

</content></entry>


      <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:198</id>
    <published>2022-07-25T17:44:10Z</published>
    <updated>2022-07-25T17:44:10Z</updated>

    <link href="https://technomancy.us/198" rel="alternate" type="text/html"/>
    <title>in which the word column has several meanings, none
of them architectural</title>
    <content type="html">

&lt;p&gt;Sometimes adding what appears to be the tiniest little feature to a
  program can lead you down a rabbit hole of edge cases and bugs. This
  time around it was
  an &lt;a href="https://todo.sr.ht/~technomancy/fennel/116"&gt;innocent-looking
  question about column numbers&lt;/a&gt; in the Fennel compiler error
  messages. The Fennel compiler has for a while now had a feature
  where errors show the line containing the error and pinpoint exactly
  where in the line the error occurred.&lt;/p&gt;

&lt;pre class="code"&gt;Compile error in test/bad/expected-even-bindings.fnl:1
  expected even number of name/value bindings

(let [x 1 y] x)
     ^^^^^^^
* Try finding where the identifier or value is missing.&lt;/pre&gt;

&lt;img src="/i/moss-stairs.jpg" alt="mossy stairs" class="right"&gt;

&lt;p&gt;You can see here that the &lt;tt&gt;^^^^^&lt;/tt&gt; indicator highlights the
  entire binding vector which caused the error. We didn't invent this feature in Fennel; we
  surveyed existing compilers and stole ideas from the ones that
  people really seemed to love. In
  particular, &lt;a href="https://elm-lang.org"&gt;Elm&lt;/a&gt;
  and &lt;a href="https://rust-lang.org"&gt;Rust&lt;/a&gt; kept coming up as role
  models whose error reporting was worth emulating, and both had this
  error pinpointing as one of the things that people reported
  liking.&lt;/p&gt;

&lt;p&gt;Now the bar is pretty low in general when it comes to compiler error
  messages; many compilers make only a token effort at providing
  error messages that are clear and understandable. On the bright
  side, this means you can put a small
  amount of effort into this and quickly place in a surprisingly high
  percentile of quality! Adding this error pinpointing to Fennel only
  took a couple weekends, and the payoff has been fantastic.&lt;/p&gt;

&lt;p&gt;However, it's not all smooth sailing. In order to display this
  information the Fennel compiler originally stored byte offsets in the AST and
  read the source file up to the line in question. However, using byte
  offsets for this led to some issues. Take a look at this code:&lt;/p&gt;

&lt;pre class="code"&gt;(let [无为] nil)&lt;/pre&gt;

&lt;p&gt;In this case, we want to highlight the &lt;tt&gt;[无为]&lt;/tt&gt; section of
  the code because it tries to introduce a new local without giving it
  a value. But its byte offsets are 5-13. If we use those byte offsets
  to draw the pinpoint indicator, we get this:&lt;/p&gt;

&lt;pre class="code"&gt;(let [无为] nil)
     ^^^^^^^^&lt;/pre&gt;

&lt;p&gt;Just because the identifier 无为 is 6 bytes doesn't mean it is 6
  characters wide; in fact it is only two. Now we don't have a foolproof
  solution for this problem in Fennel, but we can manage OK. Fennel's
  compiler runs on the Lua VM, and in versions of Lua from 5.3 onward,
  we have the &lt;tt&gt;utf8.len&lt;/tt&gt; function which can correctly identify
  "无为" as a string containing two characters.
  We have to fall back to the byte offsets when running on older
  versions of Lua, (unless the &lt;tt&gt;utf8&lt;/tt&gt; library has been
  backported) but when we can do better, we try our best.&lt;/p&gt;

&lt;p&gt;Unfortunately that's not the only wrinkle we have to deal
  with. What about this code?&lt;/p&gt;

&lt;pre class="code"&gt;(print "วัด" unknown-var)
             ^^^^^^^^^^^&lt;/pre&gt;

&lt;p&gt;This code doesn't even do anything "fancy" like unicode
  identifiers, but it still throws off the error pinpoint indicator.
  That's because the word "วัด" consists of three characters, ว, ั,
  and ด. The second is attached to the first, because Thai is an
  &lt;a href="https://en.wikipedia.org/wiki/Abugida"&gt;abugida&lt;/a&gt;, meaning
  the vowels are often attached above or below their consonants rather
  than being written separately. So วัด is nine bytes and three
  characters, but it only takes up two columns, throwing off our
  indicators. At this point we have run up against an inherent
  limitation in this approach. As long as we assume our output
  is displayed on a
  terminal, &lt;a href="https://reedmullanix.com/posts/unicode-source-spans.html"&gt;there
  is no way to determine the width of the code&lt;/a&gt; that we're trying to
  draw attention to!&lt;/p&gt;

&lt;p&gt;Even if we can determine the "width" of a character in columns,
  using that to produce an underline relies on the assumption that
  every column of characters is an equal width in pixels. The idea that a
  monospace font can reliably be monospace across all characters
  simply isn't true. In the best case, a larger character will be
  displayed in some width which is an integer multiple of the
  "normal" characters, but there's no guarantee that this will
  happen, and different fonts will make different decisions about how
  to render a given character. The only way to know how wide
  something will be is to attempt to render it in a specific font and
  measure it.&lt;/p&gt;

&lt;p&gt;And Fennel is not the only compiler to run into this
  bug. &lt;a href="/i/rust-wat.png"&gt;Rust&lt;/a&gt; and &lt;a href="/i/elm-wat.png"&gt;Elm&lt;/a&gt;
  both are easily confused by strings and identifiers which use
  characters outside the ASCII range, as
  are &lt;a href="/i/ocaml-wat.png"&gt;OCaml&lt;/a&gt;, Clang, and every other
  compiler we could find which tries to help identify errors this way.&lt;/p&gt;

&lt;p&gt;In my opinion the root cause of this bug is that when a white
  american developer sees a
  terminal they immediately interpret it as this idealized cartesian plane where
  they can lay out any writing they want in neatly-spaced characters
  that behave in "predictable" ways (in other words, behave exactly
  like English). But the reality is a lot more complex than that! You
  can't assume that everything people write will fit into your
  assumptions and your neat abstract boxes. Programming, of
  course, &lt;a href="http://opentranscripts.org/transcript/programming-forgetting-new-hacker-ethic/"&gt;is
  forgetting&lt;/a&gt;, but we need to at least try to be aware of the costs of the
  abstractions we choose and consider who it is that ends up being forgotten.&lt;/p&gt;

&lt;p&gt;We did find a way&lt;sup&gt;&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;&lt;/sup&gt; around the problem&amp;mdash;putting the indicator
  inline with the code itself allows you to avoid having to know how
  wide it is. So the next version of Fennel will highlight errors
  using terminal escape codes, which will even allow the functionality to
  work with right-to-left languages,
  assuming &lt;a href="https://github.com/arakiken/mlterm"&gt;your terminal
    supports them&lt;/a&gt;:&lt;/p&gt;

  &lt;img src="/i/qalb-fnl.png" alt="Fennel attempting to compile some
  Qalb code, which is written in Arabic" class="leftm"&gt;

  &lt;hr&gt;

&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;] I want to
  thank &lt;a href="https://reedmullanix.com/posts/unicode-source-spans.html"&gt;Reed
  Mullanix&lt;/a&gt; for his post about Unicode and source spans; I had come
  to the conclusion on my own about the old pinpointing approach being
  inherently impossible to do correctly from Fennel but his article
  helped me see that the inline highlighting alternative as a
  workaround for the problem which could still convey the same
  information reliably.&lt;/p&gt;

</content></entry>


      <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:197</id>
    <published>2022-02-08 08:52:53</published>
    <updated>2022-02-08 08:52:53</updated>

    <link href="https://technomancy.us/197" rel="alternate" type="text/html"/>
    <title>in which five different paths lead to methods</title>
    <content type="html">

&lt;p&gt;I recently made a change in a codebase I've been working on which
  illustrated an interesting trade-off around modeling in software. The
  project was &lt;a href="https://git.sr.ht/~technomancy/taverner"&gt;Taverner&lt;/a&gt;,
  an IRC server written in &lt;a href="https://fennel-lang.org"&gt;Fennel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In particular it had to do with the way that channels were
  modeled. A channel is basically a "chat room"; it's just something that
  users can join which lets you send messages to anyone else who's
  also in the channel. In many languages you would define a Channel
  class which has a bunch of methods like join, part, send,
  etc. Fennel doesn't have classes, but there are a few different
  alternatives available&lt;sup&gt;&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;h4&gt;Take 1: Module-based methods&lt;/h4&gt;

&lt;p&gt;The most obvious approach is to have a &lt;tt&gt;channel&lt;/tt&gt; module
  which just exports the functions that would have been methods along
  with a constructor function:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;send&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; buffer} nick ...]
  (&lt;span class="type"&gt;table.insert&lt;/span&gt; buffer [nick (&lt;span class="type"&gt;table.concat&lt;/span&gt; [...] &lt;span class="string"&gt;" "&lt;/span&gt;)]))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;join&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members &lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="keyword"&gt;&amp;amp;as&lt;/span&gt; ch} nick conn]
  (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick conn)
  (send ch &lt;span class="string"&gt;""&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) &lt;span class="builtin"&gt;:JOIN&lt;/span&gt; name))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;part&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members &lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="keyword"&gt;&amp;amp;as&lt;/span&gt; ch} nick ?cmd]
  (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick nil)
  (send ch nil (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) (&lt;span class="keyword"&gt;or&lt;/span&gt; ?cmd &lt;span class="builtin"&gt;:PART&lt;/span&gt;) name)
  (&lt;span class="keyword"&gt;when&lt;/span&gt; (empty? ch)
    (&lt;span class="type"&gt;ch.remove&lt;/span&gt;)))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;flush&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members &lt;span class="keyword"&gt;:&lt;/span&gt; buffer}]
  (&lt;span class="keyword"&gt;each&lt;/span&gt; [nick conn (&lt;span class="builtin"&gt;pairs&lt;/span&gt; members)]
    (&lt;span class="keyword"&gt;each&lt;/span&gt; [_ [sender msg] (&lt;span class="builtin"&gt;ipairs&lt;/span&gt; buffer)]
      (&lt;span class="keyword"&gt;when&lt;/span&gt; (&lt;span class="keyword"&gt;not=&lt;/span&gt; nick sender)
        (conn&lt;span class="builtin"&gt;:send&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; msg &lt;span class="string"&gt;"\r\n"&lt;/span&gt;)))))
  (&lt;span class="keyword"&gt;while&lt;/span&gt; (&lt;span class="builtin"&gt;next&lt;/span&gt; buffer)
    (&lt;span class="type"&gt;table.remove&lt;/span&gt; buffer)))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;empty?&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members}] (&lt;span class="keyword"&gt;=&lt;/span&gt; nil (&lt;span class="builtin"&gt;next&lt;/span&gt; members)))
(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;member-names&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members}] (&lt;span class="keyword"&gt;icollect&lt;/span&gt; [k (&lt;span class="builtin"&gt;pairs&lt;/span&gt; members)] k))
(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;member?&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members} nick] (&lt;span class="keyword"&gt;not=&lt;/span&gt; nil (&lt;span class="keyword"&gt;.&lt;/span&gt; members nick)))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;make-channel&lt;/span&gt; [name state]
  {&lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="builtin"&gt;:members&lt;/span&gt; {} &lt;span class="builtin"&gt;:buffer&lt;/span&gt; []
   &lt;span class="builtin"&gt;:remove&lt;/span&gt; &lt;span class="keyword"&gt;#&lt;/span&gt;(&lt;span class="keyword"&gt;tset&lt;/span&gt; &lt;span class="type"&gt;state.channels&lt;/span&gt; name nil)})

{&lt;span class="keyword"&gt;:&lt;/span&gt; send &lt;span class="keyword"&gt;:&lt;/span&gt; join &lt;span class="keyword"&gt;:&lt;/span&gt; part &lt;span class="keyword"&gt;:&lt;/span&gt; flush
 &lt;span class="keyword"&gt;:&lt;/span&gt; empty? &lt;span class="keyword"&gt;:&lt;/span&gt; member-names &lt;span class="keyword"&gt;:&lt;/span&gt; member?
 &lt;span class="keyword"&gt;:&lt;/span&gt; make-channel}&lt;/pre&gt;

&lt;p&gt;There's nothing particularly clever going on here, which I believe
  is a big strength. Everything is obvious. The &lt;tt&gt;make-channel&lt;/tt&gt;
  function acts as a constructor, while every other function in the
  module takes a channel as its first argument, so you can write code
  like &lt;tt&gt;(channel.join ch client.nick client.conn)&lt;/tt&gt;
  where &lt;tt&gt;ch&lt;/tt&gt; is a channel table you got from calling the
  constructor.&lt;/p&gt;

&lt;p&gt;The biggest downside here is that it
  lacks &lt;em&gt;encapsulation&lt;/em&gt;. All the data for a channel is exposed
  in the table that gets passed around to other modules, and it isn't
  clear which fields are safe to use and which are implementation
  details which might change later on. In a small codebase maybe this
  is no problem, but as it grows and changes over time, it will
  make it more difficult to know what effect a given change might
  have in a different part of the codebase.&lt;/p&gt;

&lt;h4&gt;Take 2: Closure-based methods&lt;/h4&gt;

&lt;p&gt;There's an old saying that "closures are a poor man's objects" and
  "objects are a poor man's closures". Keeping internal data private
  by exporting functions whose scope closes over the internal data is
  one of the oldest tricks in the book:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;make-channel&lt;/span&gt; [name server-state]
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [members {}
        buffer []]

    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;flush&lt;/span&gt; []
      (&lt;span class="keyword"&gt;each&lt;/span&gt; [nick conn (&lt;span class="builtin"&gt;pairs&lt;/span&gt; members)]
        (&lt;span class="keyword"&gt;each&lt;/span&gt; [_ [sender msg] (&lt;span class="builtin"&gt;ipairs&lt;/span&gt; buffer)]
          (&lt;span class="keyword"&gt;when&lt;/span&gt; (&lt;span class="keyword"&gt;not=&lt;/span&gt; nick sender)
            (conn&lt;span class="builtin"&gt;:send&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; msg &lt;span class="string"&gt;"\r\n"&lt;/span&gt;)))))
      (&lt;span class="keyword"&gt;while&lt;/span&gt; (&lt;span class="builtin"&gt;next&lt;/span&gt; buffer)
        (&lt;span class="type"&gt;table.remove&lt;/span&gt; buffer)))

    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;send&lt;/span&gt; [nick ...]
      (&lt;span class="type"&gt;table.insert&lt;/span&gt; buffer [nick (&lt;span class="type"&gt;table.concat&lt;/span&gt; [...] &lt;span class="string"&gt;" "&lt;/span&gt;)]))

    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;join&lt;/span&gt; [nick conn]
      (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick conn)
      (send &lt;span class="string"&gt;""&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) &lt;span class="builtin"&gt;:JOIN&lt;/span&gt; name))

    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;part&lt;/span&gt; [nick ?cmd]
      (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick nil)
      (send nick nick (&lt;span class="keyword"&gt;or&lt;/span&gt; ?cmd &lt;span class="builtin"&gt;:PART&lt;/span&gt;) name)
      (&lt;span class="keyword"&gt;when&lt;/span&gt; (&lt;span class="keyword"&gt;=&lt;/span&gt; nil (&lt;span class="builtin"&gt;next&lt;/span&gt; members)) &lt;span class="comment"&gt;; last one out off the lights
&lt;/span&gt;        (&lt;span class="keyword"&gt;tset&lt;/span&gt; &lt;span class="type"&gt;server-state.channels&lt;/span&gt; name nil)))

    {&lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="keyword"&gt;:&lt;/span&gt; send &lt;span class="keyword"&gt;:&lt;/span&gt; join &lt;span class="keyword"&gt;:&lt;/span&gt; part &lt;span class="keyword"&gt;:&lt;/span&gt; flush
     &lt;span class="builtin"&gt;:empty?&lt;/span&gt; &lt;span class="keyword"&gt;#&lt;/span&gt;(&lt;span class="keyword"&gt;=&lt;/span&gt; nil (&lt;span class="builtin"&gt;next&lt;/span&gt; members))
     &lt;span class="builtin"&gt;:member-names&lt;/span&gt; &lt;span class="keyword"&gt;#&lt;/span&gt;(&lt;span class="keyword"&gt;icollect&lt;/span&gt; [k (&lt;span class="builtin"&gt;pairs&lt;/span&gt; members)] k)
     &lt;span class="builtin"&gt;:member?&lt;/span&gt; &lt;span class="keyword"&gt;#&lt;/span&gt;(&lt;span class="keyword"&gt;not=&lt;/span&gt; nil (&lt;span class="keyword"&gt;.&lt;/span&gt; members &lt;span class="keyword"&gt;$&lt;/span&gt;))}))

{&lt;span class="keyword"&gt;:&lt;/span&gt; make-channel}&lt;/pre&gt;

&lt;p&gt;Now the module only exports one thing: &lt;tt&gt;make-channel&lt;/tt&gt;
  function, which returns a table that you can think of as if it were
  an instance of a Channel class. It has functions inside the table
  which act like methods would. This makes the interface of the
  channel very clear and well-defined. If you want to do anything
  with a channel, you have to use one of the functions in the channel
  table. You can change anything about the internals, and as long as
  you update everything in the &lt;tt&gt;make-channel&lt;/tt&gt; function, you
  know you won't break something elsewhere. In a word, it's
  encapsulated.&lt;/p&gt;

&lt;p&gt;But there is one very serious downside to this
  style&lt;sup&gt;&lt;a href="#fn2" name="rfn2"&gt;2&lt;/a&gt;&lt;/sup&gt;: reloading the code would only
  affect new channels; existing ones would keep the same old code as
  before since only the module gets the new
  functions&lt;sup&gt;&lt;a href="#fn3" name="rfn3"&gt;3&lt;/a&gt;&lt;/sup&gt;. In a normal program I
  might put up with this, even though I really love reloading. But in
  a long-running IRC server, it's really not a good idea!  Getting everyone to
  leave a channel so you can recreate a version of it which has the
  new version of the code is extremely disruptive. I absolutely need the
  ability to fix bugs and add features while the server is running
  without disrupting the users, and that meant as nice as this code
  feels, it's not going to cut it. How can we get both encapsulation and
  reloadability?&lt;/p&gt;

&lt;h4&gt;Take 3: Metatable-based methods&lt;/h4&gt;

&lt;p&gt;Lua tables have one feature which gives them an extraordinary
  amount of flexibility: metatables. There's a lot you can do with
  metatables, but for the purposes of this code the most important
  thing is that you can set an &lt;tt&gt;__index&lt;/tt&gt; method on them which
  will let you set a fallback for when you try to look up a field
  which does not exist in the table. This lets us keep doing method
  lookups using the module table directly (allowing reloads) but also
  keeping the data itself out of the table which is exposed as the
  public interface:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;send&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; buffer} nick ...]
  (&lt;span class="type"&gt;table.insert&lt;/span&gt; buffer [nick (&lt;span class="type"&gt;table.concat&lt;/span&gt; [...] &lt;span class="string"&gt;" "&lt;/span&gt;)]))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;join&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members &lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="keyword"&gt;&amp;amp;as&lt;/span&gt; ch} nick conn]
  (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick conn)
  (send ch &lt;span class="string"&gt;""&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) &lt;span class="builtin"&gt;:JOIN&lt;/span&gt; name))

&lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;... all the methods are the same as the first version
&lt;/span&gt;
(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;make-channel&lt;/span&gt; [name state]
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [public {&lt;span class="keyword"&gt;:&lt;/span&gt; name}
        channel-state {&lt;span class="builtin"&gt;:members&lt;/span&gt; {} &lt;span class="builtin"&gt;:buffer&lt;/span&gt; []
                       &lt;span class="builtin"&gt;:remove&lt;/span&gt; &lt;span class="keyword"&gt;#&lt;/span&gt;(&lt;span class="keyword"&gt;tset&lt;/span&gt; &lt;span class="type"&gt;state.channels&lt;/span&gt; name nil)}]
    (&lt;span class="builtin"&gt;setmetatable&lt;/span&gt; public {&lt;span class="builtin"&gt;:__index&lt;/span&gt; channel-state})
    (&lt;span class="builtin"&gt;setmetatable&lt;/span&gt; channel-state {&lt;span class="builtin"&gt;:__index&lt;/span&gt; (&lt;span class="builtin"&gt;require&lt;/span&gt; &lt;span class="builtin"&gt;:channel&lt;/span&gt;)})
    public))

{&lt;span class="keyword"&gt;:&lt;/span&gt; send &lt;span class="keyword"&gt;:&lt;/span&gt; join &lt;span class="keyword"&gt;:&lt;/span&gt; part &lt;span class="keyword"&gt;:&lt;/span&gt; flush
 &lt;span class="keyword"&gt;:&lt;/span&gt; empty? &lt;span class="keyword"&gt;:&lt;/span&gt; member-names &lt;span class="keyword"&gt;:&lt;/span&gt; member?
 &lt;span class="keyword"&gt;:&lt;/span&gt; make-channel}&lt;/pre&gt;

&lt;p&gt;This looks nice! It's very clear what the public fields are
  (only the channel's &lt;tt&gt;name&lt;/tt&gt;) and the private fields are attached
  using the first metatable. But if we put the method functions
  directly into the &lt;tt&gt;channel-state&lt;/tt&gt; table &lt;em&gt;during the
  constructor&lt;/em&gt; we would have the same reload problem as the previous
  version where the module containing the methods would change after
  we already pulled the functions out of it, and we wouldn't see the
  new values. Because of that, we use &lt;em&gt;the module itself&lt;/em&gt; as
  the metatable of the metatable.&lt;/p&gt;

&lt;p&gt;There's one big downside to this compared to the previous version:
  it lacks transparency:&lt;/p&gt;

&lt;pre&gt;&gt;&gt; (local channel (require :make-channel))
&gt;&gt; (local ch (make-channel "#mychannel" state []))
&gt;&gt; ch
{:name "#mychannel"} ; wait, where are the methods?
&gt;&gt; ch.join
#&amp;lt;function: 0x55c7d468f0&amp;gt; ; but it's found if you ask for it directly
&gt;&gt; (ch:join client.nick client.conn) ; and this works fine!
&lt;/pre&gt;

&lt;p&gt;The functions are found (via &lt;tt&gt;__index&lt;/tt&gt;) when you go look them up, but they do not show
  up otherwise. This is a common problem with using metatables; they
  can lead to surprising, unpredictable behavior. While there is a
  workaround to this (the &lt;tt&gt;pairs&lt;/tt&gt; metamethod) it's error-prone
  and does not work on all versions of the Lua runtime. Personally I try to avoid
  metatables unless the downsides of the alternatives are too
  great. But what other options are there?&lt;/p&gt;

&lt;h4&gt;Take 4: Class-based methods&lt;/h4&gt;

&lt;p&gt;Just because Lua and Fennel don't have classes as part of the
  language doesn't mean you can't use classes; metatables give you the
  flexibility to construct your own class system if that's what you
  really want. The &lt;a href="https://github.com/kikito/middleclass"&gt;middleclass&lt;/a&gt;
  library is one of the most popular implementations of this for Lua,
  which means of course that we can use it from Fennel too:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;class&lt;/span&gt; (&lt;span class="builtin"&gt;require&lt;/span&gt; &lt;span class="builtin"&gt;:middleclass&lt;/span&gt;))

(&lt;span class="keyword"&gt;local&lt;/span&gt; &lt;span class="variable-name"&gt;Channel&lt;/span&gt; (class &lt;span class="builtin"&gt;:Channel&lt;/span&gt;))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;Channel.send&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; buffer} nick ...]
  (&lt;span class="type"&gt;table.insert&lt;/span&gt; buffer [nick (&lt;span class="type"&gt;table.concat&lt;/span&gt; [...] &lt;span class="string"&gt;" "&lt;/span&gt;)]))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;Channel.join&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members &lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="keyword"&gt;&amp;amp;as&lt;/span&gt; ch} nick conn]
  (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick conn)
  (ch&lt;span class="builtin"&gt;:send&lt;/span&gt; &lt;span class="string"&gt;""&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) &lt;span class="builtin"&gt;:JOIN&lt;/span&gt; name))

&lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;the methods are the same as before
&lt;/span&gt;
(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;Channel.initialize&lt;/span&gt; [self name state]
  (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.name&lt;/span&gt; name)
  (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.members&lt;/span&gt; {})
  (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.buffer&lt;/span&gt; [])
  (&lt;span class="keyword"&gt;set&lt;/span&gt; &lt;span class="type"&gt;self.remove&lt;/span&gt; &lt;span class="keyword"&gt;#&lt;/span&gt;(&lt;span class="keyword"&gt;tset&lt;/span&gt; &lt;span class="type"&gt;state.channels&lt;/span&gt; name nil)))

Channel&lt;/pre&gt;

&lt;p&gt;If you're used to Java or Ruby or another class-based language,
  this may look comfortingly familiar to you. You define a class, and
  you give it methods. You invoke them using &lt;tt&gt;(ch:join client.nick
  client.conn)&lt;/tt&gt; notation. But how does it fare on the encapsulation
  and reloadability fronts?&lt;/p&gt;

&lt;pre&gt;&gt;&gt; (Channel:new "mychannel" {})
{:buffer {}
 :class {:__declaredMethods {:__tostring #&amp;lt;function: 0x55c7c28450&amp;gt;
                             :empty? #&amp;lt;function: 0x55c7b56660&amp;gt;
                             :flush #&amp;lt;function: 0x55c7bd4aa0&amp;gt;
                             :initialize #&amp;lt;function: 0x55c7b711c0&amp;gt;
                             :isInstanceOf #&amp;lt;function: 0x55c7d45000&amp;gt;
                             :join #&amp;lt;function: 0x55c7d36da0&amp;gt;
                             :member-names #&amp;lt;function: 0x55c7c06720&amp;gt;
                             :member? #&amp;lt;function: 0x55c7f06a80&amp;gt;
                             :part #&amp;lt;function: 0x55c7b710b0&amp;gt;
                             :send #&amp;lt;function: 0x55c7f03e40&amp;gt;}
         :__instanceDict @3{:__index @3{...}
                            :__tostring #&amp;lt;function: 0x55c7c28450&amp;gt;
                            :empty? #&amp;lt;function: 0x55c7b56660&amp;gt;
                            :flush #&amp;lt;function: 0x55c7bd4aa0&amp;gt;
                            :initialize #&amp;lt;function: 0x55c7b711c0&amp;gt;
                            :isInstanceOf #&amp;lt;function: 0x55c7d45000&amp;gt;
                            :join #&amp;lt;function: 0x55c7d36da0&amp;gt;
                            :member-names #&amp;lt;function: 0x55c7c06720&amp;gt;
                            :member? #&amp;lt;function: 0x55c7f06a80&amp;gt;
                            :part #&amp;lt;function: 0x55c7b710b0&amp;gt;
                            :send #&amp;lt;function: 0x55c7f03e40&amp;gt;}
         :name "Channel"
         :static {:allocate #&amp;lt;function: 0x55c7c0a930&amp;gt;
                  :include #&amp;lt;function: 0x55c7b8a5d0&amp;gt;
                  :isSubclassOf #&amp;lt;function: 0x55c7d44f70&amp;gt;
                  :new #&amp;lt;function: 0x55c7d44b40&amp;gt;
                  :subclass #&amp;lt;function: 0x55c7b8a590&amp;gt;
                  :subclassed #&amp;lt;function: 0x55c7af5600&amp;gt;}
         :subclasses {}}
 :members {}
 :name "mychannel"
 :remove #&amp;lt;function: 0x55c7d2e570&amp;gt;}
&lt;/pre&gt;

&lt;p&gt;Yikes! That's a lot of ... stuff. The methods are just dumped
  straight into a nested table inside the instance itself (twice, for
  some reason?) and the fields are not encapsulated away at all. The
  middleclass wiki
  has &lt;a href="https://github.com/kikito/middleclass/wiki/Private-stuff"&gt;some
  suggestions for how to keep data private&lt;/a&gt; but they are quite
  inconvenient compared to simply using closures. On top of that, the
  printed representation of the instance is very cluttered and
  messy. Overall it's not clear that we gain much from this approach
  beyond a sense of familiarity for people who come from certain other
  languages.&lt;/p&gt;

&lt;h4&gt;Take 5: Reloadable, encapsulated methods&lt;/h4&gt;

&lt;p&gt;So far the closure version from take 2 has appealed to me the most;
  the tight encapsulation there just feels &lt;em&gt;so tidy&lt;/em&gt;. What
  if we could go back to that but do something about the reloading? Well, there is
  actually one other concern we haven't touched on with reloading
  yet, and it leads us to our solution.&lt;/p&gt;

&lt;p&gt;When you reload, you're bringing a new version of a module into
  play in a system that's already running. When your program is a
  server, that means that you've got "in-flight" connections with
  active users of your program. What happens when you add a function
  that expects some new fields that didn't exist when your users
  initially connected? For example, let's say we add a ban list to the
  channels. This data wasn't included in the existing channels, but now
  you need to check it when you join:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;join&lt;/span&gt; [{&lt;span class="keyword"&gt;:&lt;/span&gt; members &lt;span class="keyword"&gt;:&lt;/span&gt; banned &lt;span class="keyword"&gt;&amp;amp;as&lt;/span&gt; ch} nick conn]
  (&lt;span class="builtin"&gt;assert&lt;/span&gt; (&lt;span class="keyword"&gt;not&lt;/span&gt; (&lt;span class="type"&gt;lume.find&lt;/span&gt; (&lt;span class="keyword"&gt;or&lt;/span&gt; banned []) nick)) &lt;span class="string"&gt;"Cannot join channel; banned."&lt;/span&gt;)
  (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick conn)
  (send ch &lt;span class="string"&gt;""&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) &lt;span class="builtin"&gt;:JOIN&lt;/span&gt; name))&lt;/pre&gt;

&lt;p&gt;You could code defensively and make sure that every single
  reference to the field is wrapped in an &lt;tt&gt;or&lt;/tt&gt;, but that's a
  drag. You're sure to miss one. And do you really want that check sticking
  around in your codebase forever? What we really want here
  is something like Erlang's upgrade process&lt;sup&gt;&lt;a href="#fn4" name="rfn4"&gt;4&lt;/a&gt;&lt;/sup&gt; for when it
  &lt;a href="https://learnyousomeerlang.com/designing-a-concurrent-application#hot-code-loving"&gt;hot
    loads a new module&lt;/a&gt;. Here we provide an &lt;tt&gt;upgrade&lt;/tt&gt; function
  which takes the existing table and replaces its contents with the closures from the new version:&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;make-channel&lt;/span&gt; [name server-state ?members ?buffer ?banned]
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [members (&lt;span class="keyword"&gt;or&lt;/span&gt; ?members {})
        banned (&lt;span class="keyword"&gt;or&lt;/span&gt; ?banned [])
        buffer (&lt;span class="keyword"&gt;or&lt;/span&gt; ?buffer [])]

    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;send&lt;/span&gt; [nick ...]
      (&lt;span class="type"&gt;table.insert&lt;/span&gt; buffer [nick (&lt;span class="type"&gt;table.concat&lt;/span&gt; [...] &lt;span class="string"&gt;" "&lt;/span&gt;)]))

    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;join&lt;/span&gt; [nick conn]
      (&lt;span class="builtin"&gt;assert&lt;/span&gt; (&lt;span class="keyword"&gt;not&lt;/span&gt; (&lt;span class="type"&gt;lume.find&lt;/span&gt; banned nick))
              &lt;span class="string"&gt;"Cannot join channel; banned."&lt;/span&gt;)
      (&lt;span class="keyword"&gt;tset&lt;/span&gt; members nick conn)
      (send &lt;span class="string"&gt;""&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;":"&lt;/span&gt; nick) &lt;span class="builtin"&gt;:JOIN&lt;/span&gt; name))

    &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;... the methods are all the same as the closure-based version
&lt;/span&gt;
    (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;upgrade&lt;/span&gt; [self new-make]
      (&lt;span class="keyword"&gt;each&lt;/span&gt; [k v (&lt;span class="builtin"&gt;pairs&lt;/span&gt; (new-make name server-state members buffer banned))]
        (&lt;span class="keyword"&gt;tset&lt;/span&gt; self k v)))

    {&lt;span class="keyword"&gt;:&lt;/span&gt; name &lt;span class="keyword"&gt;:&lt;/span&gt; send &lt;span class="keyword"&gt;:&lt;/span&gt; join &lt;span class="keyword"&gt;:&lt;/span&gt; part &lt;span class="keyword"&gt;:&lt;/span&gt; flush
     &lt;span class="keyword"&gt;:&lt;/span&gt; empty? &lt;span class="keyword"&gt;:&lt;/span&gt; member-names &lt;span class="keyword"&gt;:&lt;/span&gt; member?
     &lt;span class="keyword"&gt;:&lt;/span&gt; upgrade}))

{&lt;span class="keyword"&gt;:&lt;/span&gt; make-channel}&lt;/pre&gt;

&lt;p&gt;We've extended the constructor to accept all the state fields as
  optional arguments, (the ones beginning with a question mark)
  allowing you to build a new version of an existing channel by
  passing the existing state on in. The &lt;tt&gt;upgrade&lt;/tt&gt; function does
  exactly this with the private data it's closed over. We'll need to
  modify the server's reload command to call &lt;tt&gt;upgrade&lt;/tt&gt; on every one of
  the channels with the new constructor as its second
  argument. The &lt;tt&gt;upgrade&lt;/tt&gt; function calls the new constructor to
  get an updated version of the channel, then it takes all these new
  functions from it and drops them into the existing channel,
  seamlessly upgrading it in-place without dropping any
  connections. Any currently-running code which had access to the old channel
  now can see all the new methods from the new constructor. It's the
  best of both worlds, and it didn't require sacrificing
  encapsulation. Best of all only took a few lines of code to
  accomplish.&lt;/p&gt;

&lt;p&gt;But I do want to stress that each of these five approaches are all
  just trade-offs, and none of them are universally wrong. If you're not
  writing a server that keeps live connections open, it might not make
  sense to care about hot-loading upgrades. If you're writing a
  program that launches, prints its output, and immediately exits, you
  might not care about reloading, and the second approach is probably
  fine. If you've got a high tolerance for weird/unexpected behavior,
  maybe metatables are fine. If serialization is important to you, the
  first one might come out ahead. Even though the class-based approach is
  my least favorite, it could suit some projects if the
  people working on the codebase have a background in object-oriented
  languages and aren't comfortable changing their style. Context
  is &lt;em&gt;everything&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;All in all I have to say that writing an IRC server has been a lot
  of fun and not as difficult as I expected it to be. At this point my
  code is only 366 lines but it supports channels, private messages,
  channel operators, bans, kicks, listing, and more. Writing an IRC bot is
  of course easier (a simple one is under a hundred lines) but this
  could be good if you're looking for a little more of a challenge
  when picking up a new language.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;] If you don't know Fennel, you can probably
  still follow along if you understand scope and closures; the main
  things to know are that &lt;tt&gt;fn&lt;/tt&gt; declares a function, the curly
  brackets in the argument list are used to pull fields out of a table
  argument, curly brackets outside a the argument list are used to
  make tables, &lt;tt&gt;:colon-style&lt;/tt&gt; is string shorthand, and &lt;tt&gt;#(+
  2 $)&lt;/tt&gt; is shorthand for a function that adds 2 to its
  argument.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn2" href="#rfn2"&gt;2&lt;/a&gt;] Another smaller problem with this approach is
  that closures cannot be serialized, so if you had to save off a
  channel, you can't just take the channel table and write it out to
  disk. This isn't an issue in Taverner, but it could be for other
  things which could be modeled this way.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn3" href="#rfn3"&gt;3&lt;/a&gt;] This is because in the Lua runtime used by
  Fennel, modules are the unit of reloading. Reloading a module
  involves taking the module table, emptying it out, re-executing the
  module's file, and pouring the resulting fields back into the
  original table, meaning that any existing code which had access to
  the module table can see the new fields. I &lt;a href="/189"&gt;wrote
  about this in more detail&lt;/a&gt; in a previous blog post.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn4" href="#rfn4"&gt;4&lt;/a&gt;] Of course, Erlang's version is much more
  sophisticated; it allows the old and new versions of the module to
  both exist simultaneously, moving process over when it detects an
  opportune time to call the upgrade function. Since we don't have
  to worry about concurrency in Lua, it's much simpler.&lt;/p&gt;

</content></entry>


      <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:196</id>
    <published>2021-09-12 13:51:37</published>
    <updated>2021-09-12 13:51:37</updated>

    <link href="https://technomancy.us/196" rel="alternate" type="text/html"/>
    <title>in which not everything is static, but most things are</title>
    <content type="html">


&lt;p&gt;The &lt;a href="https://fennel-lang.org"&gt;Fennel programming
    language&lt;/a&gt; recently celebrated its fifth birthday, and we ran
  &lt;a href="https://fennel-lang.org/2021"&gt;a survey&lt;/a&gt; to learn more
  about the community and what has been working well and what
  hasn't. Fennel's approach has always been one of simplicity; not
  just in the conceptual footprint of the language, but in reducing
  dependencies and moving parts, and using on a runtime that fits in
  under 200kb. In order to reflect this, the Fennel web site is hosted
  as static files on the same Apache-backed shared hosting account
  I've been using for this blog since 2005.&lt;/p&gt;

&lt;p&gt;Of course, &lt;a href="https://git.sr.ht/~technomancy/fennel-lang.org/tree/main/item/main.fnl"&gt;generating
  HTML from lisp code&lt;/a&gt; is one of the oldest tricks in the
  book[&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;], so I won't bore anyone with the details there. But what
  happens when you want to mix in something that &lt;em&gt;isn't&lt;/em&gt;
  completely static, like this survey? Well, that's where it gets interesting.&lt;/p&gt;

&lt;blockquote&gt;
  I put the shotgun in an Adidas bag and padded it out with four pairs
 of tennis socks, not my style at all, but that was what I was aiming
 for: If they think you're crude, go technical; if they think you're
 technical, go crude. I'm a very technical boy. So I decided to get as
 crude as possible. These days, though, you have to be pretty
 technical before you can even aspire to crudeness.[&lt;a href="#fn2" name="rfn2"&gt;2&lt;/a&gt;]
&lt;/blockquote&gt;

&lt;p&gt;When I was in school, I learned how to write and deploy Ruby web
  programs. The easiest way to get that set up was
  using &lt;a href="https://en.wikipedia.org/wiki/Common_Gateway_Interface"&gt;CGI&lt;/a&gt;. A CGI script is just a process which is launched by the
  web server in such a way that the request comes in on stdin and
  environment variables and the response is sent over stdout. But
  larger Ruby programs tended to have very slow boot times, which
  didn't fit very well with CGI's model of launching a process afresh
  for every request that came in, and eventually other models replaced
  CGI. Most people regard CGI as somewhat outmoded and obsolete, but
  it fits Fennel's ethos nicely and complements a mostly-static-files
  approach.&lt;/p&gt;

&lt;p&gt;So the
  survey &lt;a href="https://git.sr.ht/~technomancy/fennel-lang.org/tree/3bed58d0007ac8f9616486ef20094cffc2c10562/item/survey/survey.fnl#L39"&gt;generates
    an HTML form&lt;/a&gt; in a static file which points to a CGI script as
  its &lt;tt&gt;action&lt;/tt&gt;. The CGI script looks like this, but it gets
  compiled to Lua as part of the deploy process to keep the
  server-side dependencies light.&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;let&lt;/span&gt; [contents (&lt;span class="type"&gt;io.read&lt;/span&gt; &lt;span class="string"&gt;"*all"&lt;/span&gt;)
      date (&lt;span class="type"&gt;io.popen&lt;/span&gt; &lt;span class="string"&gt;"date --rfc-3339=ns"&lt;/span&gt;)
      id (&lt;span class="keyword"&gt;:&lt;/span&gt; (date&lt;span class="builtin"&gt;:read&lt;/span&gt; &lt;span class="string"&gt;"*a"&lt;/span&gt;) &lt;span class="builtin"&gt;:sub&lt;/span&gt; 1 -2)]
  (&lt;span class="keyword"&gt;with-open&lt;/span&gt; [raw (&lt;span class="type"&gt;io.open&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;"responses/"&lt;/span&gt; id &lt;span class="string"&gt;".raw"&lt;/span&gt;) &lt;span class="builtin"&gt;:w&lt;/span&gt;)]
    (raw&lt;span class="builtin"&gt;:write&lt;/span&gt; contents))
  (&lt;span class="builtin"&gt;print&lt;/span&gt; &lt;span class="string"&gt;"status: 301 redirect"&lt;/span&gt;)
  (&lt;span class="builtin"&gt;print&lt;/span&gt; &lt;span class="string"&gt;"Location: /survey/thanks.html\n"&lt;/span&gt;))&lt;/pre&gt;

&lt;p&gt;As you can see, all this does is read the request body
  using &lt;tt&gt;io.read&lt;/tt&gt;, create a file with the current timestamp
  as the filename (we shell out to &lt;tt&gt;date&lt;/tt&gt; because the
  built-in &lt;tt&gt;os.time&lt;/tt&gt; function lacks subsecond resolution) and
  prints out a canned response redirecting the browser to another
  static HTML page. We could have printed HTML for the response body,
  but why complicate things?&lt;/p&gt;

&lt;p&gt;At this point we're all set as far as gathering data goes. But what
  do we do with these responses? Well, a typical approach would be to
  write them to a database rather than the filesystem, and to create
  another script which reads from the database whenever it gets an
  HTTP request and emits HTML which summarizes the results. You could
  certainly do this in Fennel
  using &lt;a href="https://openresty.org/en/postgres-nginx-module.html"&gt;nginx
  and its postgres&lt;/a&gt; module, but it didn't feel like a good fit for
  this. A database has a lot of moving parts and complex features
  around consistency during concurrent writes which are simply
  astronomically unlikely[&lt;a href="#fn3" name="rfn3"&gt;3&lt;/a&gt;] to happen in this
  case.&lt;/p&gt;

&lt;p&gt;At this point I think it's time to take a look at the &lt;tt&gt;Makefile&lt;/tt&gt;:&lt;/p&gt;

&lt;pre class="code"&gt;&lt;span class="makefile-targets"&gt;upload&lt;/span&gt;: index.html save.cgi thanks.html 2021.html
    &lt;span class="makefile-targets"&gt;rsync -rAv &lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="constant"&gt;&lt;span class="makefile-targets"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt; fennel-lang.org&lt;/span&gt;:fennel-lang.org/survey/

&lt;span class="makefile-targets"&gt;index.html&lt;/span&gt;: survey.fnl questions.fnl
    ../fennel/fennel --add-fennel-path &lt;span class="string"&gt;"../?.fnl"&lt;/span&gt; $&lt;span class="constant"&gt;&amp;lt;&lt;/span&gt; &amp;gt; &lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;&lt;span class="constant"&gt;@&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class="makefile-targets"&gt;save.cgi&lt;/span&gt;: save.fnl
    echo &lt;span class="string"&gt;"#!/usr/bin/env lua"&lt;/span&gt; &amp;gt; &lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;&lt;span class="constant"&gt;@&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    ../fennel/fennel --compile $&lt;span class="constant"&gt;&amp;lt;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;&lt;span class="constant"&gt;@&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    chmod 755 &lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;&lt;span class="constant"&gt;@&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class="makefile-targets"&gt;pull&lt;/span&gt;:
    &lt;span class="makefile-targets"&gt;rsync -rA fennel-lang.org&lt;/span&gt;:fennel-lang.org/survey/responses/ responses/

&lt;span class="makefile-targets"&gt;2021.html&lt;/span&gt;: summary.fnl chart.fnl questions.fnl responses/* commentary/2021/*
    ../fennel/fennel --add-fennel-path &lt;span class="string"&gt;"../?.fnl"&lt;/span&gt; $&lt;span class="constant"&gt;&amp;lt;&lt;/span&gt; &amp;gt; &lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class="makefile-targets"&gt;&lt;span class="makefile-targets"&gt;&lt;span class="constant"&gt;@&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;So the &lt;tt&gt;pull&lt;/tt&gt; target takes all the raw response files from
  the server and brings them into my local checkout of the web site on
  my laptop. The &lt;tt&gt;2021.html&lt;/tt&gt; target runs
  the &lt;tt&gt;summary.fnl&lt;/tt&gt; script locally to read thru all the
  responses, parse them, aggregate them, and emit static HTML
  containing inline SVG charts. Then the &lt;tt&gt;upload&lt;/tt&gt; task puts the
  output back on the server. Here's the code which takes that raw form
  data from the CGI script and turns it into a data structure[&lt;a href="#fn4" name="rfn4"&gt;4&lt;/a&gt;]:&lt;/p&gt;

&lt;pre class="code"&gt;&lt;span class="region"&gt;(&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;fn&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="function-name"&gt;&lt;span class="region"&gt;parse&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [contents] &lt;/span&gt;&lt;span class="comment"&gt;&lt;span class="region"&gt;; for form-encoded data
&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;  (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;fn&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="function-name"&gt;&lt;span class="region"&gt;decode&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [str] (str&lt;/span&gt;&lt;span class="builtin"&gt;&lt;span class="region"&gt;:gsub&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="string"&gt;&lt;span class="region"&gt;"%%(%x%x)"&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;fn&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [v] (&lt;/span&gt;&lt;span class="type"&gt;&lt;span class="region"&gt;string.char&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="builtin"&gt;&lt;span class="region"&gt;tonumber&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; v 16)))))
  (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;let&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [out {}]
    (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;each&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [k v (contents&lt;/span&gt;&lt;span class="builtin"&gt;&lt;span class="region"&gt;:gmatch&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="string"&gt;&lt;span class="region"&gt;"([^&amp;amp;=]+)=([^&amp;amp;=]+)"&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;)]
      (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;let&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; [key (decode (k&lt;/span&gt;&lt;span class="builtin"&gt;&lt;span class="region"&gt;:gsub&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="string"&gt;&lt;span class="region"&gt;"+"&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="string"&gt;&lt;span class="region"&gt;" "&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;))]
        (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;when&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;not&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; out key))
          (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;tset&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; out key []))
        (&lt;/span&gt;&lt;span class="type"&gt;&lt;span class="region"&gt;table.insert&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; out key) (&lt;/span&gt;&lt;span class="keyword"&gt;&lt;span class="region"&gt;pick-values&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; 1 (decode (v&lt;/span&gt;&lt;span class="builtin"&gt;&lt;span class="region"&gt;:gsub&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="string"&gt;&lt;span class="region"&gt;"+"&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt; &lt;/span&gt;&lt;span class="string"&gt;&lt;span class="region"&gt;" "&lt;/span&gt;&lt;/span&gt;&lt;span class="region"&gt;))))))
    out))&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;The final piece I want to mention is the charts in the survey
  results. I wasn't sure how I'd visualize the results, but I had some
  experience writing SVG from
  my &lt;a href="https://gitlab.com/technomancy/atreus/-/blob/master/case/case.rkt"&gt;programmatically
  generated keyboard cases&lt;/a&gt; I had constructed on my laser
  cutter. If you've never looked closely at SVG before, it's a lot
  more straightforward than you might expect. This code takes the data from
  the previous function after it's been aggregated by response count
  and emits a bar chart with counts for each response. Here's an
  example of one of the charts; inspect the source to see how it looks
  if you're curious:&lt;/p&gt;

&lt;svg width="900" aria-describedby="desc-5" role="img"
     aria-label="bar graph" height="105" class="chart"&gt;
  &lt;g class="bar"&gt;
    &lt;rect width="430" height="20" y="0"&gt;&lt;/rect&gt;
    &lt;text x="435" y="12" dy="0.35em"&gt;Linux-based (43)&lt;/text&gt;
  &lt;/g&gt;
  &lt;g class="bar"&gt;
    &lt;rect width="110" height="20" y="21"&gt;&lt;/rect&gt;
    &lt;text x="115" y="33" dy="0.35em"&gt;MacOS (11)&lt;/text&gt;
  &lt;/g&gt;
  &lt;g class="bar"&gt;
    &lt;rect width="60" height="20" y="42"&gt;&lt;/rect&gt;
    &lt;text x="65" y="54" dy="0.35em"&gt;Windows (6)&lt;/text&gt;
  &lt;/g&gt;
  &lt;g class="bar"&gt;
    &lt;rect width="40" height="20" y="63"&gt;&lt;/rect&gt;
    &lt;text x="45" y="75" dy="0.35em"&gt;Other BSD-based (4)&lt;/text&gt;
  &lt;/g&gt;
  &lt;desc id="desc-5"&gt;Linux-based: 43, MacOS: 11, Windows: 6, Other
    BSD-based: 4&lt;/desc&gt;
&lt;/svg&gt;

&lt;p&gt;I had never tried putting SVG directly into HTML before, but I
  found you can just embed an &amp;lt;svg&amp;gt; element like any other. The
  &amp;lt;desc&amp;gt; elements even allow it to be read by a screen reader.&lt;/p&gt;

&lt;pre class="code"&gt;(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;bar-rect&lt;/span&gt; [answer count i]
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [width (&lt;span class="keyword"&gt;*&lt;/span&gt; count 10)
        y (&lt;span class="keyword"&gt;*&lt;/span&gt; 21 (&lt;span class="keyword"&gt;-&lt;/span&gt; i 1))]
    [&lt;span class="builtin"&gt;:g&lt;/span&gt; {&lt;span class="builtin"&gt;:class&lt;/span&gt; &lt;span class="builtin"&gt;:bar&lt;/span&gt;}
     [&lt;span class="builtin"&gt;:rect&lt;/span&gt; {&lt;span class="keyword"&gt;:&lt;/span&gt; width &lt;span class="builtin"&gt;:height&lt;/span&gt; 20 &lt;span class="keyword"&gt;:&lt;/span&gt; y}]
     [&lt;span class="builtin"&gt;:text&lt;/span&gt; {&lt;span class="builtin"&gt;:x&lt;/span&gt; (&lt;span class="keyword"&gt;+&lt;/span&gt; 5 width) &lt;span class="builtin"&gt;:y&lt;/span&gt; (&lt;span class="keyword"&gt;+&lt;/span&gt; y 12) &lt;span class="builtin"&gt;:dy&lt;/span&gt; &lt;span class="string"&gt;"0.35em"&lt;/span&gt;}
      (&lt;span class="keyword"&gt;..&lt;/span&gt; answer &lt;span class="string"&gt;" ("&lt;/span&gt; count &lt;span class="string"&gt;")"&lt;/span&gt;)]]))

(&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;bar&lt;/span&gt; [i data ?sorter]
  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;by default, sort in descending order of count of responses, but
&lt;/span&gt;  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;allow sorting to be overridden, for example with the age question
&lt;/span&gt;  &lt;span class="comment-delimiter"&gt;;; &lt;/span&gt;&lt;span class="comment"&gt;the answers should be ordered by the age, not response count.
&lt;/span&gt;  (&lt;span class="keyword"&gt;fn&lt;/span&gt; &lt;span class="function-name"&gt;count-sorter&lt;/span&gt; [k1 k2]
    (&lt;span class="keyword"&gt;let&lt;/span&gt; [v1 (&lt;span class="keyword"&gt;.&lt;/span&gt; data k1) v2 (&lt;span class="keyword"&gt;.&lt;/span&gt; data k2)]
      (&lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="keyword"&gt;=&lt;/span&gt; v1 v2) (&lt;span class="keyword"&gt;&amp;lt;&lt;/span&gt; k1 k2) (&lt;span class="keyword"&gt;&amp;lt;&lt;/span&gt; v2 v1))))
  (&lt;span class="keyword"&gt;let&lt;/span&gt; [sorter (&lt;span class="keyword"&gt;or&lt;/span&gt; ?sorter count-sorter)
        answers (&lt;span class="keyword"&gt;doto&lt;/span&gt; (&lt;span class="keyword"&gt;icollect&lt;/span&gt; [k (&lt;span class="builtin"&gt;pairs&lt;/span&gt; data)] k) (&lt;span class="type"&gt;table.sort&lt;/span&gt; sorter))
        svg [&lt;span class="builtin"&gt;:svg&lt;/span&gt; {&lt;span class="builtin"&gt;:class&lt;/span&gt; &lt;span class="builtin"&gt;:chart&lt;/span&gt; &lt;span class="builtin"&gt;:role&lt;/span&gt; &lt;span class="builtin"&gt;:img&lt;/span&gt;
                   &lt;span class="builtin"&gt;:aria-label&lt;/span&gt; &lt;span class="string"&gt;"bar graph"&lt;/span&gt; &lt;span class="builtin"&gt;:aria-describedby&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;"desc-"&lt;/span&gt; i)
                   &lt;span class="builtin"&gt;:width&lt;/span&gt; 900 &lt;span class="builtin"&gt;:height&lt;/span&gt; (&lt;span class="keyword"&gt;*&lt;/span&gt; 21 (&lt;span class="keyword"&gt;+&lt;/span&gt; 1 (&lt;span class="keyword"&gt;length&lt;/span&gt; answers)))}]
        descs []]
    (&lt;span class="keyword"&gt;each&lt;/span&gt; [i answer (&lt;span class="builtin"&gt;ipairs&lt;/span&gt; answers)]
      (&lt;span class="type"&gt;table.insert&lt;/span&gt; svg (bar-rect answer (&lt;span class="keyword"&gt;.&lt;/span&gt; data answer) i))
      (&lt;span class="type"&gt;table.insert&lt;/span&gt; descs (&lt;span class="keyword"&gt;..&lt;/span&gt; answer &lt;span class="string"&gt;": "&lt;/span&gt; (&lt;span class="keyword"&gt;.&lt;/span&gt; data answer))))
    (&lt;span class="type"&gt;table.insert&lt;/span&gt; svg [&lt;span class="builtin"&gt;:desc&lt;/span&gt; {&lt;span class="builtin"&gt;:id&lt;/span&gt; (&lt;span class="keyword"&gt;..&lt;/span&gt; &lt;span class="string"&gt;"desc-"&lt;/span&gt; i)} (&lt;span class="type"&gt;table.concat&lt;/span&gt; descs &lt;span class="string"&gt;", "&lt;/span&gt;)])
    svg))

{&lt;span class="keyword"&gt;:&lt;/span&gt; bar}&lt;/pre&gt;

&lt;p&gt;In the end, other than the
  actual &lt;a href="https://git.sr.ht/~technomancy/fennel-lang.org/tree/main/item/survey/questions-2021.fnl"&gt;questions&lt;/a&gt;
  of the survey, all the code clocked in at just over 200 lines. If
  you're curious to read thru the whole thing you can find it
  in &lt;a href="https://git.sr.ht/~technomancy/fennel-lang.org/tree/main/item/survey/"&gt;the
  survey/ subdirectory of the fennel-lang.org repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As you can see
  from &lt;a href="https://fennel-lang.org/survey/2021"&gt;reading the
    results&lt;/a&gt;, one of the things people wanted to see more of with Fennel
  was some detailed example code. So hopefully this helps with that,
  and people can learn both about how the code is put together and the
  unusual approach to building it out.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;] In fact,
  the &lt;a href="https://git.sr.ht/~technomancy/fennel-lang.org/tree/main/item/html.fnl"&gt;HTML
    generator code&lt;/a&gt; which is used for Fennel's web site was written
  in 2018 at &lt;a href="https://conf.fennel-lang.org/2018"&gt;the first FennelConf&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn2" href="#rfn2"&gt;2&lt;/a&gt;] from &lt;i&gt;Johnny Mnemonic&lt;/i&gt; by William Gibson.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn3" href="#rfn3"&gt;3&lt;/a&gt;] If we &lt;em&gt;had&lt;/em&gt; used &lt;tt&gt;os.time&lt;/tt&gt;
  with its second-level granularity instead of &lt;tt&gt;date&lt;/tt&gt; with
  nanosecond precision then concurrent conflicting writes would have
  moved from astronomically unlikely to merely very, very unlikely,
  with the remote possibility of two responses overwriting each other
  if they arrived within the same second. We had fifty responses over
  a period of 12 days, so this never came close to happening, but in
  other contexts it could have, so choose your data storage mechanism
  to fit the problem at hand.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn4" href="#rfn4"&gt;4&lt;/a&gt;] This code is actually taken from the code I
  wrote a couple years ago to handle signups
  for &lt;a href="https://conf.fennel-lang.org/2019"&gt;FennelConf 2019&lt;/a&gt;.
  If I wrote it today I would have made it use the &lt;tt&gt;accumulate&lt;/tt&gt;
  or &lt;tt&gt;collect&lt;/tt&gt; macros.&lt;/p&gt;

</content></entry>


       <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:195</id>
    <published>2021-09-06 11:18:46</published>
    <updated>2021-09-06 11:18:46</updated>

    <link href="https://technomancy.us/195" rel="alternate" type="text/html"/>
    <title>in which a laptop defies the trends</title>
    <content type="html">

&lt;p&gt;Over the years I've been a consistent[&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;] user of
  Thinkpads; early on because I liked the keyboards, but then later
  just because I wanted hardware that would last a long time rather
  than a machine with a soldered-in battery that's designed to be more
  disposable.&lt;/p&gt;

&lt;img src="/i/x301.jpg" alt="thinkpad x301" class="right"&gt;

&lt;p&gt;My most recent device was a Thinkpad X301 built in 2008 which I
  started using in 2016. While it's no speed demon, using Firefox with
  &lt;a href="https://ublockorigin.com"&gt;uBlock Origin&lt;/a&gt; configured to
  block 3rd-party scripts by default left it feeling quite usable for
  my purposes, and the physical design of the device was perfect. They
  used a rubberized coating for the chassis in the X301 that
  I haven't seen in any other model that feels really nice on the palm
  rest. Unfortunately while nearly every component of that machine has
  withstood the test of time, the battery has not. The original
  battery's charge is down to around 90 minutes, and while it's
  swappable, working new batteries in this form factor simply cannot
  be purchased for any amount of money. I bought from two separate
  vendors claiming to have original batteries, but both of them sold
  me a battery which ballooned up and became unusable after a month or
  two.&lt;/p&gt;

&lt;p&gt;When I started to look for replacements I was dismayed. So many of
  the newer models had fallen into the Appleization
  trap&amp;mdash;everything must be made as thin and as glossy as possible
  at the expense of every other concern. I don't want a thin laptop! I
  want a laptop where I can look at it and see what's
  displayed on the screen instead of my own face staring back at me. It
  seemed it was still possible to find a model with a replaceable
  battery, but even this basic feature was becoming increasingly rare.&lt;/p&gt;

&lt;img src="/i/recursion.jpg" alt="mnt reform in a hammock"&gt;

&lt;p&gt;A couple years ago I became aware of
  the &lt;a href="https://crowdsupply.com/mnt/reform"&gt;MNT Reform
  laptop&lt;/a&gt;, and it seems like the perfect antidote to the mistakes
  the entire industry seems dead-set on repeating. It's a laptop
  that's focused on open design with schematics freely available and
  all parts easily serviceable by the end user. Finding this was like
  a breath of fresh air; it's like someone was finally listening to
  my frustrations.&lt;/p&gt;

&lt;img src="/i/appleii.jpg" align="left"
     alt="an Apple ][ computer with its case open"&gt;

&lt;p&gt;The MNT Reform has
  been &lt;a href="https://www.inputmag.com/reviews/mnt-reform-review-your-diy-laptop-fantasy-is-here-at-last"&gt;described&lt;/a&gt;
  as "the anti-macbook" which I think is fitting, but ironically I
  prefer to think of it as the Apple ][ of laptops (in a good way). If
  you're like me and you're fed up with thin laptops, you will be
  pleased to see that this machine is &lt;em&gt;chonky&lt;/em&gt;. It has to be in
  order to have room for its three most unique features: a mechanical
  keyboard, a trackball, and a standardized 18650-cell
  battery bay. Originally the batteries were what caught my attention
  after all the trouble I'd had buying replacements for my Thinkpad, but
  when I saw the mechanical keyboard I knew I had to have one. (But
  also: can we talk for a second about the &lt;em&gt;audacity&lt;/em&gt; of
  producing a laptop with a trackball? Much respect.)&lt;/p&gt;

&lt;p&gt;Part of having an open design is having everything
  documented. While you can get the schematics for everything from the
  motherboard PCB to the 3D printed trackball buttons, the part that
  nearly everyone will benefit from is the
  excellent &lt;a href="https://mntre.com/reform2/handbook/index.html"&gt;Operator
    Handbook&lt;/a&gt; which describes the usage of the system in detail.&lt;/p&gt;

&lt;img src="/i/reform-clear.jpg" class="right"
     alt="bottom view of reform with components and PCB visible" /&gt;

&lt;p&gt;Other than the thick size, perhaps the most eye-catching feature of
  the Reform is its transparent bottom plate, which is laser cut from
  acrylic. Similar to the open lid of the Apple ][, it invites you to
  take a look inside and reminds you that this machine isn't magic:
  it's wires and capacitors and screws and connectors. It's physical
  parts you can understand and control.&lt;/p&gt;

&lt;p&gt;This machine isn't perfect though; there are trade-offs. The four
  ARM Cortex A53 cores in the CPU do not perform any out-of-order or
  speculative execution, which means they are not vulnerable to
  attacks like &lt;a href=""&gt;Spectre&lt;/a&gt; and Meltdown, but at the cost of
  speed. (I'm using it mostly for chat, email, and developing
  the &lt;a href="https://fennel-lang.org"&gt;Fennel compiler&lt;/a&gt;, and it's
  plenty fast for that.) The lid closes with a satisfying magnetic
  snap, but it doesn't have a lid sensor, so you'll have to turn off
  the screen yourself. The stock wifi antenna's range is quite
  limited. (But you can easily replace it!) Suspend is
  currently &lt;a href="https://source.mnt.re/reform/reform/-/issues/8"&gt;not
  super reliable&lt;/a&gt;, but there are ongoing efforts to improve
  that.&lt;/p&gt;

&lt;img src="/i/reform-kb.jpg" alt="reform kb" class="right"&gt;

&lt;p&gt;The keyboard is ... well, it's head-and-shoulders above any other laptop
  keyboard I've tried. Instead of a comically huge space bar, the
  bottom row is broken up into a reasonably-sized space bar plus
  several other useful keys. But it's still frustrating in a few ways. (Note
  that I'm a major keyboard nerd who has spent a lot of time getting my
  keyboard setup &lt;a href="https://atreus.technomancy.us"&gt;just
  right&lt;/a&gt; and I am far more picky about this kind of thing than most
  people!) While you can reprogram the keybord firmware to
  reassign keys with ease, the physical layout is very awkward. It has
  a conventional row-stagger which is not great but also not
  unusual. The problem is that in most row-staggered boards each
  row is offset from the one above it by 1.25 key widths or so, and on
  the Reform it's 1.5. Even 1.25 is too much (zero would be ideal),
  but 1.5 makes it so you have to contort your hand even more to hit
  keys on the "ZXCV" row.&lt;/p&gt;

&lt;p&gt;Of course, it's a hackable laptop! Reprogramming the firmware to
  rearrange the keys can't fix problems with the physical arrangement,
  but I've built hundreds of keyboards by hand, so I planned to do design and
  construct one from scratch for my Reform when I got it. Unfortunately it's a little more complicated than I anticipated;
  the stock keyboard is integrated with the system controller which is
  involved with powering on the entire system and controls the OLED
  display containing the battery indicator, etc. I couldn't just adapt
  my existing design for a new form factor.&lt;/p&gt;

&lt;img src="/i/olkb.jpg" align="left"
     alt="ortholinear kb mock-up in a reform" /&gt;

&lt;p&gt;Luckily the folks at OLKB
  announced they were
  developing &lt;a href="https://www.theregister.com/2021/07/06/ortholinear_keyboard_laptop/"&gt;a
  kit for an improved keyboard&lt;/a&gt; with no row-staggering. I'd prefer
  an ergonomic design, but this is still a big improvement over the
  stock board, which is itself light years beyond anything I've ever
  used in a laptop before. I'm looking forward to building one out.&lt;/p&gt;

&lt;p&gt;Overall I'm thrilled with this laptop. It's available both as a DIY set
  which needs some assembly (just screwing things together and
  plugging connectors; no soldering) and as a prebuilt laptop, but
  honestly if you're anywhere near the target market for the Reform,
  you're probably going to enjoy the assembly process and are best off
  skipping the pre-assembled option. In the end the Reform is a
  powerful antidote to the user-hostile trends which have prevailed in
  computing over the past decade or so, and if you're anything like me
  and you don't mind a little tinkering, I can't recommend it
  enough. &lt;/p&gt;

&lt;p&gt;&lt;b&gt;Update&lt;/b&gt; (2023): While the Reform is a great machine overall,
  my use cases are extremely idiosyncratic in that I need
  to &lt;a href="/184"&gt;use EXWM as my window manager&lt;/a&gt; for
  accessibility reasons. The Reform's GPU supports Wayland well, but
  not Xorg. So I found it to be very frustrating to use. In the end I
  replaced it
  with &lt;a href="https://blog.mattgauger.com/2022/08/01/the-51nb-x210/"&gt;a
  modified X210 Thinkpad&lt;/a&gt; from 51nb that can run EXWM which I'm
  much happier with.&lt;/p&gt;

&lt;hr&gt;
&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;] Starting with &lt;a href="/74"&gt;a T60p in
    2007&lt;/a&gt; followed by an X61, then an X200s, and finally a
    X301. I &lt;a href="/160"&gt;took a brief detour&lt;/a&gt; with a Samsung
    ultrabook but the keyboard was so unpleasant that it didn't last
  long before I sold it.&lt;/p&gt;
</content></entry>


    
  <entry xml:base="https://technomancy.us">
    <author><name>Phil Hagelberg</name></author>
    <id>tag:technomancy.us,2007:194</id>
    <published>2021-03-22 08:35:03</published>
    <updated>2021-03-22 08:35:03</updated>

    <link href="https://technomancy.us/194" rel="alternate" type="text/html"/>
    <title>in which there is no such thing as a functional programming language</title>
    <content type="html">

&lt;p&gt;There is no such thing as a functional programming language.&lt;/p&gt;

&lt;p&gt;Ahem. Is this thing on? Let me try again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no such thing as a functional programming language.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All right, now that I've got your attention, let me
  explain. Functional programming does not have a single definition
  that's easy to agree upon. Some people use it to mean any kind of
  programming that centers around first-class functions passed around
  as arguments to other functions. Other people use it in a way that
  centers on the mathematical definition of a function such
  as &lt;tt&gt;ƒ(x) = x + 2&lt;/tt&gt;; that is, a pure transformation of argument
  values to return values. I believe it's more helpful to think of it
  as a "spectrum of functionalness" rather than criteria for making a
  binary "functional or not" judgment.&lt;/p&gt;

&lt;img src="/i/oregon2.jpg" class="right" alt="oregon coast" /&gt;

&lt;p&gt;So functional programming is an action; it describes something you
  do, or maybe you could say that it describes a way that you can
  program. Functional programming results in functional programs. Any
  given program exists somewhere on the spectrum between "not
  functional at all" to "purely functional". So the quality of
  "functionalness" is a property that you apply to programs.&lt;/p&gt;

&lt;p&gt;Obviously "functional programming language" is a term in widespread
  use that people do use to describe a certain kind of language. But
  what does it really mean? I would argue that a language cannot be
  functional; only a program can be more or less functional. When
  people say "functional programming language" what they mean is a
  language that encourages or allows programs to be written in a
  functional way.&lt;/p&gt;

&lt;p&gt;Except for very rare cases, the language itself does not force the
  programs written in it to be more or less functional. All the
  language can do is make it more or less difficult/awkward to write
  functional programs in. Ruby is rarely called a functional
  programming language. But it's possible (and often wise)
  to &lt;a href="https://www.destroyallsoftware.com/talks/boundaries"&gt;write
  functional programs&lt;/a&gt; in Ruby. Haskell is basically the textbook
  example of a functional programming language,
  but &lt;a href="https://hackage.haskell.org/package/base-4.12.0.0/docs/System-IO-Unsafe.html"&gt;imperative
  Haskell programs exist&lt;/a&gt;. So calling a programming language
  functional (when taken literally) is a bit of a category error. But
  "a language that encourages programming in a functional way" is an
  awkward phrase, so it gets shortened to "functional programming
  language".&lt;/p&gt;

&lt;p&gt;Incidentally the exact same argument about "functional programming
  language" can be applied to the term "fast programming
  language". There is no such thing as a language that is fast. Only
  programs can be fast[&lt;a href="#fn1" name="rfn1"&gt;1&lt;/a&gt;]. The language
  affects speed by determining the effort/speed trade-off, and by
  setting an upper bound to the speed it's possible to achieve
  while preserving the semantics of the
  language[&lt;a href="#fn2" name="rfn2"&gt;2&lt;/a&gt;]. But it does not on its own determine
  the speed.&lt;/p&gt;

&lt;p&gt;Please don't misunderstand me—I don't say this in order to be
  pedantic and shout down people who use the term "functional
  programming language". I think it's actually pretty clear what
  people mean when they use the term, and it doesn't really bother me
  when people use it. I just want to offer an alternate way of
  thinking about it; a new perspective that makes you re-evaluate some
  of your assumptions to see things in a different light.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;[&lt;a name="fn1" href="#rfn1"&gt;1&lt;/a&gt;] If you want to be even more pedantic, only
  individual executions of a program can be fast or slow. There is no
  inherent speed to the program that exists in a meaningful way
  without tying it to specific measurable runs of the program.&lt;/p&gt;

&lt;p&gt;[&lt;a name="fn2" href="#rfn2"&gt;2&lt;/a&gt;] For instance, the Scheme programming language
  has &lt;a href="http://community.schemewiki.org/?scheme-faq-standards#implementations"&gt;scores
  of different implementations&lt;/a&gt;. The same program run with the Chez
  Scheme compiler will often run several times faster than when it's
  run with TinyScheme. So saying "Scheme is fast" is a category error;
  Scheme is not fast or slow. The same is true of Lua; you will
  usually get much faster measurements when you run a Lua program with
  LuaJIT vs the reference implementation.&lt;/p&gt;

</content></entry>


</feed>
