<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>trop.in</title><id>https://trop.in/feed/blog.xml</id><subtitle>Recent Posts</subtitle><updated>2026-07-22T14:00:59Z</updated><link href="https://trop.in/feed/blog.xml" rel="self" /><link href="https://trop.in" /><entry><title>Pay for Software That Respects You</title><id>https://trop.in/blog/pay-for-software-that-respects-you.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2026-07-20T12:00:00Z</updated><link href="https://trop.in/blog/pay-for-software-that-respects-you.html" rel="alternate" /><content type="html">&lt;p&gt;I consider myself a pretty chill and calm person, I can endure and
tolerate quite a lot, but I become furious when a program spits in my
face with what I didn't ask for, take me for a dumbass or exploit
legacy vulnurabilities of my brain.  Annoying notification,
algorithmic feeds and recommendations, unasked advices and tutorials,
stolen out of my pocket data and attention.  If I observe such
behaviors from a person, I will politely ask them to fuck off at very
least. I wouldn't tolerate it from a human, and I definitely won't let
it slide from a program.&lt;/p&gt;
&lt;p&gt;The computer (laptop/phone/ereader) is mine.  Programs I run on them
are my tools.  They are here to ultimately serve me, not the
enterpreneurs, goverments, corporations or whoever what.  There is no
room for discussion here, there is no &amp;quot;an alternative good opinion&amp;quot;.
Keep your hands and noses out of my stuff.&lt;/p&gt;
&lt;p&gt;You might reasonably ask: &amp;quot;Andrew, if the program is ethical,
respectful and doesn't fuck around with you, how people behind it will
get a profit?&amp;quot;. That's a good question, and I have some thoughts on
it.&lt;/p&gt;
&lt;p&gt;First of all, there is a hidden assumption that all things should
always grow and bring profits.  It's not true, a lot of cool things
will become much better if they stay at the certain size and just
become sustainable (bring enough money, joy and other resources) for
their authors and maintainers and keep serving their users. Moreover,
in many cases, things built around profit or endless growth simply
exploit the planet's resources and vulnerable people under the hood
and never become actually sustainable.&lt;/p&gt;
&lt;p&gt;Nevertheless, I don't deny that getting money for creating and
maintaining software is important.  One can of course go quite far on
a pure enthusiasm and joy from a positive feedback, but it won't bring
food to the table.  And even when you make software &lt;a href=&quot;https://trop.in/blog/i-have-to-live-in-a-forest-to-work-on-open-source&quot;&gt;living in the
forest&lt;/a&gt;
you still need some funds.&lt;/p&gt;
&lt;p&gt;Respectful software is almost always also a Free and Open Source
Software.  The free as in a freedom of course, but also free as in a
free beer. The irony here is that monetizing free programs in ethical
way is exceptionally hard. Two primary options are grants and
donations.  There a few more, but they work only for a particular
types of software.  Donations can be a quite sustainable source of
income for the project.  So, don't understimate the impact of a small
(preferably recurring) donations.  It makes a night-and-day difference
for developers.&lt;/p&gt;
&lt;p&gt;The bottom line is: we have to normalize paying for a respectful
software that we obtain for free. Not because we have to, but because
it respects us and we want express the respect and appreciation to its
authors and maintainers.&lt;/p&gt;
&lt;h2&gt;My list&lt;/h2&gt;
&lt;p&gt;There are a few projects that I rely on daily, they are respectful AF
to me, but I hadn't had a chance to express my gratitude with a cold
hard cash to some of their creators yet, so today I fixed it.  I don't
recommend you to follow my list, I just want to give you a bit of
inspiration and food for thought.&lt;/p&gt;
&lt;h3&gt;Operating systems&lt;/h3&gt;
&lt;p&gt;The fundamental part of any non-trivial electronic devices, the place
where computing starts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://guix.gnu.org/&quot;&gt;GNU Guix&lt;/a&gt;
(&lt;a href=&quot;https://guix.gnu.org/en/donate/&quot;&gt;donate&lt;/a&gt;) :: my primary and only
OS on the laptop is based on guix functional package manager.  I
spend most of the day with it, I do most of the work with it.  No
question it's a crucial part of my life.  No question the package
manager is one of the most important pieces of any computing, the
foundation for reliable and trustworthy supply chain.  The primary
repo contains only FOSS, it's not a guarantee on its own, but
together with the work of the community it gives a reasonable amount
of confidence and piece of mind. And I didn't even started to talk
about all the goodies like declarative OS configuration,
transactional updates/rollbacks, lisp all over the places and much
more.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://grapheneos.org/&quot;&gt;GrapheneOS&lt;/a&gt;
(&lt;a href=&quot;https://grapheneos.org/donate&quot;&gt;donate&lt;/a&gt;) :: it's my phone's OS
based on AOSP.  Hardened, minimalistic, zero crapware OOB, good
sandboxing, granular capabilities/scopes/permissions to restrict
untrusted applications.  I don't like phones, I don't like to use
them, I hate most of the mobile apps, but graphene makes the
experience at least manageable and provides some security and
privacy to the crappy mobile phone world.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://f-droid.org&quot;&gt;F-droid&lt;/a&gt;
(&lt;a href=&quot;https://f-droid.org/en/donate&quot;&gt;donate&lt;/a&gt;,
&lt;a href=&quot;https://keepandroidopen.org/&quot;&gt;sign&lt;/a&gt;) :: my go to Android App
Repository. Free and Open Source Mobile Software Distribution
Ecosystem.  We already talked about supply chain for desktop/server
OS, now let's cover a mobile devices counterpart. F-droid is an app
itself, the app for obtaining and distributing other apps.  It's
also an app repository.  It's a set of tools for creating own
packages and repositories.  It's user's freedom and privacy first
project.  It's hard to underestimate how important program
distribution is, right? So, this was an obvious pick.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://crossink.uxj.io/&quot;&gt;CrossInk&lt;/a&gt;
(&lt;a href=&quot;https://ko-fi.com/uxjulia&quot;&gt;sponsor&lt;/a&gt;) :: a fork of CrossPoint, a
simple and efficient OS for esp32-based E-ink readers.  I have a
luxury and luck to spend more time on my ultralight E-reader rather
than on my phone, and I enjoy every minute using it thanks to
CrossPoint.  KOreader sync, calibre integration, WebUI for uploading
books over the air.  CrossInk adds some missing features like
control remapping, good OOB fonts, reading stats and IMHO improves
the UX significantly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://koreader.rocks/&quot;&gt;KOreader&lt;/a&gt;
(&lt;a href=&quot;https://liberapay.com/KOReader&quot;&gt;support&lt;/a&gt;) :: it's not exactly OS,
it's more of a reading application, but it works as a graphical
shell, file browser, bookmark manager, dictionary and much more.
Basically it's only user facing program on my ereader, almost pid 1.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Content and Social Networks&lt;/h3&gt;
&lt;p&gt;The programs are usually fun and useful, but some (or many of them)
can't go far without a content and a way to exchange it (being it
audio, video, text or whatever).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://annas-archive.gl&quot;&gt;Anna's Archive&lt;/a&gt;
(&lt;a href=&quot;https://annas-archive.gl/donate&quot;&gt;donate&lt;/a&gt;) :: The Biggest Library
in the world, the project for preserving human knowledge and culture
and making it accessible to everyone.  Everytime I need a book or
scientific paper, I reach for it.  The code and datasets are open
source.  Books can be fun and entertaining, but books also a
foundation of our civilization.  If book brought you a joy or was
helpful, donate to author of the book and write them a letter of
appreciation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://librewolf.net/&quot;&gt;LibreWolf&lt;/a&gt; (no donations) :: the browser
based on ff, a window to the web.  Full-fledged, No BS, freedom and
privacy-respecting.  Rare, but luckily existing.  The project
doesn't accept donations, but you can contribute your time to it or
just share a word about the project.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.migadu.com&quot;&gt;Migadu&lt;/a&gt;
(&lt;a href=&quot;https://www.migadu.com/pricing/&quot;&gt;pay&lt;/a&gt;) :: my primary email
provider and email is my primary communication tool.  While most of
the infrastructure is based on FOSS, it's not a FOSS project on its
own.  This is one of the rare examples of commercial projects making
a respectful software.  Also, a good example of the project that
don't try to grow indefinitely, and operates on its own comfortable
scale.  &amp;quot;Not so much for profit company&amp;quot;.  The web interface is
clean and on point, the protocols are standard and complaint.  No
ads, no vendorlock, no attention robbery.  People in support are
professional, they are technical specialists, who can actually
answer the questions or help to troubleshoot a problem.  Commercial
software, web sites and services can be respectful too, this is a
clear example.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://fosstodon.org&quot;&gt;Fosstodon&lt;/a&gt;
(&lt;a href=&quot;https://hub.fosstodon.org/support&quot;&gt;support&lt;/a&gt;) :: a short message
oriented fediverse (mastodon) instance.  Mastodon is similiar to
twitter/bluesky, but federated. Interoperable with other fedi
services, including peertube, lemmy, pixelfed and &lt;a href=&quot;https://fediverse.party/&quot;&gt;much
more&lt;/a&gt;.  There are a few options: to pay to
&lt;a href=&quot;https://joinmastodon.org/sponsors&quot;&gt;mastodon devs&lt;/a&gt; and to people who
maintain, operate and moderate a particular instance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sourcehut.org/&quot;&gt;SourceHut&lt;/a&gt;
(&lt;a href=&quot;https://sourcehut.org/pricing/&quot;&gt;pay&lt;/a&gt;) :: No AI, No JS, No BS, No
ADS.  My primary git hosting at the moment.  Not the easiest, not
the prettiest, but very capable and do the shit you need from a git
hosting.  One can contribute and use most of the functionality
without even creating an account.  It has a kernel-org vibe.  It's
another example of a managed FOSS service, mid-sized and
sustainable.  If you look for more github-like alternative with Pull
Requests and stuff, take a look at
&lt;a href=&quot;https://codeberg.org/&quot;&gt;codeberg.org&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://voice.woitaschek.de/&quot;&gt;Voice&lt;/a&gt;
(&lt;a href=&quot;https://voice.woitaschek.de/&quot;&gt;support&lt;/a&gt;) :: a simple audiobook
player.  It doesn't have to be a huge, mission critical application,
we can pay for a thing, when it just makes our life a bit easier and
nicer.  This one will be that one.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I could go further for hours. Video editing with ffmpeg, actually
useful feeds with fresshrss, books library management with calibre,
etc, etc.  It's not the point to list all of the cool stuff here, the
point is to share the idea, to give the inspiration, to normalize
paying for the good craft and respectful things.  Just try it.  It
will make the day for the author of the program.  It will make the day
for you.&lt;/p&gt;
</content></entry><entry><title>Context Makes Tests Reusable</title><id>https://trop.in/blog/context-makes-tests-reusable.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2026-06-17T12:00:00Z</updated><link href="https://trop.in/blog/context-makes-tests-reusable.html" rel="alternate" /><content type="html">&lt;h1&gt;Context Makes Tests Reusable&lt;/h1&gt;
&lt;p&gt;I've been designing and implementing a testing framework for around a
year.  The API was simple and stable for a long time, but a couple of
things were bothering me: setting up and tearing down testing
environments is cumbersome, reusing tests is inconvinient.&lt;/p&gt;
&lt;p&gt;I found out that a minor adjustment to API can make a huge difference
and significantly improve UX, so I share my observations and findings
with you.&lt;/p&gt;
&lt;p&gt;This is a testing library design note and a list of cool consequences
of one design decision that made a whole class of tasks easier.&lt;/p&gt;
&lt;p&gt;The first two sections provide necessary context for understanding of
the syntax and semantics of code examples.  The third section
introduces &amp;quot;The Problem&amp;quot;. The next sections immediately uncover
deceptively simple design decision, and gradually show how it
addresses the issues mentioned in the previous section and radically
improves the overall situations.&lt;/p&gt;
&lt;h2&gt;Original Design and Syntax&lt;/h2&gt;
&lt;p&gt;After dozens of iterations, weeks of research and multiple workarounds
of imlicit and explicit constraits I ended up with a quite clean
syntax and three primary entities: assertion, test and suite.  This is
how test definition syntax looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(suite &amp;quot;outer&amp;quot;
 (suite &amp;quot;nested&amp;quot;
  (test &amp;quot;the test&amp;quot; ; test can be re-executed multiple times
   (define val (prepare-value somehow))
   (is (good? val)) ; asserting
   (is (really-good? val))) ; asserting again
  (test &amp;quot;small&amp;quot;
   (is (= 4 (+ 2 2))))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The way it works is (maybe) not intuitive, but simple: the &lt;code&gt;test&lt;/code&gt;
macro captures its body, creates a thunk (zero-arguments procedure)
out of it and sends this procedure with some extra data to
test-runner.  This process is called test loading: test runner
registers a test (test/body-procedure + test/description +
test/metadata) and later can execute and re-execute
test/body-procedure whenever it is needed.&lt;/p&gt;
&lt;p&gt;The test being a first-class runtime entity is a must-have property
for interactive development workflows (REPL-driven or similiar).
Moreover, having a test + information from previous runs, we can
schedule next executions in a very effective way and integrate the run
with other development tooling, e.g. we can schedule the quickest
previously failed tests to be executed first, and in case of failure,
halt/pause the further execution and immediately bring up a debugger.&lt;/p&gt;
&lt;p&gt;A test can exist on its own, but for big projects it's better to keep
things organized. The &lt;strong&gt;suite&lt;/strong&gt; entity helps with it, it serves two
purposes: grouping related entities and building hierarchies.  After
&lt;code&gt;outer&lt;/code&gt; suite is loaded the test runner's will be aware of the test
hierarchy:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;📂 outer
└─ 📂 nested
   ├─ 📄 the test
   └─ 📄 small
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is cool on its own, but becomes even cooler, when we add metadata
feature into the equation. The concept of metadata is simple, you can
add arbitrary data to the test or suite. Like this:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;(test &amp;quot;my test&amp;quot;
 'metadata '((tags . (slow integration)))
 (is (good? val)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way we can provide more info for the test runner, which can be
later used to simplify our lives, e.g. we can mark some tests with
&lt;code&gt;slow&lt;/code&gt; tag and ask test runner to temporary skip them during
[re-]executions.  It will keep our feedback loop tighter, but still
allow to easily run all the tests once in a while.  Of course, it's
only one of use cases, I bet you already have a bunch of cool ideas of
what we can do with access to test-runner internals and this
mechanism, but let's explore metadata for suites first.&lt;/p&gt;
&lt;p&gt;Syntax-wise it looks exactly the same, but internally it works a bit
different.  Test runner loads the whole hierarchy and for each test
computes a &lt;code&gt;test/compound-metadata&lt;/code&gt; value by merging metadata of all
enclosing suites and test itself.  So tests basically &amp;quot;inherit&amp;quot; the
metadata of their ancestors (suites).  This way we can mark the whole
suite of tests with a particular tag or provide a fixture for setting
up a db connection for all db-related tests at once.&lt;/p&gt;
&lt;h2&gt;Dynamically Scoped Variables&lt;/h2&gt;
&lt;p&gt;A small almost offtopic, but important section, explaining parameters
(dynamically scoped variables) by a short example.  We will need
understanding of it in the next section.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define s-s-v 6)

(define (fn)
  (display s-s-v))

(let ((s-s-v 7))
  ;; let doesn't affect staticallyh/lexically scoped s-s-v captured by fn
  (fn) ; prints 6
  (display s-s-v) ; prints 7
  )

(define d-s-v (make-parameter 6))

(define (fn2)
  (display (d-s-v)))

(parameterize ((d-s-v 7))
  ;; parameterize directly affects the value of dynamically scoped d-s-v
  (fn2) ; prints 7
  (display (d-s-v)) ; prints 7
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AFAIK, In modern PLT, lexical scoping is preferred over dynamic for
general-purpose languages.  Code with lexically scoped variables is
easier to reason about and maintain.&lt;/p&gt;
&lt;p&gt;Sometimes dynamically scoped variables can be useful, e.g. when you
want to add a new parameter to a function, but don't want to propagate
extra argument to all the callers. In this case dynamically scoped
variables can be a compromise: they are still better than global
mutable variables, it less refactoring work then updating all the
callers signatures and adding extra argument to them.&lt;/p&gt;
&lt;p&gt;Still, they introduce unecessary coupling and make flow control much
more cumbersome and opaque, so it's better to avoid them when
possible.&lt;/p&gt;
&lt;h2&gt;The Inconvinience of Original Design&lt;/h2&gt;
&lt;p&gt;All the prerequisits are discussed and set, time to get back to the
testing library.  Suites gave us grouping, hierarchies, metadata
inheritance and while all that is cool, there is a fundamental issue:
only test runner has access to it. Tests have no clue about the
surrounding and it's limiting.  Let's sketch a hypothetical test suite
and dissect it next.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define db* (make-parameter #f))

(define create-admin-fixture
  (lambda (f)
    (init-db-with-admin-user! (db*) ...)
    (f)))

(define create-user-fixture
  (lambda (f)
    (init-db-with-basic-user! (db*) ...)
    (f)))

(define db-connection-fixture
  ;; In real-world code it's better to use dynamic-wind to make sure
  ;; teardown is executed on exception or other non-local control
  ;; transfer.
  (lambda (f)
    (parameterize ((db* (open-db-connection ...)))
      (f)
      (close-db-connection! (db*)))))

(define-suite (user-tests)
  'metadata
  `((fixtures ,create-user-fixture))

  (test &amp;quot;user is present&amp;quot;
    (is (user-exists? (db*) &amp;quot;user&amp;quot;)))

  (test &amp;quot;user id is set&amp;quot;
    (is (number? (user-id (db*) &amp;quot;user&amp;quot;))))

  (test &amp;quot;user is not admin&amp;quot;
    (is (not (member &amp;quot;admins&amp;quot; (get-user-groups (db*) &amp;quot;user&amp;quot;)))))

(define-suite (admin-tests)
  'metadata
  `((fixtures ,create-admin-fixture))

  (test &amp;quot;user is present&amp;quot;
    (is (user-exists? (db*) &amp;quot;admin&amp;quot;)))

  (test &amp;quot;user id is set&amp;quot;
    (is (number? (user-id (db*) &amp;quot;admin&amp;quot;))))

  (test &amp;quot;user is admin&amp;quot;
    (is (member &amp;quot;admins&amp;quot; (get-user-groups (db*) &amp;quot;admin&amp;quot;)))))

(define-suite (multiple-users-tests)
  'metadata
  `((fixtures ,create-admin-fixture ,create-user-fixture))

  ;; TODO: [Andrew Tropin, 2026-06-15] Make tests unaware of database,
  ;; by introducing users to context for db-csv-export-test

  (test &amp;quot;there are two users&amp;quot;
    (define users (get-users (db*)))
    (is (= 2 (length users))))

  (test &amp;quot;admin and user are present&amp;quot;
    (define users (get-users (db*)))
    (define user-names (map user-name users))

    (is (member &amp;quot;admin&amp;quot; user-names))
    (is (member &amp;quot;user&amp;quot; user-names))))


(define-suite (db-tests)
  'metadata
  `((fixtures ,db-connection-fixture))

  (user-tests)
  (admin-tests)
  (multiple-users-tests))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code is relatively straightforward and due to its size still looks
quite elegant, but when we start scaling up the amount of tests in our
project, we will see some issue emerging and biting us.  Let's focus
on two primary that are already visible on this suite.&lt;/p&gt;
&lt;p&gt;From library design introduction section we remember that tests are
independent executable units, they can be run and re-run in arbitrary
order, multiple times.  They must work despite the order and number of
re-runs.  That means, we have to setup a proper clean environment
everytime we run a test and that's totally fine and expected.&lt;/p&gt;
&lt;p&gt;Simple option could be to repeat setup and teardown in each test
manually, but it lead to significant repetitions when the setup is the
same for multiple tests, and the extra setup code visually obscures
the test's logic.&lt;/p&gt;
&lt;p&gt;Luckily, in the example above we do it with fixtures.  Fixtures are
reusable and composable.  The issue here is that test-runner has no
direct communication channel with tests.  Which means there is no way
to explicitly share the execution context with a test.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;For the fixtures it means we forced to provide execution
environment via dynamically scoped variables. It couples tests,
fixtures via &lt;code&gt;db*&lt;/code&gt; parameter.  It has all the cons of parameters.
Excessive coupling is no good too.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Not having access to execution context from inside the test means
we can't adjust its behavior and reuse same in multiple different
contexts. In real life it often handy, e.g. to test API-compatible
implementations with the same test suite or when we want to run
same checks, but with sligtly different settings or data sources.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's call those issues Excessive Coupling and Context Unawareness.
This way it will be easier to reference them and discuss how the
solution addresses them.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Syntax-wise the solution is exceptionally simple, we just add an
argument to the test that can be referenced in its body.  We wrap it
in parenthesis to make it look clearer.  That's it:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;;; The old syntax
(test &amp;quot;description&amp;quot;
  (is (ok? (some-external-dependency*)))
  (is (good? value)))

;; becomes =&amp;gt;

(test (&amp;quot;description&amp;quot; ctx)
  (is (ok? (assoc-ref ctx 'value)))
  (is (good? value)))

;; ctx, _, context you name it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition to syntax changes, our test's &lt;code&gt;test/body-procedure&lt;/code&gt;
changes from a zero-argument to single-argument procedure.  We also
need to update test-runner implementation to properly construct the
context and pass it to &lt;code&gt;test/body-procedure&lt;/code&gt;, but it's trivial.&lt;/p&gt;
&lt;p&gt;Now, we all set, so let's see the effect.&lt;/p&gt;
&lt;h3&gt;Contextual Awareness&lt;/h3&gt;
&lt;p&gt;Contextual Awareness provides multiple benifits, but we will start
from deduplication.  You probably noticied a repetitive pattern in
&lt;code&gt;user-tests&lt;/code&gt; and &lt;code&gt;admin-tests&lt;/code&gt;.  The existing checks are almost
identical with a minor difference of user name to be checked.  The
original code was:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define-suite (user-tests)
  'metadata
  `((fixtures ,create-user-fixture))

  (test &amp;quot;user is present&amp;quot;
    (is (user-exists? (db*) &amp;quot;user&amp;quot;)))

  (test &amp;quot;user id is set&amp;quot;
    (is (number? (user-id (db*) &amp;quot;user&amp;quot;))))

  (test &amp;quot;user is not admin&amp;quot;
    (is (not (member &amp;quot;admins&amp;quot; (get-user-groups (db*) &amp;quot;user&amp;quot;)))))

(define-suite (admin-tests)
  'metadata
  `((fixtures ,create-admin-fixture))

  (test &amp;quot;user is present&amp;quot;
    (is (user-exists? (db*) &amp;quot;admin&amp;quot;)))

  (test &amp;quot;user id is set&amp;quot;
    (is (number? (user-id (db*) &amp;quot;admin&amp;quot;))))

  (test &amp;quot;user is admin&amp;quot;
    (is (member &amp;quot;admins&amp;quot; (get-user-groups (db*) &amp;quot;admin&amp;quot;)))))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's extract the common part into a separate suite, refactor it to
new syntax and generalize it.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define-suite (user-set-correctly-tests)
  (test (&amp;quot;user is present&amp;quot; ctx)
    (is (user-exists? (db*) (assoc-ref ctx 'sut/user))))

  (test (&amp;quot;user id is set&amp;quot; ctx)
    (is (number? (user-id (db*) (assoc-ref ctx 'sut/user))))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of hardcoded user name, now we obtain it from &lt;code&gt;ctx&lt;/code&gt;.  To add
the name to the context, we will just modify a metadata for enclosing
suites. &lt;code&gt;sut&lt;/code&gt; stands for subject under the test.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define-suite (user-set-correctly-tests)
  (test (&amp;quot;user is present&amp;quot; ctx)
    (is (user-exists? (db*) (assoc-ref ctx 'sut/user))))

  (test (&amp;quot;user id is set&amp;quot; ctx)
    (is (number? (user-id (db*) (assoc-ref ctx 'sut/user))))))

(define-suite (user-tests)
  'metadata
  `((fixtures ,create-user-fixture)
    (sut/user . &amp;quot;user&amp;quot;))

  (user-set-correctly-tests)

  (test (&amp;quot;user is not admin&amp;quot; _)
    (is (not (member &amp;quot;admins&amp;quot; (get-user-groups (db*) &amp;quot;user&amp;quot;))))))

(define-suite (admin-tests)
  'metadata
  `((fixtures ,create-admin-fixture)
    (sut/user . &amp;quot;admin&amp;quot;))

  (user-set-correctly-tests)

  (test (&amp;quot;user is admin&amp;quot; _)
    (is (member &amp;quot;admins&amp;quot; (get-user-groups (db*) &amp;quot;admin&amp;quot;)))))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In some scenarios duplication can be completely justified, but for the
cases where you do a lot of re-use, something like api-compatible
library reimplementation, copy-pasting and monkeypatching tests is a
guaranteed way to hell.&lt;/p&gt;
&lt;p&gt;Of course, code reuse is not the only benifit of contextual awarness.
By having an execution context accessible, test can control its
behavior.  For example if test sees &lt;code&gt;fast-run?&lt;/code&gt; set to &lt;code&gt;#t&lt;/code&gt; it can
skip expensive computations and related assertions.  Or in our
&lt;code&gt;user-set-correctly-tests&lt;/code&gt; suite we can add an extra assertion that
checks for user named &lt;code&gt;&amp;quot;admin&amp;quot;&lt;/code&gt; that corresponding permission field in
the database is initialized with a correct value.  The imagination is
the limit now, not a testing library :)&lt;/p&gt;
&lt;h3&gt;Excessive Uncoupling&lt;/h3&gt;
&lt;p&gt;Let's explore the situtation with dynamic vars, fixtures and
unnecesseray coupling and how much it is improved or worsen, hehe.&lt;/p&gt;
&lt;p&gt;Decoupling tests from dynamic vars is easy, we just obtain db or any
other part of execution environment from the context:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(test (&amp;quot;db access example&amp;quot; ctx)
  (define db (assoc-ref ctx 'db))
  (is (db-connection? db)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this small change, test became self-contained, decoupled from
fixture implementation, dynamic vars and whatever else.  The
dependency is now explicit, test connects directly to the test-runner
through context argument.&lt;/p&gt;
&lt;p&gt;After this update test-runner needs to contruct a proper execution
context and call &lt;code&gt;test/body-procedure&lt;/code&gt; with context as an argument.
The implementation is trivial, but this will also affect how fixtures
work. Instead of relying on some shared dynamically scoped variable
and implicit prameterization of it, now they just enrich context with
values necessary for other fixtures and tests, and pass it to the next
fixtures/test in the stack.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define (db-connection-fixture f)
  (lambda (ctx)
    (let ((db (open-db-connection ...)))
      ;; Add `(db . ,db) pair to context, so the further fixtures and
      ;; tests have access to db
      (f (acons 'db db ctx))

      (cleanup-db! db)
      (close-db-connection! db))))

(define (create-admin-fixture f)
  (lambda (ctx)
    (init-db-with-admin-user! (assoc-ref ctx 'db))
    (f ctx)))

(define (create-user-fixture f)
  (lambda (ctx)
    (init-db-with-basic-user! (assoc-ref ctx 'db))
    (f ctx)))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You probably already saw this pattern, where a number of composable
functions wrap each other in some particular order to build a context
and possibly process the return value and enrich it on the way back.
Such functions usually called middlewares.  This composition is very
sensitive to order, but the implementation is deadly trivial.&lt;/p&gt;
&lt;h2&gt;The Synergetic Power Of Friendship&lt;/h2&gt;
&lt;p&gt;Cool, context awareness added, excessive coupling removed, but can we
do now what was impossible before?  I got an example, where the both
improvements combines, synergize and provide a quite nice developer
experience.&lt;/p&gt;
&lt;p&gt;We have &lt;code&gt;multiple-users-tests&lt;/code&gt;, but didn't touch it yet. How about
making this suite generic enough, so it can be used both for testing
db connection and csv backup?&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;;; The original code
(define-suite (multiple-users-tests)
  'metadata
  `((fixtures ,create-admin-fixture ,create-user-fixture))

  (test &amp;quot;there are two users&amp;quot;
    (define users (get-users (db*)))
    (is (= 2 (length users))))

  (test &amp;quot;admin and user are present&amp;quot;
    (define users (get-users (db*)))
    (define user-names (map user-name users))

    (is (member &amp;quot;admin&amp;quot; user-names))
    (is (member &amp;quot;user&amp;quot; user-names))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we will make tests unaware of the source from which users are
comming.  They will only know that somebody provides a list of users
via context.  After rewriting tests to the new syntax and removing
dependency on &lt;code&gt;db*&lt;/code&gt; we get following:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define-suite (multiple-users-tests)
  (test (&amp;quot;there are two users&amp;quot; ctx)
    (define users (assoc-ref ctx 'users))
    (is (= 2 (length users))))

  (test (&amp;quot;admin and user are present&amp;quot; ctx)
    (define users (assoc-ref ctx 'users))
    (define user-names (map user-name users))

    (is (member &amp;quot;admin&amp;quot; user-names))
    (is (member &amp;quot;user&amp;quot; user-names))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that we return back the db-based functionality. We need an extra
fixture, which extracts users from database and put them into the
context.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define (db-&amp;gt;users-fixture f)
  (lambda (ctx)
    (chain
     (assoc-ref ctx 'db) ; obtain db connection from context
     (get-users _) ; pass it as an argument to get-users
     (acons 'users _ ctx) ; add a list of users to the context
     (f _) ; call fixture/test further down the stack
     )))

(define-suite (db-multiple-users-tests)
  'metadata
  `((fixtures ,create-admin-fixture ,create-user-fixture ,db-&amp;gt;users-fixture))

  (multiple-users-tests))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Refactoring is kinda complete and we can add new functionality:
running &lt;code&gt;multiple-users-tests&lt;/code&gt; on csv backup source instead of db
connection.  One more fixture and we ready to go.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define (csv-backup-&amp;gt;users-fixture f)
  (lambda (ctx)
    (chain
     ;; (assoc-ref ctx 'users-csv-file-name)
     &amp;quot;resources/users-table-backup.csv&amp;quot; ; hardcode filename for now
     (get-users-from-csv _) ; obtain list of users from csv
     (acons 'users _ ctx) ; add a list of users to the context
     (f _) ; call fixture/test further down the stack
     )))

(define-suite (csv-backup-tests)
  'metadata
  `((fixtures ,csv-backup-&amp;gt;users-fixture))

  (multiple-users-tests))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, neat and concise. Now imagine we are going further and try to
implement a full test suite for backup validation. We already have
fixtures for connecting to db, and initializing necessary values, so
all we need is to add a fixture which serializes a db table to the
file and we are ready to go: just take already existing db test, make
them independent from data source and run them on both original db
suites and backup verification suites.&lt;/p&gt;
&lt;p&gt;We won't demonstrate the implementation here, but I hope the idea
sparks the joy and curiosity in your head.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With one small change, we were able to achive quite a lot.  Let's
quickly recap what we've got:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A slightly more verbose syntax :'(&lt;/li&gt;
&lt;li&gt;A direct communication channel between test-runner and tests&lt;/li&gt;
&lt;li&gt;A subjectively clearer &lt;code&gt;test&lt;/code&gt; syntax which reads as a re-runnable entity&lt;/li&gt;
&lt;li&gt;Tests are reusable now&lt;/li&gt;
&lt;li&gt;Tests are context-aware and smart now! :)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Computation-wise, it didn't add something that was impossible before.
The both variants still turing-equivalent, but it's definitely changed
the UX/DX quite a lot and I hope to the better.&lt;/p&gt;
&lt;p&gt;The new syntax is reflected in the &lt;a href=&quot;https://github.com/scheme-requests-for-implementation/srfi-269/pull/1&quot;&gt;second
draft&lt;/a&gt;
of SRFI-269.  The fixtures and test-runner modifications for suitbl
testing library are still work-in-progress at the moment of writing.&lt;/p&gt;
&lt;h2&gt;Support&lt;/h2&gt;
&lt;p&gt;The work on &lt;a href=&quot;https://git.sr.ht/~abcdw/guile-ares-rs&quot;&gt;suitbl testing
library&lt;/a&gt; and
&lt;a href=&quot;https://srfi.schemers.org/srfi-269/&quot;&gt;SRFI-269&lt;/a&gt; is proudly funded by
&lt;a href=&quot;https://nlnet.nl&quot;&gt;NLnet&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you enjoyed the reading, consider &lt;a href=&quot;/support&quot;&gt;to support&lt;/a&gt; me, my
work and projects.  Any help, being it a coin or a word makes a
difference.&lt;/p&gt;
&lt;h2&gt;Future work&lt;/h2&gt;
&lt;p&gt;The one thing that still bothers me is that metadata attached to the
suite-loader, which means to adjust metadata you have to wrap one
suite into another.  Seems reasonable, but there is the other option:
we could make it possible to call suite-loader with optional metadata
argument, which will enchance original metadata, so instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define-suite (my-suite)
  'metadata
  '((original-metadata . #t))
  (test ...)
  (test ...))

(define-suite (proxy-1-suite)
  'metadata
  '((extra-metadata . 1))
  (my-suite))

(define-suite (proxy-2-suite)
  'metadata
  '((extra-metadata . 2))
  (my-suite))

(define-suite (main-suite)
  (proxy-1-suite)
  (proxy-2-suite)
  (other-important-suite))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we can do something like:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define-suite (my-suite)
  'metadata
  '((original-metadata . #t))
  (test ...)
  (test ...))

(define-suite (main-suite)
  (my-suite '((extra-metadata . 1)))
  (my-suite '((extra-metadata . 2)))
  (other-important-suite))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Definitely looks more elegant, but it's a very recent design idea, so
other implications are not clear yet. Need a bit of time to process
and think about it.&lt;/p&gt;
&lt;p&gt;BTW, SRFI-269 will be finalized in a couple of weeks, so there is a
chance to share feedback, uncovered use cases, &lt;a href=&quot;https://srfi-email.schemers.org/srfi-269/&quot;&gt;propose
changes&lt;/a&gt; before the spec
set in stone.&lt;/p&gt;
&lt;h2&gt;Appendix&lt;/h2&gt;
&lt;h3&gt;Complete example&lt;/h3&gt;
&lt;p&gt;All the snippets put together in one place:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define (db-connection-fixture f)
  (lambda (ctx)
    (let ((db (open-db-connection ...)))
      ;; Add `(db . ,db) pair to context, so the further fixtures and
      ;; tests have access to db
      (f (acons 'db db ctx))

      (cleanup-db! db)
      (close-db-connection! db))))

(define (create-admin-fixture f)
  (lambda (ctx)
    (init-db-with-admin-user! (assoc-ref ctx 'db))
    (f ctx)))

(define (create-user-fixture f)
  (lambda (ctx)
    (init-db-with-basic-user! (assoc-ref ctx 'db))
    (f ctx)))

;;; User tests

(define-suite (user-set-correctly-tests)
  (test (&amp;quot;user is present&amp;quot; ctx)
    (is (user-exists? (assoc-ref ctx 'db) (assoc-ref ctx 'sut/user))))

  (test (&amp;quot;user id is set&amp;quot; ctx)
    (is (number? (user-id (assoc-ref ctx 'db) (assoc-ref ctx 'sut/user))))))

(define-suite (user-tests)
  'metadata
  `((fixtures ,create-user-fixture)
    (sut/user . &amp;quot;user&amp;quot;))

  (user-set-correctly-tests)

  (test (&amp;quot;user is not admin&amp;quot; ctx)
    (is (not (member &amp;quot;admins&amp;quot; (get-user-groups (assoc-ref ctx 'db) &amp;quot;user&amp;quot;))))))

(define-suite (admin-tests)
  'metadata
  `((fixtures ,create-admin-fixture)
    (sut/user . &amp;quot;admin&amp;quot;))

  (user-set-correctly-tests)

  (test (&amp;quot;user is admin&amp;quot; ctx)
    (is (member &amp;quot;admins&amp;quot; (get-user-groups (assoc-ref ctx 'db) &amp;quot;admin&amp;quot;)))))

(define-suite (multiple-users-tests)
  (test (&amp;quot;there are two users&amp;quot; ctx)
    (define users (assoc-ref ctx 'users))
    (is (= 2 (length users))))

  (test (&amp;quot;admin and user are present&amp;quot; ctx)
    (define users (assoc-ref ctx 'users))
    (define user-names (map user-name users))

    (is (member &amp;quot;admin&amp;quot; user-names))
    (is (member &amp;quot;user&amp;quot; user-names))))

;;; Database suites

(define (db-&amp;gt;users-fixture f)
  (lambda (ctx)
    (chain
     (assoc-ref ctx 'db) ; obtain db connection from context
     (get-users _) ; pass it as an argument to get-users
     (acons 'users _ ctx) ; add a list of users to the context
     (f _) ; call fixture/test further down the stack
     )))

(define-suite (db-multiple-users-tests)
  'metadata
  `((fixtures ,create-admin-fixture ,create-user-fixture ,db-&amp;gt;users-fixture))

  (multiple-users-tests))

(define-suite (db-tests)
  'metadata
  `((fixtures ,db-connection-fixture))

  (user-tests)
  (admin-tests)
  (db-multiple-users-tests))

;;; CSV backup suite

(define (csv-backup-&amp;gt;users-fixture f)
  (lambda (ctx)
    (chain
     ;; (assoc-ref ctx 'users-csv-file-name)
     &amp;quot;resources/users-table-backup.csv&amp;quot; ; hardcode filename for now
     (get-users-from-csv _) ; obtain list of users from csv
     (acons 'users _ ctx) ; add a list of users to the context
     (f _) ; call fixture/test further down the stack
     )))

(define-suite (csv-backup-tests)
  'metadata
  `((fixtures ,csv-backup-&amp;gt;users-fixture))

  (multiple-users-tests))


;;; All tests suite

(define-suite (all-tests)
  (db-tests)
  (csv-backup-tests))
&lt;/code&gt;&lt;/pre&gt;
</content></entry><entry><title>Quick Review of &quot;Nix Flakes and their Guix Equivalents&quot;</title><id>https://trop.in/blog/quick-review-of-nix-flakes-and-their-guix-equivalents.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2026-06-13T12:00:00Z</updated><link href="https://trop.in/blog/quick-review-of-nix-flakes-and-their-guix-equivalents.html" rel="alternate" /><content type="html">&lt;p&gt;There is a recent blog post about Nix Flakes vs Guix:
&lt;a href=&quot;https://web.archive.org/web/20260613000103/https://coopi.neocities.org/posts/nix-flakes-vs-guix&quot;&gt;https://coopi.neocities.org/posts/nix-flakes-vs-guix&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After briefly skimming it, I realized it definitely not fact
checked. After re-reading it again I started to suspect that the text
of the post was highly LLM assisted*.&lt;/p&gt;
&lt;p&gt;I wanted to ignore it at first, but later I saw Ludovic &lt;a href=&quot;https://toot.aquilenet.fr/@civodul/116739238432127929&quot;&gt;endorsing
it&lt;/a&gt; and a few
other peers keep sharing the post with me, so I decided to write a
review.&lt;/p&gt;
&lt;p&gt;Despite the way that post is written (with LLMs or not), it still
touches the important topics. And I've spent quite some time working
and thinking on them, so here I'm to share my thoughts.&lt;/p&gt;
&lt;p&gt;I'll replicate the sections structure and will be writing my notes and
quote original text when necessary.&lt;/p&gt;
&lt;p&gt;P.S. When I was writing this text, I found my old notes on the related
topic: &lt;a href=&quot;https://github.com/abcdw/notes/blob/master/notes/20240511144223-guix_channels_are_not_flakes.org&quot;&gt;Guix Channels are not
Flakes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;P.P.S. I'm sad to see &lt;a href=&quot;https://coopi.neocities.org/posts/taking-down-nix-flakes-vs-guix&quot;&gt;feelings of the author got
hurt&lt;/a&gt;
by my suspections.  In fact, I didn't even care much if their post was
hand-crafted or not:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; Despite the way that post is written (with LLMs or not)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And definitely didn't care enough about a potential human on the other
end of the cable.  I appreciate style, art and craft, but it was
compeletely out of my focus at the moment of writing (my eyes still
can barely focus on close up objects yet, heh).&lt;/p&gt;
&lt;p&gt;It's very likely I can understand the feelings and moreover can
probably relate.  I had a similiar expreience with my very &lt;a href=&quot;https://trop.in/blog/i-have-to-live-in-a-forest-to-work-on-open-source&quot;&gt;own
writing&lt;/a&gt;
about times of my life in the forest. Somebody shared it on hn and in
a few hours it was
&lt;a href=&quot;https://news.ycombinator.com/item?id=46308295&quot;&gt;accused&lt;/a&gt; of being
AI-generated and drug induced and then flagged.  I know how unpleasant
and upsetting it can be. I also know how discouraging it is to get a
paper review with devaluing comment in the first sentence.&lt;/p&gt;
&lt;p&gt;I'm always unhappy, when my actions unfairly hurt people's feelings
and discourage them.  I'm always happy to be wrong about my
pessimistic assumptions.&lt;/p&gt;
&lt;p&gt;Stay strong and happy my friends.&lt;/p&gt;
&lt;h2&gt;Table of Content&lt;/h2&gt;
&lt;p&gt;There is a TOC of a blog post under review:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 1. Who is this for?
&amp;gt; 2. What even is a flake, anyway?
&amp;gt; 3. Declaring dependencies: inputs vs. channels
&amp;gt;
&amp;gt;     3.1. Flakes: inputs
&amp;gt;     3.2. Guix: channels
&amp;gt;     3.3. The comparison
&amp;gt;
&amp;gt; 4. Pinning dependencies: flake.lock vs. guix describe
&amp;gt;
&amp;gt;     4.1. Flakes: flake.lock
&amp;gt;     4.2. Guix: guix describe and guix time-machine
&amp;gt;     4.3. The comparison
&amp;gt;
&amp;gt; 5. Purity: enforced isolation
&amp;gt;
&amp;gt;     5.1. Flakes: pure evaluation mode
&amp;gt;     5.2. Guix: purity by design
&amp;gt;     5.3. The comparison
&amp;gt;
&amp;gt; 6. The output schema: what your project produces
&amp;gt;
&amp;gt;     6.1. Flakes: structured outputs
&amp;gt;     6.2. Guix: first-class records and modules
&amp;gt;     6.3. The comparison
&amp;gt;
&amp;gt; 7. Development environments: devShells vs. manifests
&amp;gt;
&amp;gt;     7.1. Flakes: devShells
&amp;gt;     7.2. Guix: guix shell and manifests
&amp;gt;     7.3. The comparison
&amp;gt;
&amp;gt; 8. System configuration: nixosConfigurations vs. operating-system
&amp;gt;
&amp;gt;     8.1. Flakes: nixosConfigurations
&amp;gt;     8.2. Guix: operating-system
&amp;gt;     8.3. The comparison
&amp;gt;
&amp;gt; 9. So what does Guix NOT have?
&amp;gt;
&amp;gt;     9.1. A standard project entry point
&amp;gt;     9.2. A registry and quick-install syntax
&amp;gt;     9.3. nix flake show
&amp;gt;
&amp;gt; 10. And what does Guix have that flakes don't?
&amp;gt;
&amp;gt;     10.1. guix time-machine
&amp;gt;     10.2. Grafting
&amp;gt;     10.3. First-class package records
&amp;gt;     10.4. Full-source bootstrapping
&amp;gt;     10.5. Channel authentication
&amp;gt;
&amp;gt; 11. Summary table
&amp;gt; 12. So… who wins?
&amp;gt; 13. Yes I actually cited my sources :3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;quot;2. What even is a flake, anyway?&amp;quot;&lt;/h2&gt;
&lt;p&gt;This is the longest note, feel free to skip if you familiar with
overall nix and flakes history.&lt;/p&gt;
&lt;p&gt;There was an original Nix with it not-a-perfect bunch of CLI tools:
nix-channel, nix-shell, nix-build, nixos-rebuild, etc, etc.&lt;/p&gt;
&lt;p&gt;They did the job, but were far from perfect. For example they used
&lt;code&gt;NIX_PATH&lt;/code&gt; for finding files with nix expressions. Nix lang itself
allows to access environment variables and local fs (which is
obviously bad for reproducibility, as well for maintainability and
other properties of the code). AFAIR, CLI tools themselves were not
very consistent.&lt;/p&gt;
&lt;p&gt;Even on top of those tools people started to make their projects. To
make those projects reproducible (so other person can clone and
run/build it) they needed some way to pin the dependencies (as you
can't be sure that nix channels in your system are exactly the same as
of yours colleague).  And number of tools appeared: from simple
&lt;code&gt;fetchTarball&lt;/code&gt; of the exact nixpkgs revision, to more advanced &lt;a href=&quot;https://github.com/timbertson/nix-pin&quot;&gt;nix-pin&lt;/a&gt;,
&lt;a href=&quot;https://github.com/nmattia/niv&quot;&gt;niv&lt;/a&gt;, &lt;a href=&quot;https://github.com/andir/npins&quot;&gt;npins&lt;/a&gt; and probably a dozen more of alternatives and ad-hoc
solutions.&lt;/p&gt;
&lt;p&gt;Each of them had it own downsides, and most of them were incompatible
and noninteroperable with each other and often not even composable
with themselves.&lt;/p&gt;
&lt;p&gt;So, what is Nix flakes?  Nix flakes is an attempt to provide a
standardized format for writing nix expressions, with explicit inputs
and outputs. + a set of tools for managing/pinning all of that.&lt;/p&gt;
&lt;p&gt;Nix flakes can be composed (reference another flake and reuse it
outputs and/or override their inputs). They can be hermetically
evaluated (in pure enironment, without accessing env vars, local fs,
etc). Pay attention that nix evaluation and package build is two
different phases: packages themselves are already built in isolated
environment by build daemon, but to get &amp;quot;build recipes&amp;quot; you need to
evaluate a nix expression and its better to be hermetic as well.&lt;/p&gt;
&lt;p&gt;To sum up flakes supposed to provide:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Unified Project/Repo/Nix expression Structure&lt;/li&gt;
&lt;li&gt;Explorability&lt;/li&gt;
&lt;li&gt;Composability&lt;/li&gt;
&lt;li&gt;Dependency resolution, overriding and pinning&lt;/li&gt;
&lt;li&gt;Hermetic evaluation&lt;/li&gt;
&lt;li&gt;Convenience&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; Now here's the key insight: Guix already had solutions for most of
&amp;gt; these before flakes were introduced in Nix 2.4 on November 1, 2021
&amp;gt; (Project, 2021). The channels mechanism landed in Guix around
&amp;gt; 2018–2019 (Contributors, 2025a). And the solutions are orthogonal —
&amp;gt; you can use each one independently, without buying into a single
&amp;gt; monolithic abstraction.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Almost none of the items above are covered by Guix yet.  And some of
them are technically impossible without fundamental architecture
changes.  Let's go through other sections and explore each point in
more details.&lt;/p&gt;
&lt;h2&gt;&amp;quot;3 Declaring dependencies: inputs vs. channels&amp;quot;&lt;/h2&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; Running guix pull fetches all channels, compiles them, and makes
&amp;gt; their modules available to every guix command. This is your
&amp;gt; dependency resolution step.
&amp;gt;
&amp;gt; Channels can declare dependencies on other channels using a
&amp;gt; .guix-channel file in the repo root (Contributors, 2025b):
&amp;gt;
&amp;gt; ;; .guix-channel — lives at the root of a channel repository.  This tells Guix
&amp;gt; ;; that this channel depends on another channel called 'nonguix', so guix pull
&amp;gt; ;; will fetch both together.
&amp;gt; (channel
&amp;gt;   (version 0)
&amp;gt;   (dependencies
&amp;gt;     (channel
&amp;gt;       (name 'nonguix)
&amp;gt;       (url &amp;quot;https://gitlab.com/nonguix/nonguix&amp;quot;))))
&amp;gt;
&amp;gt; This is roughly analogous to inputs in a flake — one channel can
&amp;gt; pull in another. When you guix pull, all transitive channel
&amp;gt; dependencies are fetched together.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a huge difference in dependency resolution algorithm.&lt;/p&gt;
&lt;p&gt;Imagine in your flake you have flakes A and B as inputs. A depends on
flake C@rev1, B depends on flake C@rev2. (Revisions are always pinned,
when flake.lock is present).&lt;/p&gt;
&lt;p&gt;You will have 4 items resolved A@some-rev1, B@some-other-rev, C@rev1
and C@rev2.  The nix expressions from flake A will use code from
C@rev1 and B from C@rev2.&lt;/p&gt;
&lt;p&gt;If you want to override inputs for flake A, to use the same version of
C as in flake B, you can do it.  If you want build A against C@rev3
you can do it as well.  Just specify input override for the
&lt;code&gt;inputs.A&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now to the Guix channels.  Imagine similiar situation. Channel A
depends on channel C@rev1, channel B on C@rev2 and your channel
depends on both A and B.&lt;/p&gt;
&lt;p&gt;During the resolution you will end up with only 3 channels
(A@latest-revA, B@latest-revB, and C@rev1).  And B will be built
against C@rev1 and very likely will fail in general case.&lt;/p&gt;
&lt;p&gt;If for some reason A specified C as a dependency, but did not
specified the revision, you will end up with (A@latest-revA,
B@latest-revB, and C@rev2).&lt;/p&gt;
&lt;p&gt;There is a reason for it.  As channels use guile modules, you will get
name clash if you have two revisions of the same channel.  In fact you
can get module clashes anyway (same module name comming from different
channels) and then the first loaded will win (you will never know
which one, haha).&lt;/p&gt;
&lt;p&gt;P.S. There are inferiors to be able to have multiple revisions of the
same channel, but they are hacky, unreliable and not used in practice.&lt;/p&gt;
&lt;p&gt;To sum up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;(minor) You can git unpinned transitive dependency and it will be
your work to pin and manage them.&lt;/li&gt;
&lt;li&gt;In guix you can't have channels with the same name, but different
revisions.  So you forced to build all the channels against one
version of the dependency. Very ironic for a functional package
manager.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt;  Both systems let you declare external dependencies and pull them in
&amp;gt;  automatically. The main differences:
&amp;gt;
&amp;gt;     - Flakes are per-project — each repo has its own flake.nix with
&amp;gt;       its own inputs. Channels are system-wide or per-user — your
&amp;gt;       channels.scm applies to all guix invocations. This means flakes
&amp;gt;       naturally support different projects with different dependency
&amp;gt;       sets, while with Guix, you'd typically use guix time-machine or
&amp;gt;       separate profiles to achieve the same effect.
&amp;gt;
&amp;gt;     - Flakes use a URL-like syntax for references
&amp;gt;       (github:NixOS/nixpkgs, git+https://...) while channels use
&amp;gt;       plain Git URLs. The flake syntax is more ergonomic for quick
&amp;gt;       references, but channels are simpler and more explicit.
&amp;gt;
&amp;gt;     - Flakes support non-flake inputs (flake = false;) for repos
&amp;gt;       that don't contain a flake.nix. In Guix, a channel is just a
&amp;gt;       Git repo with Scheme files — there's no special opt-in
&amp;gt;       required. Any repo with Guile modules can be a channel.
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no need to compare &lt;code&gt;guix pull&lt;/code&gt; and &lt;code&gt;flakes&lt;/code&gt;. &lt;code&gt;nix-channel&lt;/code&gt; is
what the closest to &lt;code&gt;guix pull&lt;/code&gt; in this context. They are quite
similiar.  There is no per-user/per-project distinction. Both &lt;code&gt;guix pull&lt;/code&gt; and &lt;code&gt;nix-channel&lt;/code&gt; are &amp;quot;per-user&amp;quot; tools.&lt;/p&gt;
&lt;p&gt;If we talk about ad-hoc &lt;code&gt;guix time-machine&lt;/code&gt; + &lt;code&gt;channels-lock.scm&lt;/code&gt; +
&lt;code&gt;.guix-channel&lt;/code&gt;.  Then it's far from flakes, it's somewhere one step
behind &amp;quot;&lt;code&gt;nix-pin&lt;/code&gt; and friends times&amp;quot;. One step behind because their is
no widespread &lt;code&gt;guix-pin&lt;/code&gt; tools yet, everyone is doing what they think
is better.  And if it careful enough it usually quite monstrous:
&lt;a href=&quot;https://github.com/abcdw/notes/blob/master/notes/20240210123238-2024_02_10_guix_workflow.org&quot;&gt;2024-02-10 Reproducible Dev Environment Workflow with
Guix&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&amp;quot;4. Pinning dependencies: flake.lock vs. guix describe&amp;quot;&lt;/h2&gt;
&lt;p&gt;I don't want to nit-pick the whole section. So I'll go straight to
important points and will ignore the rest.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; This is your lock file, essentially. It lives in
&amp;gt; ~/.config/guix/current (as a Guile profile), not as a file in your
&amp;gt; project directory.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's a profile's provenance.  Lock file should be created and
available straight after dependency resolution, not after everything
is already built.  I would prefer to see a lock file immediately to
know what I will be working with, not 3 hours after, when everything
is already built.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; The guix time-machine command is genuinely unique and has no direct
&amp;gt; flake equivalent. It lets you travel to any point in Guix's history
&amp;gt; — not just to pinned dependency versions, but to a completely
&amp;gt; different state of the package collection (Contributors,
&amp;gt; 2025d). This is incredibly powerful for reproducibility. Like, you
&amp;gt; can run code from three years ago and it JUST WORKS?? That's wild!!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is an equivalent of
&lt;code&gt;guix time-machine -C channel-lock.scm -- build ...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nix build --reference-lock-file alternative-flake.lock ...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If for some reason one need an exact version of nix cli, they can
achieve it with the example above and &lt;code&gt;run nix whatever&lt;/code&gt; .&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; flake.lock is per-project and automatic. guix describe is per-user
&amp;gt; and automatic, while channels.scm with pinned commits is per-project
&amp;gt; but manual.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yeah, there is no good alternative to flake-like mechanism in Guix and
significantly affect UX/DX. And to mimic a fraction of flakes you need
to write a hundred lines of code: &lt;a href=&quot;https://github.com/abcdw/notes/blob/master/notes/20240210123238-2024_02_10_guix_workflow.org&quot;&gt;2024-02-10 Reproducible Dev Environment Workflow with Guix&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&amp;quot;5. Purity: enforced isolation&amp;quot;&lt;/h2&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; Guix doesn't need a &amp;quot;pure evaluation mode&amp;quot; because its evaluation is
&amp;gt; already pure by convention (LWN.net, 2024).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's not clear what this link references to, there is no mention of
pure evaluation mode and it's not clear what &amp;quot;pure by convention&amp;quot;
means.  I didn't find any related text or comments on LWN.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; Guile modules don't have access to environment variables unless you
&amp;gt; explicitly pass them in. There's no equivalent of $NIX_PATH — Guix
&amp;gt; resolves packages through its module system, not through a search
&amp;gt; path. builtins.currentSystem doesn't exist because there's no
&amp;gt; equivalent concept; you specify systems explicitly via package
&amp;gt; metadata and the --system flag.
&amp;gt;
&amp;gt; Guix achieves purity through architecture — Scheme modules are
&amp;gt; inherently more contained than Nix's channel/path system. Flakes
&amp;gt; achieve it through enforcement — a restricted evaluation mode
&amp;gt; layered on top of an otherwise impure system. Both get you to the
&amp;gt; same place. Guix's approach is arguably more elegant because it
&amp;gt; doesn't need to layer restrictions on top of something that was
&amp;gt; originally designed without them. (Though honestly, the fact that
&amp;gt; Nix managed to retrofit purity at all is kind of impressive — it's
&amp;gt; just a different philosophy of getting there.)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Guix uses &lt;a href=&quot;https://trop.in/blog/how-to-set-up-guile-load-path&quot;&gt;GUILE_LOAD_PATH&lt;/a&gt; for module resolution and loading.&lt;/li&gt;
&lt;li&gt;Guix CLI also respects &lt;code&gt;GUIX_PACKAGE_PATH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Guile code has access to env variables, local fs, and all kind of
side effects one can imagine. One can easly do &lt;code&gt;(getenv &amp;quot;PHASE_OF_THE_MOON&amp;quot;)&lt;/code&gt; from inside a package or operating-system
definition.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All the claims about evaluation purity in this context are factually
incorrect.&lt;/p&gt;
&lt;h2&gt;&amp;quot;6-8&amp;quot;&lt;/h2&gt;
&lt;p&gt;I skip those sections as less significant or because the points were
already covered earlier.  Many of the statements seems not
fact-checked, but still they are too minor to spend time on them.&lt;/p&gt;
&lt;h2&gt;&amp;quot;9. So what does Guix NOT have?&amp;quot;&lt;/h2&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 9.1. A standard project entry point
&amp;gt;
&amp;gt; Flakes have flake.nix — one file that declares dependencies, defines
&amp;gt; outputs, and provides a discoverable schema. There's nothing stopping
&amp;gt; you from finding flake.nix and understanding the project's structure
&amp;gt; at a glance.
&amp;gt;
&amp;gt; Guix projects are more convention-based. You might find manifest.scm,
&amp;gt; channels.scm, guix.scm, package.scm, or something else
&amp;gt; entirely. There's been some movement toward standardizing guix.scm as
&amp;gt; a project file that guix shell picks up automatically (Contributors,
&amp;gt; 2025h), but it's not as established as flake.nix.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Overall, it's true, but at this point I hope you already understand
how far guix.scm from flake.nix is.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 9.2. A registry and quick-install syntax
&amp;gt;
&amp;gt; nix build github:NixOS/nixpkgs#firefox
&amp;gt;
&amp;gt; Guix uses package specifications for similar ergonomics:
&amp;gt;
&amp;gt; guix shell hello
&amp;gt; guix install firefox
&amp;gt;
&amp;gt; But there's no equivalent of the registry for pointing at arbitrary
&amp;gt; Git repos by short name. You just use the URL. Honestly I think this
&amp;gt; is fine — the registry has been a source of confusion in the Nix
&amp;gt; world, since it's not always clear whether nixpkgs refers to the
&amp;gt; registry entry, a local path, or something else.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recently guix introduced url for channels specification, so it's
possible to do something like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;guix time-machine -C https://my.org/prj/channel.scm -- shell my-package&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;p&gt;&lt;code&gt;guix time-machine -C 'swh:1:cnt:&amp;lt;content-hash-of-channels.scm&amp;gt;' -- shell my-package&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There is are no registry for associating urls with short names in
Guix, but I don't think it's any significant.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 9.3. nix flake show
&amp;gt;
&amp;gt; The nix flake show command is genuinely nice — it gives you a tree
&amp;gt; view of everything a flake provides (Contributors, 2026). Guix has
&amp;gt; guix search for packages and guix system search for services, but
&amp;gt; there's no equivalent of &amp;quot;show me everything this project/repo
&amp;gt; provides.&amp;quot; You just look at the Scheme files.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No flakes - no flake show, heh.  There is no standardization, there is
no expectations you can make.  Composing, reusing?  Nah.&lt;/p&gt;
&lt;h2&gt;10. And what does Guix have that flakes don't?&lt;/h2&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; I mentioned this earlier, but it deserves emphasis. The ability to
&amp;gt; say &amp;quot;run this command as if it were any arbitrary date in Guix's
&amp;gt; history&amp;quot; is incredibly powerful for reproducibility (Contributors,
&amp;gt; 2025d). Flakes can pin dependencies, but you can't easily say &amp;quot;run
&amp;gt; this with the version of nixpkgs from six months ago&amp;quot; without
&amp;gt; manually finding and specifying the commit. With Guix, guix
&amp;gt; time-machine --commit=... -- does exactly this. I love this feature
&amp;gt; SO MUCH!!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Already mentioned before, it's trivially achieveable on nix.  With
flakes and without.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 10.2. Grafting
&amp;gt;
&amp;gt; Guix has a feature called grafting that lets it apply security
&amp;gt; updates to the dependency tree without rebuilding every dependent
&amp;gt; package (Contributors, 2025j). When a low-level library like glibc
&amp;gt; has a vulnerability, Guix can swap in the fixed version by rewriting
&amp;gt; store paths. Nix rebuilds everything. For a large dependency tree,
&amp;gt; the difference can be hours of build time. This is a HUGE advantage.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is completely unrelated to flakes, it's about nix in general.&lt;/p&gt;
&lt;p&gt;The lack of grafting in nix is true.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 10.3. First-class package records
&amp;gt;
&amp;gt; In Nix, packages are functions — you call stdenv.mkDerivation {
&amp;gt; ... } and it returns a derivation, which is an opaque attribute
&amp;gt; set. In Guix, packages are &amp;lt;package&amp;gt; records — transparent data
&amp;gt; structures with named fields that you can inspect, transform, and
&amp;gt; compose with standard Scheme procedures (Contributors, 2025f).
&amp;gt;
&amp;gt; This means you can do things like:
&amp;gt;
&amp;gt; ;; package-input-rewriting walks the entire dependency graph and replaces every
&amp;gt; ;; occurrence of 'perl' with 'perl-minimal'.  Try doing that in one line with
&amp;gt; ;; Nix!!
&amp;gt; (package-input-rewriting `((,perl . ,perl-minimal)))
&amp;gt;
&amp;gt; ;; The 'inherit' keyword works like inheriting from a parent class — you get all
&amp;gt; ;; the fields of 'coreutils' but override just the ones you specify.
&amp;gt; (package
&amp;gt;   (inherit coreutils)
&amp;gt;   (arguments
&amp;gt;    (substitute-keyword-arguments (package-arguments coreutils)
&amp;gt;      ((#:tests? _ #f) #f))))
&amp;gt;
&amp;gt; Graph rewriting is trivial in Guix because packages are data, not
&amp;gt; functions ((rekado), 2019). Nix has overlays for a similar purpose,
&amp;gt; but they're less ergonomic because the opaque function interface
&amp;gt; makes inspection and transformation harder.
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Nix packages are values, and can be expected the same way as Scheme
values.  Package definition files are functions, which accept an
attrset of everything (tools, pcakages, APIs), extract all necessary
dependencies, construct and return a attrset value representing a
derivation.&lt;/p&gt;
&lt;p&gt;The cool thing here, Nix is lazily-evaluated and one can provide a
different set of tools/packages to the same package definition to get
a new package.  That means if I update one package, all the dependent
packages will be updated during evaluation.&lt;/p&gt;
&lt;p&gt;There is a mechanism called overlays. Basically, it's just a function,
which accepts takes argument a self-reference to new attrset and an
old attrset.  After that you express transformation as a simple
function operating on recursive data structure.&lt;/p&gt;
&lt;p&gt;Requires a bit of understanding to use it, but it is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;convenient&lt;/li&gt;
&lt;li&gt;powerful&lt;/li&gt;
&lt;li&gt;first-class supported&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In terms of simple vs easy. It's simple.&lt;/p&gt;
&lt;p&gt;In Guix, you rewriting a graph is easy, you can get a transformer
function, which will go and update all the affected packages.  It's
not lazy-evaluated, so it will update all the things right now, even
if you will never use them.&lt;/p&gt;
&lt;p&gt;The worst part, is that you need to get a set of packages to apply the
input-rewrite transformer and when you define &lt;code&gt;operating-system&lt;/code&gt; or
&lt;code&gt;home-environment&lt;/code&gt; you will have hard time injecting this rewrite into
the right place.  If you are not guix developer, it's almost
impossible to do.  Most likely, you will end up just duplicating part
of the dependency graph and having multiple version of the same libs
or will just get a conflicting version of the same binary in the
profile and build failure.&lt;/p&gt;
&lt;p&gt;Dependency rewriting in Guix is easy, but not simple.&lt;/p&gt;
&lt;p&gt;In both nix and guix packages are first-class citizens, but package
rewrite are first-class only in Nix. :'(&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 10.4. Full-source bootstrapping
&amp;gt;
&amp;gt; Guix is obsessive about bootstrapping from source (Contributors,
&amp;gt; 2025k). The entire system can be built from a tiny trusted computing
&amp;gt; base — a ~500-byte hex assembler, then the mes C compiler written in
&amp;gt; Scheme, then tcc, then the full GNU toolchain, and up from there
&amp;gt; ((janneke) Nieuwenhuizen, 2023). The bootstrappable builds project
&amp;gt; has the details and it is WILD. Nix relies on more binary
&amp;gt; seeds. This matters for trust and verifiability — if you can't audit
&amp;gt; the bootstrap chain, you can't truly verify that your system was
&amp;gt; built from the sources you think it was.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nix is &lt;a href=&quot;https://nzbr.github.io/nixos-full-source-bootstrap/thesis.pdf&quot;&gt;full source
bootstrapped&lt;/a&gt;.
However, multiple packages are not properly &amp;quot;packaged&amp;quot; and vendors
dependencies and binary seeds.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; 10.5. Channel authentication
&amp;gt;
&amp;gt; Guix channels support cryptographic authentication out of the box
&amp;gt; (Contributors, 2025l). Each channel specifies an &amp;quot;introduction&amp;quot; — a
&amp;gt; specific commit and its Ed25519 signature — and Guix verifies the
&amp;gt; full chain of signatures from that introduction to the current
&amp;gt; commit. Flakes use HTTPS and GitHub's infrastructure for trust,
&amp;gt; which is a different and arguably less rigorous security model.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, guix has commit authentication, nix doesn't. It's not necessary
Ed25519, but arbitrary GPG keys, probably also SSH keys support will
appear in the future.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Flakes are not perfect, they are stuck in &amp;quot;alpha&amp;quot; and unlikely to
leave it, there is a further work and alternatives to flakes like
&lt;a href=&quot;https://nixtamal.toast.al/&quot;&gt;Nixtamal&lt;/a&gt;.  Still, Flakes showcased what
is possible.  They did a dependency pinning, input rewriting, hermetic
evaluation and standardization. They made Nix UX/DX much saner.&lt;/p&gt;
&lt;p&gt;Guix is not here, it's around &amp;quot;nix-pin times&amp;quot; at the moment.  It
requires a lot of fundamental work to get overlays and multiple
revisions of the same channel.  The good thing, that we don't need to
repeat all the steps of the Nix and can learn from their experience
and do better.  But we have to be clear about current state and
acknowledge our weaknesses.&lt;/p&gt;
&lt;h2&gt;Support&lt;/h2&gt;
&lt;p&gt;I've spent a half of my post-surgery vacation/recovery day on this
write up. So if you find it useful, interesting or entertaining, don't
hesitate to &lt;a href=&quot;https://trop.in/support&quot;&gt;support me or my projects&lt;/a&gt;.&lt;/p&gt;
</content></entry><entry><title>A Sane Directory Structure for Software Projects</title><id>https://trop.in/blog/a-sane-directory-structure-for-software-projects.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2026-03-15T07:46:00Z</updated><link href="https://trop.in/blog/a-sane-directory-structure-for-software-projects.html" rel="alternate" /><content type="html">&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I always spend too much time setting up a new project and thinking how
to structure it.  I decided to summuraize my experience, to enhance it
with a small research and to write down my thoughts on the topic. So I
can come back to it myself or reference in the discussion.&lt;/p&gt;
&lt;p&gt;Also, it can help me to unify my and maybe neighbouring projects
structure, so it's easier to navigate and work on them.  In the rest
of the post I use plural pronounce, because I think this post can end
up as a documentation or a wiki page.&lt;/p&gt;
&lt;p&gt;You probably already experienced incoviniences related &amp;quot;naturally
grown&amp;quot; project structure (especially in someone's repo you need to
work on for some reason).  When there is no clear separation between
different types of source code: auxiliary, dev, build, primary, tests,
environment setup, all mixed up.  Randomly firing side effects,
warnings, etc, just because the wrong file ended up on the load path.
Mental model and reasoning are also suffering.&lt;/p&gt;
&lt;p&gt;In this article we discuss a one particular directory structure that
scales with project complexity and the rationale behind.  The ideas
apply whether you are setting up a fresh project or refactoring an
existing one.&lt;/p&gt;
&lt;p&gt;We expect that a code base may contain multiple programming languages.
For the demonstration and examples the primary language is Guile
Scheme, but most of the ideas are similar and translate easily to
other languages.&lt;/p&gt;
&lt;p&gt;We also assume that the source code is stored in text files. Yeah,
there are languages, where the code stored on other medias (e.g.
IPLD/IPFS, &lt;a href=&quot;https://www.unison-lang.org/&quot;&gt;databases&lt;/a&gt;) and it is a
really cool thing, but in here we focus on old plain files :'( BTW,
Scheme standard doesn't specify, where and how Scheme code must be
stored, it's implementation specific detail, so it's a cool promising
area for RnD.&lt;/p&gt;
&lt;p&gt;The approach we propose should work in the most cases. If it doesn't
work, &lt;a href=&quot;https://lists.sr.ht/~abcdw/rde-discuss&quot;&gt;we&lt;/a&gt; would be curious to
hear about your use case.&lt;/p&gt;
&lt;p&gt;By the end of the reading you should understand why paths like these
are so long and how that length pays for itself and make the life
easier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src/scheme-common/markdown/parser/nodes.scm&lt;/li&gt;
&lt;li&gt;tests/guile/markdown/parser/core-test.scm&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We explain it in three steps: first, how we organize and name modules,
then why an intermediate &amp;quot;language&amp;quot; directory is useful, and finally
why grouping everything under &lt;code&gt;src/&lt;/code&gt;, &lt;code&gt;tests/&lt;/code&gt;, and friends makes so
much sense.&lt;/p&gt;
&lt;h2&gt;Modules&lt;/h2&gt;
&lt;p&gt;This whole section prepares a foundation for implementing a good
directory structure.  It's more about coding practices, naming files,
modules and recapping the concepts like load paths.&lt;/p&gt;
&lt;h3&gt;Module-Path Correspondence&lt;/h3&gt;
&lt;p&gt;In many languages (Java, Clojure, Guile Scheme, etc), there is a
mechanism called load paths or search paths.  In such cases, module
name should correspond to file path or vice versa.  This allows the
language to find and load modules by name whenever they are required,
without knowing the absolute path to the file in advance.&lt;/p&gt;
&lt;p&gt;The idea is simple: if you have module &lt;code&gt;(my-project markdown parser)&lt;/code&gt;
it should be located in &lt;code&gt;parser.scm&lt;/code&gt; file, in &lt;code&gt;./my-project/markdown/&lt;/code&gt;
directory relative to the root of a load path.  When this module is
imported in the program code, the language look through all the load
path directories and when it finds &lt;code&gt;./my-project/markdown/parser.scm&lt;/code&gt;,
it will try to load a module from it.&lt;/p&gt;
&lt;p&gt;We will talk about &lt;a href=&quot;how-to-set-up-guile-load-path&quot;&gt;how to set up load
paths&lt;/a&gt; in more detail in the section
about grouping directories, for now just keep in mind that load paths
are a list of directories where the compiler or interpreter looks for
code.&lt;/p&gt;
&lt;p&gt;The reason this convention matters so much is predictability.
Compiler knows where to find a module.  An IDE, a new contributor, or
a person passing by randomly can easily guess where to look for a
particular module.&lt;/p&gt;
&lt;h3&gt;Namespace (Module) Naming Rules&lt;/h3&gt;
&lt;p&gt;In the first subsection we mentioned that file path SHOULD correspond
to module name, but not MUST.  It was intentional.  In Guile Scheme
you can have a module defined in any file, moreover you can have a
module defined in multiple files or have a file without module
definition at all.&lt;/p&gt;
&lt;p&gt;While all this possible, we recommend not to do so, unless it's really
needed for some reason and keep module-path correspondence from
previous section.&lt;/p&gt;
&lt;p&gt;Now, the question, where &lt;code&gt;(json)&lt;/code&gt; module comes from?  You own sources,
guile-json library, or somewhere else?  Quite unclear.  For
&lt;code&gt;(my-project ffi json)&lt;/code&gt; it's much clearer that it very likely a json
bindings for C library implemented by &lt;code&gt;my-project&lt;/code&gt;.  Naming module
with at least 3 elements, is a good idea for multiple reasons:
clarity, minimal chance of name clashes and not too long at the same
time.&lt;/p&gt;
&lt;p&gt;First element is usually a project or domain name.  In java world they
use reverse domain name notation (&lt;code&gt;org.apache.kafka&lt;/code&gt;,
&lt;code&gt;org.apache.spark&lt;/code&gt;), it easy to sort it, but we find it hard to read
for humans. So we suggest to use either project name &lt;code&gt;(spark ...)&lt;/code&gt; or
a domain &lt;code&gt;(kafka-apache-org ffi json)&lt;/code&gt; or &lt;code&gt;(kafka.apache.org ffi json)&lt;/code&gt; if your language supports dots in module elements.&lt;/p&gt;
&lt;p&gt;Second element is usually up to you, group it whatever way it make
sense for the project.  We won't give any recommendations here.&lt;/p&gt;
&lt;p&gt;Third one is usually straightforward.  The question you can probably
face is: has it to be singular or plural?  The short answer is:
singular.  Usually module represents some concept/domain, not the
entities of the domain themselves.  In rare case module is a
collection or registry of items (like package definitions), in such
cases it may make sense.  If you in doubt: use singular.&lt;/p&gt;
&lt;p&gt;We figured out the naming, now one more things to address left, a
small anti-pattern in the source code inside modules.&lt;/p&gt;
&lt;h3&gt;Never Top-level Sidefectful Expressions&lt;/h3&gt;
&lt;p&gt;Back in the days, it was usual to use files as shell scripts, you
probably saw shebangs like &lt;code&gt;#!/usr/bin/python&lt;/code&gt; or &lt;code&gt;#!/bin/env guile&lt;/code&gt;
at the beginning of the file or even a direct invocation of the file
like &lt;code&gt;guile code.scm&lt;/code&gt;.  While it makes it easy to run code from CLI
and could be a handy trick for one shot throw away utility, it has a
few serious downsides.&lt;/p&gt;
&lt;p&gt;Imagine, your colleague writes &lt;code&gt;serializer-test.scm&lt;/code&gt; and executes it like
&lt;code&gt;guile serializer-test.scm&lt;/code&gt;. It creates some stub markdown files, prints
progress to stdout, and saves the test results to &lt;code&gt;tests.log&lt;/code&gt; file.
Now, they put this file somewhere along the rest of the project's
source code. So now anybody can benifit from project having tests.
Sounds good, right?&lt;/p&gt;
&lt;p&gt;Now imagine, how surprised you will be, when after pulling fresh
sources, you compile the project and get a lot of &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.log&lt;/code&gt;
files thrown around source code tree out of nowhere (or something even
worse than that).  It happens, because of two reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;serializer-test.scm&lt;/code&gt; file contain top-level expressions with &lt;a href=&quot;https://en.wikipedia.org/wiki/Side_effect_(computer_science)&quot;&gt;side
effects&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The file is located on the load paths of the compiler and thus gets
loaded automatically, when compiler is invoked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Auto-firing side effects is why we don't put side-effectful forms in
top-level in our source files (and we recommend you to do the
same). Moreover, the files in the load path can be loaded in arbitrary
order and potentially multiple times, so it's a good idea to expose
only constants and function definitions in them.  In case somebody
wants to fire a particular side effect or call a function, they can do
explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;guile -L ./our/load/path -c '((@ (my-project markdown parser) dirty-parse) &amp;quot;./path/to/test.md&amp;quot;)'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, you probably noticed that the second point we mentioned that the
code for testing got mixed into the load paths of the library itself,
which can cause other potential problems. For example library source
code can accidentially import some helper functions defined in tests
modules and those functions can be potentially unsafe (as they
supposed to be executed only during development/testing phases and
wasn't audited for security carefully).  We will explain how to solve
it in Top-Level Directory Structure Section, and now let's talk about
multiple languages and subprojects.&lt;/p&gt;
&lt;h2&gt;Language Subdirectories (Not Really)&lt;/h2&gt;
&lt;p&gt;Don't skip this section even for mono-language projects.&lt;/p&gt;
&lt;p&gt;In modern world, it's very likely that you can't stay in the
boundaries of one language. Let's imagine we are implementing markdown
parser using tree-sitter C bindings, and we use &lt;a href=&quot;https://spritely.institute/hoot/&quot;&gt;Guile
Hoot&lt;/a&gt; (Scheme on WebAssembly) for a
web frontend renderer and pre-viewer.&lt;/p&gt;
&lt;p&gt;It's already 3 languages with different compilers, module machinery
(or lack of it), and other things. It would be good to store the
source code for them in different subdirectories to reduce the mess
and to make it clear which tool uses which source code directories and
where to look for the sources of a particular part of the system.  So
we can split the primary source code of the project into a few units:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;guile&lt;/code&gt; :: for guile scheme source code, (e.g. parsers, web server).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hoot&lt;/code&gt; :: for hoot, a scheme dialect compiled to wasm (e.g. web frontend).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scheme-common&lt;/code&gt; :: records, data models used by both frontend and backend.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt; :: native binding, high-performance implementation, etc.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python&lt;/code&gt; :: legacy python markdown parser for benchmarking against.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can already see that &lt;code&gt;scheme-common&lt;/code&gt; is not exactly tied to a
particular language, it's more of a logical grouping.  Language can be
a grouping criteria, but not necessary.  It just happened to be the
first one comming to our minds.&lt;/p&gt;
&lt;p&gt;We could have &lt;code&gt;backend&lt;/code&gt;, &lt;code&gt;frontend&lt;/code&gt; and &lt;code&gt;shared&lt;/code&gt; or &lt;code&gt;c-parser&lt;/code&gt;,
&lt;code&gt;scm-parser&lt;/code&gt;, &lt;code&gt;scm-veiwer&lt;/code&gt; and &lt;code&gt;scm-common&lt;/code&gt;.  Name it the way it make
sense for the project, but please keep this intermediate level of
directories even if it's only one at the moment.  You never know when
you will need to introduce a new language or split the existing system
in a few subsystems.&lt;/p&gt;
&lt;p&gt;Now, if we followed this convention, the navigation becomes easy.  For
native bindings we look for &lt;code&gt;tree-sitter-helpers.c&lt;/code&gt; in &lt;code&gt;c/&lt;/code&gt; directory.
If we look for a frontend code, it's in &lt;code&gt;hoot&lt;/code&gt; and some shared between
frontend and backend is in &lt;code&gt;scheme-common&lt;/code&gt;.  Also, constructing load
paths or pointing compiler to the sources becomes simplier:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;GUILE_LOAD_PATH=&amp;quot;./guile:./scheme-common&amp;quot; guile -c '((@ (my-project web) server))'
HOOT_LOAD_PATH=&amp;quot;./hoot:./scheme-common&amp;quot; guile -c '((@ (my-project hoot) generate-wasm-binary))'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We know that there is no unintended backend code leaked into frontend
build.  Also, we know that our primary source doesn't rely on any
shady util function from a test module, but we know it due to the idea
from the next section :)&lt;/p&gt;
&lt;h2&gt;Top-level Project Directories&lt;/h2&gt;
&lt;p&gt;How we ensure that no test or dev code end up in the release? We put
them in separate directories(!), so it's hard to unintentionally mix
them up.  There are 4 top-level source code directories we propose.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;env/&lt;/code&gt; :: code for setting up dependencies and development enviroments.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/&lt;/code&gt; :: the primary source code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests/&lt;/code&gt; :: tests, I guess.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dev/&lt;/code&gt; :: drafts and snippets useful for development, but not going
to the final build.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even in a small personal project you will very likely need all of
them.  You need to setup the dev environment (libraries, compilers,
etc) and it's better to persist in, at least as &lt;code&gt;env/setup.sh&lt;/code&gt;, but
better something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;env/guix/my-project/channels.scm&lt;/code&gt; :: guix channels for exact guix
revision and repositories of packages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;env/dev/my-project/packages.scm&lt;/code&gt; :: package definitions and
collections used for development/testing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;env/release/my-project/packages.scm&lt;/code&gt; :: packages need for release,
to make sure no dev/test/debugging functions leak into final build.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see that we follow a grouping pattern similiar to the one
described in the previous section.  It's a good balance between
verbosness and clarity.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;src/&lt;/code&gt; we do the same: apply the intermediate grouping level from
the previous section.  The primary source splits into subsystems or
languages, and each subsystem lives in its own subdirectory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/guile/&lt;/code&gt; :: Guile Scheme backend code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/hoot/&lt;/code&gt; :: Hoot frontend compiled to WebAssembly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/scheme-common/&lt;/code&gt; :: shared data models and records.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/c/&lt;/code&gt; :: native bindings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Constructing release load paths is now simple and explicit:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;GUILE_LOAD_PATH=&amp;quot;./src/guile:./src/scheme-common&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the same grouping to &lt;code&gt;tests/&lt;/code&gt;, mirroring the structure of
&lt;code&gt;src/&lt;/code&gt;.  A test for &lt;code&gt;src/guile/my-project/markdown/parser.scm&lt;/code&gt; lives
at &lt;code&gt;tests/guile/my-project/markdown/parser-test.scm&lt;/code&gt;.  The mapping is
mechanical: swap the top-level directory and append &lt;code&gt;-test&lt;/code&gt; to the
filename.  Finding the test for any module takes zero thinking and is
easy to implement on IDE side.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;dev/&lt;/code&gt; directory is for anything useful during development that has
no place in the final build: REPL session snippets, profiling scripts,
one-off data migration helpers, experimental ideas.  It follows the
same grouping convention:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dev/guile/my-project/drafts/bench.scm&lt;/code&gt; :: benchmarks.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dev/guile/my-project/drafts/scratch.scm&lt;/code&gt; :: throwaway experiments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final dev load paths will look like:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;GUILE_LOAD_PATH=&amp;quot;./dev/guile:./tests/guile:./src/guile:./src/scheme-common&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beyond the four source code directories, there are three more, you
will likely need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doc/&lt;/code&gt; :: architecture notes, design decisions, onboarding guides.
Not inline code comments, but the higher-level texts that explain
&lt;em&gt;why&lt;/em&gt; the system is the way it is.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target/&lt;/code&gt; :: build artifacts and generated output.  Never committed,
always in &lt;code&gt;.gitignore&lt;/code&gt; (or similiar).  Having a dedicated name
avoids the &lt;code&gt;build/&lt;/code&gt;, &lt;code&gt;out/&lt;/code&gt;, &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;_build/&lt;/code&gt; lottery.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tmp/&lt;/code&gt; :: throwaway files that don't deserve a place even in &lt;code&gt;dev/&lt;/code&gt;.
Also gitignored.  Better to have an explicit place than to let the
junk and temporary files accumulate in the primary part of the repo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Putting it all together, the full project tree looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;my-project/
├── dev/
│   └── guile/
│       └── my-project/
│           └── markdown/
│               └── bench.scm
├── doc/
│   └── architecture.md
├── env/
│   ├── dev/
│   │   └── my-project/
│   │       └── packages.scm
│   ├── guix/
│   │   └── my-project/
│   │       └── channels.scm
│   └── release/
│       └── my-project/
│           └── packages.scm
├── src/
│   ├── c/
│   │   └── tree-sitter-helpers.c
│   ├── guile/
│   │   └── my-project/
│   │       └── markdown/
│   │           └── parser.scm
│   ├── hoot/
│   │   └── my-project/
│   │       └── markdown/
│   │           └── viewer.scm
│   └── scheme-common/
│       └── my-project/
│           └── markdown/
│               └── node.scm
├── target/
├── tests/
│   └── guile/
│       └── my-project/
│           └── markdown/
│               └── parser-test.scm
└── tmp/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Slightly verbose, but clear, predictable and scales well.  We are
pretty happy with it, so we hope you will be happy as well.  Or maybe
not, anyway, do NOT &lt;a href=&quot;/contact&quot;&gt;contact&lt;/a&gt; Andrew, he &lt;a href=&quot;/does&quot;&gt;works&lt;/a&gt; on
fileless and directoryless, content-addressable future for our
civilization.&lt;/p&gt;
</content></entry><entry><title>How to Set Up Guile Load Path</title><id>https://trop.in/blog/how-to-set-up-guile-load-path.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2025-10-30T12:00:00Z</updated><link href="https://trop.in/blog/how-to-set-up-guile-load-path.html" rel="alternate" /><content type="html">&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Guile Load Path is a place (or more precisely places), where Guile
looks for the source code.  This is the first thing one needs to set
correctly to work on a Guile Scheme project.  It makes Guile aware
both of your own modules and external dependencies.  For more details
refer to &lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Load-Paths.html&quot;&gt;Load
Paths&lt;/a&gt;
page in Guile Reference Manual. This guide will focus on getting and
setting the right values for it and discussing different approaches to
do so.&lt;/p&gt;
&lt;h2&gt;Getting the Load Paths&lt;/h2&gt;
&lt;p&gt;The current value of all load paths for the current guile process is
stored in &lt;code&gt;%load-path&lt;/code&gt; variable, so it's easy to obtain it in runtime
using CLI, REPL or &lt;a href=&quot;https://git.sr.ht/~abcdw/guile-ares-rs&quot;&gt;your IDE&lt;/a&gt;.
To see what value it has you can just pretty print the variable.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; guile -c '((@ (ice-9 pretty-print) pretty-print) %load-path)'

(&amp;quot;/home/bob/.guix-home/profile/share/guile/site/3.0&amp;quot;
 &amp;quot;/run/current-system/profile/share/guile/site/3.0&amp;quot;
 &amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default with a clean Guile installation you should see only the
directory which contains Guile's own source code. In more real-world
scenarios, for example, in Guix System, I see two combined directories
containing my system's and home's guile packages respectively, the
directory with guile's sources and a few bogus entries, which I have
truncated with ellipsis (&lt;code&gt;…&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; echo $GUILE_LOAD_PATH
/home/bob/.guix-home/profile/share/guile/site/3.0:/run/current-system/profile/share/guile/site/3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first two directories comes from the environment variable
&lt;code&gt;$GUILE_LOAD_PATH&lt;/code&gt;.  The rest are baked into guile distribution.&lt;/p&gt;
&lt;h2&gt;Getting a Clean Environment&lt;/h2&gt;
&lt;p&gt;To get a clean environment you can unset &lt;code&gt;$GUILE_LOAD_PATH&lt;/code&gt; variable
(to ensure there are no dependencies added to the project
accidentally). You can also construct a pure environment with &lt;code&gt;guix shell --pure guile&lt;/code&gt;, which has similiar effect, but for the sake of
clarity we will unset the variable manually.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; unset GUILE_LOAD_PATH
&amp;gt; guile -c '((@ (ice-9 format) format) #t &amp;quot;~y&amp;quot; %load-path)'
(&amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The guile's libraries directory is still present in load paths. It is
hardcoded in the guile binary and not affected by &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Setting &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Now, let's assume there is a project module &lt;code&gt;(myproj core)&lt;/code&gt; in
&lt;code&gt;./tmp/guile/myproj/core.scm'&lt;/code&gt; (usually it is &lt;code&gt;src&lt;/code&gt; instead of &lt;code&gt;tmp&lt;/code&gt;,
but we provide examples, which are easy to clean up by invoking &lt;code&gt;rm ./tmp -r&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; mkdir -p tmp/guile/myproj \
echo &amp;quot;(define-module (myproj core))&amp;quot; &amp;gt; tmp/guile/myproj/core.scm \
echo &amp;quot;(define-public (main) (display 'hi))&amp;quot; &amp;gt;&amp;gt; tmp/guile/myproj/core.scm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trying to run &lt;code&gt;guile -c '(begin (use-modules (myproj core)) (main))'&lt;/code&gt;
leads to &lt;code&gt;no code for module (myproj core)&lt;/code&gt; because this module is not
on the load path yet. To fix this, you have to add &lt;code&gt;tmp/guile&lt;/code&gt; to the
load path. Pay attention that we add &lt;code&gt;tmp/guile&lt;/code&gt; and not &lt;code&gt;tmp&lt;/code&gt; or
&lt;code&gt;tmp/guile/proj&lt;/code&gt;. The module &lt;code&gt;(myproj core)&lt;/code&gt; have to be in
&lt;code&gt;myproj/core.scm&lt;/code&gt; file, which must be located in the root of a load
path, if this convention violated the module won't be found.  Let's
temporary set &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt; for the next invocation of guile.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; GUILE_LOAD_PATH=&amp;quot;./tmp/guile&amp;quot; \
guile -c '(begin (use-modules (myproj core)) (main))'
hi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Voila, we got a message printed by the main function.&lt;/p&gt;
&lt;h2&gt;The Order Matters&lt;/h2&gt;
&lt;p&gt;It's possible to have a few directories by separating them with colon.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; GUILE_LOAD_PATH=&amp;quot;./test/guile:./tmp/guile&amp;quot; \
guile -c '((@ (ice-9 pretty-print) pretty-print) %load-path)'
(&amp;quot;./test/guile&amp;quot;
 &amp;quot;./tmp/guile&amp;quot;
 &amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The order matters: if there are two modules with the same name on the
load paths, the first one encountered will be used.&lt;/p&gt;
&lt;p&gt;Also, there is a special three dots value (&lt;code&gt;...&lt;/code&gt;) you can use when
setting &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;.  It will get expanded to the path of
guile's own libraries.  This is useful if you want to make sure your
library doesn't override any built-ins.  Keep in mind that this three
dots is distinct from the ellipsis I have added when truncating the
output of the &lt;code&gt;guile&lt;/code&gt; command for this blog post.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; GUILE_LOAD_PATH=&amp;quot;./tmp/guile:...:./my-srfis&amp;quot; \
guile -c '((@ (ice-9 pretty-print) pretty-print) %load-path)'
(&amp;quot;./tmp/guile&amp;quot;
 &amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …
 &amp;quot;./my-srfis&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Adding to &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes the &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt; is already provided and set and you
want to extend its value.  It doesn't really matter if it comes from
&lt;code&gt;guix shell&lt;/code&gt;, set by the script of your colleague or whatever.  For
demonstration purposes, we will set it manually in our shell.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; export GUILE_LOAD_PATH=&amp;quot;./path/set/by/someone-else&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Almost always, you want to prepend the directories you need to the
existing value.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; GUILE_LOAD_PATH=&amp;quot;./dev/guile:./test/guile:${GUILE_LOAD_PATH}&amp;quot; \
guile -c '((@ (ice-9 pretty-print) pretty-print) %load-path)'
(&amp;quot;./dev/guile&amp;quot;
 &amp;quot;./test/guile&amp;quot;
 &amp;quot;./path/set/by/someone-else&amp;quot;
 &amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pay attention that we set a variable only for the next invocation of
guile.  We do not mutate the variable value, and if we run this code a
few times we won't see the duplicated values in the load paths.&lt;/p&gt;
&lt;p&gt;To append to the existing &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt; or order the directories
anyhow else, just move &lt;code&gt;${GUILE_LOAD_PATH}&lt;/code&gt; accordingly.&lt;/p&gt;
&lt;p&gt;In addition to &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;, there are at least two more ways to
adjust load paths: via CLI and API.  It's good to be aware of where
else load path values can come from, especially when you work on
someone else's project.&lt;/p&gt;
&lt;h2&gt;Modifying via CLI&lt;/h2&gt;
&lt;p&gt;For most cases &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt; is enough and should be used, but
sometimes it may be tricky or inconvenient to set environment
variables. The &lt;code&gt;guile&lt;/code&gt; command line program has the &lt;code&gt;-L&lt;/code&gt; flag, which
allows for prepending a directory to the load paths.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; guile -L ./dev/guile -L ./test/guile -c '((@ (ice-9 pretty-print) pretty-print) %load-path)'
(&amp;quot;./dev/guile&amp;quot;
 &amp;quot;./test/guile&amp;quot;
 &amp;quot;./path/set/by/someone-else&amp;quot;
 &amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a complete equivalent to the example from the previous
section.  The order of added directories still matters.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;-L&lt;/code&gt; Has Priority Over &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;An important thing to know is that &lt;code&gt;-L&lt;/code&gt; directories have &lt;strong&gt;higher
priority&lt;/strong&gt; than &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt; directories.  &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt; is
processed before command-line arguments, and then &lt;code&gt;-L&lt;/code&gt; prepends its
directories to the front of &lt;code&gt;%load-path&lt;/code&gt;.  This means &lt;code&gt;-L&lt;/code&gt; entries
always come first.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; GUILE_LOAD_PATH=./env1:./env2 \
guile -L ./flag1 -L ./flag2 \
-c '((@ (ice-9 pretty-print) pretty-print) %load-path)'
(&amp;quot;./flag1&amp;quot;
 &amp;quot;./flag2&amp;quot;
 &amp;quot;./env1&amp;quot;
 &amp;quot;./env2&amp;quot;
 &amp;quot;/gnu/store/37m0a0ydy74wl2qrf2w1jdgqhxwbaxac-guile-3.0.9/share/guile/3.0&amp;quot;
 …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The effective order in &lt;code&gt;%load-path&lt;/code&gt; is following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Directories added via &lt;code&gt;-L&lt;/code&gt; (leftmost &lt;code&gt;-L&lt;/code&gt; argument has highest
priority).&lt;/li&gt;
&lt;li&gt;Directories from &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Guile's built-in default paths.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is useful when you need to override a module provided by the
environment for a specific invocation without changing
&lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Modifying via API&lt;/h2&gt;
&lt;p&gt;So far, we have used &lt;code&gt;%load-path&lt;/code&gt; to get the value of load paths, but
we can also use it to set load paths with &lt;code&gt;set!&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We do not recommend setting load paths dynamically (in runtime) via
API.  All load paths should be known before the guile process is
started and thus they should be set via &lt;code&gt;GUILE_LOAD_PATH&lt;/code&gt;, but if you
are really sure you want to, there are a couple of things to know.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;%load-path&lt;/code&gt; must be modified before the expression is evaluated.
That means if you need to set load paths in the same expression you
want to use the new value it should be wrapped into &lt;code&gt;(eval-when (expand))&lt;/code&gt;.  See
&lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Eval-When.html&quot;&gt;Eval-when&lt;/a&gt;
for more details.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;&amp;gt; guile -c '(begin (eval-when (expand) (set! %load-path (list &amp;quot;tmp/guile&amp;quot;)))
(use-modules (myproj core)) (main))'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don't forget to preserve the necessary values of the &lt;code&gt;%load-path&lt;/code&gt;,
otherwise things can break, such as goto definition, loading modules,
etc.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;(set! %load-path (cons &amp;quot;new/dir&amp;quot; %load-path))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a convenience wrapper macro, which does both of those things:
&lt;code&gt;add-to-load-path&lt;/code&gt;, but we hope you will never need any info from this
section.&lt;/p&gt;
&lt;h2&gt;Clean Up&lt;/h2&gt;
&lt;p&gt;To wrap up all the things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do &lt;code&gt;rm -r ./tmp&lt;/code&gt; to clean up all the tails.&lt;/li&gt;
&lt;li&gt;Thank &lt;a href=&quot;/contacts&quot;&gt;authors&lt;/a&gt; of this guide.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Acknowledgments&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Edited by &lt;a href=&quot;https://breatheoutbreathe.in/&quot;&gt;Joseph Turner&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content></entry><entry><title>I Have to Live in a Forest to Work on Open Source</title><id>https://trop.in/blog/i-have-to-live-in-a-forest-to-work-on-open-source.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2025-10-19T10:00:00Z</updated><link href="https://trop.in/blog/i-have-to-live-in-a-forest-to-work-on-open-source.html" rel="alternate" /><content type="html">&lt;p&gt;This fictional story begins more than 10 years ago. I was a student at
technical university and was confused by how outdated some of the
programming-related courses are. I was checking out a few first
lections and usually skipping the rest of them (except a couple
courses that were fun and uptodate). In my spare time I was tinkering
on gentoo linux, cybersecurity and competitive programming
(codeforces, ACM ICPC, etc). I wanted to start working ASAP, so that I
could finally get to the most interesting part, but man, how wrong I
was...&lt;/p&gt;
&lt;p&gt;This write up is inspired by my university friend, who made a
&lt;a href=&quot;https://youtu.be/mE1sk1FKxMA&quot;&gt;film&lt;/a&gt; about my time living in a tent in
a turkish forest and working on my FOSS projects.&lt;/p&gt;
&lt;h2&gt;The Corporate Work&lt;/h2&gt;
&lt;p&gt;To impress one girl, in third year of University, I passed an
interview and got a job offer in the hugest internet corporation in
the country. I did a bit of Python/webdev and in a few months and a
couple of internal interviews I switched to SRE role for search engine
and services around.  This was my first disenchantment in technologies
and processes around.  Everything was a mess and barely
maintainable. Three implementations of string type with different
memory allocation approaches, monorepo of hundred gigabytes, tons of
services glued together and only the god knows how they keep working.&lt;/p&gt;
&lt;p&gt;But the worst part is not a mess or lack of well-defined processes,
the worst part is a feeling of helplessnes, a feeling that your have a
0 impact, you just spend months of your life to keep this stuff
floating. 1 year into corporate work and I have a clear understanding
that health insurance, extremely comfy office with massage, yoga,
language clubs, cookies or watever you can imagine, a salary times
higher than the average in your country doesn't matter if you feel
miserable.  Moreover, I was afraid to get into the trap of comfort and
to start stagnating together with the corporation and its messy
codebase :) P.S. The girl wasn't particularly impressed by those
achievements as well.&lt;/p&gt;
&lt;h2&gt;New University and Startups&lt;/h2&gt;
&lt;p&gt;Luckily, around this time I got a chance to switch the university.
This time it was fun.  It was a completely new city built for this
particular Uni, professors were from all over the world, program was
made in collaboration with
&lt;a href=&quot;https://en.wikipedia.org/wiki/Carnegie_Mellon_University&quot;&gt;CMU&lt;/a&gt;,
almost all communication was in English.  It was tons of fun.  I had a
good background and amount of hard skills to keep up with studies
relatively easy and I took the opportunity to acquire soft skills and
business skills as much as I could.&lt;/p&gt;
&lt;p&gt;In addition to techncical communication, enterpreneurship and other
courses, we were building students community and organizations,
creating sport clubs, changing the university itstelf, participating
in hackathons, and spinning up startups.  The amount of energy and
effort was impressive.  We've built a streaming app, before periscope
became a thing, created a delivery service for our city before taxi
apps appeared.  And this is only a couple of projects I personally was
in a charge of. A lot more things were happening around.  It was
exciting time.&lt;/p&gt;
&lt;p&gt;I didn't forget about Computer Science and cool technologies and was
learning Lisp, FP and Clojure in parallel.  So mindblowing, so
interesting.  I desired to apply it in a real world.  Maybe rewrite
the delivery platform?  A few days after the meeting, where we
discussed a potential refactoring, my uni friend came to me and asked:
Would you like to help my two friends from UK and France to build a
platform for managing commercial buildings? We can use watever tech
stack you want.&lt;/p&gt;
&lt;p&gt;Despite being a cool experience, the delivery wasn't a profitable
project, it was more like a fun pet project made by students for city
citizens.  It was already at the end of its lifecycle, so getting into
a more scalable international adventure was tempting.  We had a calls
with the guys and started to build the platform from scratch in a
completely new and unusual tech stack.&lt;/p&gt;
&lt;h2&gt;The Dark Times&lt;/h2&gt;
&lt;p&gt;Everything started bright and fun, we were building and delivering
relatively fast, techonologies were awesome and all that, but there
were a few catches. 1. I wasn't a founder of the project and didn't
have a enough &amp;quot;business vote power&amp;quot;. 2. My soft and leading skills
were much better than a few years ago, but still very suboptimal. 3. I
already had enough fun with business parts in delivery project and was
focused almost solely on technologies and software development in this
one.&lt;/p&gt;
&lt;p&gt;I was designing architecture, CI pipelines, containirized
infrastructure (before kubernetes was a thing), workflows, I was doing
docs, refactorings, scrums-agiles, onboardings, task tracker
configurations, etc.  It was a lot of new and important experience,
but at the same time it was a lot of load.  Of course, I couldn't do
everything well.  There are a few conflicts and tension points
appeared in the team and I didn't resolve them properly and
completely.  Moreover, I didn't have enough energy to do so.  I was
tired.  I was exhausted.&lt;/p&gt;
&lt;p&gt;I cared too much about tech and processes, but let the other aspects
of the project slip, including communication, top-level decision
making power and meaningfulness.  At the point, where I overworked,
stressed and lacking power and leverage, it was extremely hard to
change anything.  I couldn't just keep working, because I was almost
physically vomiting when open the project in the text editor.  I
decided to leave.&lt;/p&gt;
&lt;p&gt;Cause of my random contribution to some open source Clojure project,
just a couple weeks after I left the project, I got an invite for an
interview. Did I mention that for my whole life I had a desire to make
a free and open source software? It looked right to me, it felt
important and impactful.  Some of it were inspired by hacker culture
of 2000s and my youth idealism, but some of it became a part of me and
became a kind of an internal belief.&lt;/p&gt;
&lt;p&gt;However, it was never enough time for it. Everyone around were telling
you can do it next sprint/month/whatever, let's finish this feature
first, or we can't do so, otherwise our competitors would be able to
steal our tech, so I was only casually contributing to FOSS, when I
could, but it was usually minor.  Even with those minors
contributions, somehow I got this invite.&lt;/p&gt;
&lt;h2&gt;The Alps&lt;/h2&gt;
&lt;p&gt;The interview was by the one of the best Clojure teams in my country
and I passed it relatively easy and also got x5 salary without any
negotiation.  Yes, I cared only about technologies on my previous
project and it wasn't too hard to make an x5, but it was still quite
high salary for the market at the moment.  This time I didn't have any
expectations.  I just wanted to save money and buy a free time to
recover from my previous overworking experience.&lt;/p&gt;
&lt;p&gt;Plan was simple: Work for a year, save some money, get at least a
couple years to travel, work with psychologist, play the games, ride
the bicycle and feel happy.&lt;/p&gt;
&lt;p&gt;I moved to the cultural capital (most beatiful and historically rich
cities), spend a few cool, fulfiling and interesting days at my
ex-coworker's place (fellow hacker and researcher, one of the
important figures in tor project), while looking for an appartement
for rent. When I found the apparts near the office I moved in and
started my new job.&lt;/p&gt;
&lt;p&gt;The work was relatively boring, I myself was tired, but I was
following the plan: I was saving around 90% of the salary and did my
dids and duties. I also had some activities outside of the work:
riding the snowboard, learning acrobatics, visiting mountains from
Siberia to Alps, hanging out with friends, visiting some iconic
historical places, traveling for hackathons and conferences around the
Europe.&lt;/p&gt;
&lt;p&gt;Despite the fun I got from sports and social life, I still felt
exhausted, I didn't want to wake up, it was hard to get up from the
bed, it was not much reason to do so, it was a deep apathy.  Luckily,
around this time I got allocated to lead a new project, a EMR for
hospices in US.  This turned out to be an incredible experience.&lt;/p&gt;
&lt;p&gt;It started, when we with my friend were hanging out in Austria, in
cozy chalet on the side of the mountain. We got tickets for airplane
for 80$ (the price for both ways, we got it half a year in advance on
a random sale), and rented a room for 10EUR/night(?)  We had to share
the bed and we were cooking ourselves to keep expenses low, but it was
incredibly amazing place.  Not usual &amp;quot;amazing&amp;quot;, but really-really
amazing.  The weather, the snow, the views.  We were snowboarding the
powder, glashiers and forests. We were comming back home in the
evening and eating meals we made near a crackling fireplace.&lt;/p&gt;
&lt;p&gt;In one of such beautiful evenings of my vacations I got a work call,
the call about this potential new project for hospices. I met two
stackeholders and we get to know each other a bit. It was pleasant,
they were nice and seemed smart, their expectations were unrealistic,
but they were very cooperative and understanding, so I got a feeling
that it can be a fun project.&lt;/p&gt;
&lt;h2&gt;Grown-Up Start-Up&lt;/h2&gt;
&lt;p&gt;After I came back from vacations I started to work with those two guys
from US and building a PoC of the system.  We were discussing
requirements, did remote user testings and assesments.  I onboarded a
couple more devs in the team and we started to build stuff even
faster.  It was very pleasant to work with those men, I was genuinely
happy to interact with them, but I wasn't too much excited about the
project, I wasn't too much excited about life in general at the
moment.  It was just another commercial EMR.  I didn't feel any
meaning in it, I only felt that I'm underemployed, I make some CRUDs
and web pages, when I spent years learning quite involved Computer
Science and Math.&lt;/p&gt;
&lt;p&gt;It was already almost a half a year into the project, but we still
didn't have any real users.  Moreover, I was afraid that the system
we've built was based on our discussion and extrapolations mostly,
rather than on real use-cases.  I suggested that I come to US, go
around the hospice and we interview doctors and nurses, visit their
planning meeting and all that things.&lt;/p&gt;
&lt;p&gt;It turned out to be a great experience, together with my lovely
stackholders we collected a lot of data and insights and adjusted the
current implementation quite radically to fit the real needs.  Besides
the work I had a lot of new experiences (&lt;a href=&quot;https://youtu.be/tAXST-wqW34&quot;&gt;video log
1&lt;/a&gt;, &lt;a href=&quot;https://youtu.be/tAXST-wqW34&quot;&gt;vlog
2&lt;/a&gt;): visiting a lot of places, shooting
guns, dating a wonderful girl, spending a time on the farm, going to
hot springs, learning Spanish from Mexican workers, watching american
football games IRL, doing beautiful hikes.&lt;/p&gt;
&lt;p&gt;What is more important I've built a long-term friendship with Troy and
Robb. After I spent time in hospice, the project became more
meaningful, I started to feel the real need for it, I also had a lot
of impact on the project.  It didn't feel completely right because of
proprietary nature, but other than that I was very happy about it.
The humans involved in it were top-notch, interactions with them were
a pure pleasure.&lt;/p&gt;
&lt;h2&gt;Burning and Saving&lt;/h2&gt;
&lt;p&gt;We were getting closer to one year point mark, still not yet deployed
on our first hospice (owned by one of the four stackeholders).  I work
as hard as possible and we get all base functionality ready, but it
has the price: I got even more exhausted. Guys are very happy with
what we achieved and asking me if I want to become a stackeholder.
Rationally speaking, it's a great opportunity: wonderful humans, very
reliable and scalable business, fancy tech stack, but personally I
don't feel like it's the right way.&lt;/p&gt;
&lt;p&gt;It's a proprietary software and it bothers me.  It's a CRUD web app
and feels much less than I'm capable of. And last, but not least, I
still exhausted, maybe even a bit more in the last a few months, so
I'm afraid that I would unintentionally start sabotaging the work we
do if I keep working on the project.&lt;/p&gt;
&lt;p&gt;At this point, I had enough money for 10 years of a quite minimalistic
life.  I have an apartment at home town, my expenses are low, I spend
around 100$ for food and 100$ for sports a month and do occasional
budget-friendly trips around the world.  We made the first production
release, onboarded a few people (nurses and doctors) and got our first
billing done IIRC. And... I deciding to leave the project, follow my
original plan and finally get some rest.&lt;/p&gt;
&lt;h2&gt;Vacations and the Start of Open Source Journey&lt;/h2&gt;
&lt;p&gt;I allocated two month to do nothing. I spent a couple of weeks laying
on the bed and walking around.  After that I started to play some
video games, wandering around the town and going for casual street
workouts with my friend.  It was intentional, I was learning how not
to blame myself for not being productive, I was learning how to care
of myself, my physical and emotional health.  I started to feel, I
stopped to hurry.  After those two month I built a bit of useful
boredom, which made me continue to tinker on Nix, reproducible dev
environments and all those things.&lt;/p&gt;
&lt;p&gt;I couldn't work much at the beginning. Maybe 15 or 30 minutes a day
before I got exhausted again, some days I couldn't work at all,
sometimes I couldn't even speak.  Slowly but steadily, I got my
curiousity and courage back.  I started to make videos and streams on
the topics I learn and explore, I started to build my small FOSS pet
projects.  Not immediately, bu I got back some of my ability to work
and to live.  I found power to get a few sessions with psychologist
and it also helped to feel better or at least something (:&lt;/p&gt;
&lt;p&gt;In half a year I got to the level, where I could work a few hours
straight (not every day, but quite often).  I already had a few minor
FOSS projects and I switched from Nix to Guix (because I wanted a
general purpose language instead of DSL and liked lisps ATM).  It
turned out that there is no Home Manager for Guix, however, it wasn't
a big deal, I had a confidence that I can make it myself and I did
it. I built it in a few months and later upsteamed it to Guix as a
Guix Home subsystem.&lt;/p&gt;
&lt;p&gt;This was a year into my Open Source journey, but I already gained a
lot of my productivity, curiosity and fullfilment back. I made a few
FOSS project like &lt;a href=&quot;/rde&quot;&gt;RDE&lt;/a&gt;, Guix Home. I got a lot of positive
emails and feedback.  I became much more lively and happy.  I wouldn't
say I was completely happy at this moment, but I was a half a way into
it.&lt;/p&gt;
&lt;h2&gt;The War&lt;/h2&gt;
&lt;p&gt;I definitely was on a right track and everything were getting together
and I was getting better with every day.  I was tinkering, having fun,
getting the meaning, happinnes and liveness.  I felt like taking a few
years off and working full time on Open Source was a great
decision. The year went quickly, I did a lot of contributions to mine
and others FOSS projects, I broke my leg on a wakeboard, got into
climbing and kayking and planned two trips for the winter.&lt;/p&gt;
&lt;p&gt;I've spent New Year week in Turkey in the mountains climbing with
folks I met a couple of months before in a climbing gym. Two months
later I had a snowboarding trip with my university friends in Siberia.
One morning I woke up 5 am and saw my half-sleeping friend watching
some crappy message by president on TV in the other room.  I was like:
what the heck are you doing, bro? He replied: it seems like the we
(goverment of our country) started a war.  I was: No way, you are
messing with me.&lt;/p&gt;
&lt;p&gt;We couldn't accept this fact for a few days, we couldn't belive that
it can happen in the 21th century, but it actually happened.  We
continued to ride the powder and tried to enjoy our time, but were
constantly scrolling the news in disbelief.&lt;/p&gt;
&lt;p&gt;A few month forward, I got a new pasport for travels instead of
expiring one, got some other documents ready, packed a backpack, took
my mom's car and went to the Georgia for unknown amount of time.  I
had only a couple thousands of dollars in cash and a couple more I
transfered to my friend three months before. My savings on the bank
and investment accounts were frozen, so instead of peacful 10 years of
minimalistic life I just got into unknowns without a job, a home and
running out of budget quickly.&lt;/p&gt;
&lt;p&gt;The awfulness of war, opressive regimes and all that are important
topics, but we won't talk about them today, let's focus on how I ended
up in the forest writing an open source software.&lt;/p&gt;
&lt;h2&gt;Running Out of Money&lt;/h2&gt;
&lt;p&gt;I've been to Georgia (Sakartvelo) in 2019 and was a bit familiar with
this beatiful hospitable country.  This time it was even more
enjoyable experience.  So much new and interesting things, a lot of
infrastructure improvements since the last time.  The weather is so
wondeful, that it was hard to be stereotypically grumpy as I was the
whole life.  Almost every day is sunny and nice.  I was enjoying every
moment here.&lt;/p&gt;
&lt;p&gt;The bureauchracy is very manageable, I got a sim card, bank account
and legal entity in a couple of weeks.  After the most of the
paperwork was settled, I came back to my FOSS projects, spending tons
of time with them.  The only issue at the moment was money, they were
evaporating fast.  With two my good friends we were renting a room at
guesthouse.  It costed around 200$/month/person, but it was too tight
for 3 people.  A little bit later we found an apartment with 3
bedrooms and it became 500$/month/person (yeah, this is quite
expensive for Georgia, but a lot of people came here cause of the war
and the demand was too high at the moment).  It was sparse and comfy,
I finally could function properly and work efficiently.&lt;/p&gt;
&lt;p&gt;A half of the time I was coding, half of the time I was researching
and trying to build a sustainable financial model for the projects and
a half of the time doing side projects to replenish the treasury.  It
turned out to be quite a hard task to raise funding for FOSS work,
without getting into VC money. And VC money will likely screw up your
project.  At least from what I see from other open source projects
nearby.&lt;/p&gt;
&lt;h2&gt;Donations and Trip to Turkey&lt;/h2&gt;
&lt;p&gt;Other option was &lt;a href=&quot;/support&quot;&gt;donations&lt;/a&gt;, I made an &lt;a href=&quot;https://opencollective.com/rde&quot;&gt;opencollective
page&lt;/a&gt; and it is quite successful for
the size of the project, we get around 2-3k EUR/year.  However, it's
not enough to pay the bills (at least at the moment) even for one
person, not talking about other contributors.  So, I decided not to
rely on them and keep it as a backup for the harsh time or some
project-related activities (later we organaized an &lt;a href=&quot;https://lists.sr.ht/~abcdw/rde-announce/%3C87ecx0trh6.fsf@xn--no-cja.eu%3E&quot;&gt;internship for
RDE&lt;/a&gt;
from this funds).&lt;/p&gt;
&lt;p&gt;I started to look for consulting contracts, so I can apply the stuff I
develop to real world projects and also get paid for it.  I knew from
the beginning that at some point of time I'll need to get money for my
work somehow. That's why when I started my Open Source Journey a
couple years ago I also started to make &lt;a href=&quot;https://youtube.com/@abcdw&quot;&gt;videos and
streams&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thanks to those videos I landed my first guix-related contract.  I
went to the Turkey for one month, built a custom Guix-based operating
system for PinePhone, teached a small dev team about Guix and Emacs
and had a very pleasant time with very hospitable guys.  It also gave
me enough money to cover the next few months of my life.&lt;/p&gt;
&lt;p&gt;By the end of the spring our rent finished and we with my friends
moved apart. I found a room in a coliving for 300$/month and was keep
looking on how to stay afloat.&lt;/p&gt;
&lt;h2&gt;UAE to Save Money&lt;/h2&gt;
&lt;p&gt;I was invited to teach a lisp course in &lt;a href=&quot;https://lalambda.school/&quot;&gt;Lalambda
2023&lt;/a&gt;, a summer school on advanced
programming and contemporary art.  It's a not for profit activity, but
I like teaching, so I committed to it and spent a few weeks preparing
materials and exercises for students.  At the same time we were
chatting with my mid-school-times friend and he invited me to stay at
his place in Emirates and to hang out. It was a nice opportunity to
spend time with my friend and also save some money. I took tickets to
Abu-Dhabi for the next day after summer school finishes.&lt;/p&gt;
&lt;p&gt;Lalambda was a fantastic experience, the classes went great, I got a
lot of positive feedback, I met many cool people and a very nice girl
with PhD. And on this positive note I left Georgia.&lt;/p&gt;
&lt;p&gt;Next three month I spent in UAE.  It was hot, 46-48 degrees celsius
outside, so most of the time I spent either at home working or at
climbing gym training, and of course sometimes hanging out with my
school friend and his friends.&lt;/p&gt;
&lt;p&gt;Very cool and productive times, I implemented a lot of features in my
current projects, made a few releases, started a &lt;a href=&quot;/does&quot;&gt;new FOSS
project&lt;/a&gt; (an IDE for Guile Scheme) and started preparation for
upcoming conferences.&lt;/p&gt;
&lt;p&gt;However, my UAE visa was expiring and I had to find another country to
stay.  Finances didn't get better, so it had to be very inexpensive
country.  I decided to go head first into the Turkey as most cost
efficient and familiar option I knew at the moment.  I've been here a
couple of times already: wakeboarding in 2020, rockclimbing in winter
2021-2022 and consulting in 2023. But this time was different, I had a
backpack, 900$ and 200EUR in cash and no foreseable source of funding.&lt;/p&gt;
&lt;h2&gt;Hiding in the Forest&lt;/h2&gt;
&lt;p&gt;Fast forward a few intermediate stops, I landed in Antalya and was
looking for the bus to Geyikbairi (a pine forest valley surrounded by
mountains, a disneyland for rock climbers).  I've been here in 2021,
but last time my friend picked up me from airport and delivered
straight to the bungalow, this time I was on my own.  I missed the
bus, but somehow managed to get to the valley.  I had a tent booked in
one of the campings for 8EUR/night, it included access to shower,
kitchen and common indoor space.&lt;/p&gt;
&lt;p&gt;There was a cat (a few of them) in the kitchen and common space,
causing a severe allergy.  So I was walking around the valley and
looking for another place to stay for a few days and found another
camping, namely camp Geyik.  In the meantime I recorded &lt;a href=&quot;https://emacsconf.org/2023/talks/scheme/&quot;&gt;my talk about
Scheme IDE&lt;/a&gt; for EmacsConf
2023, so people can see what cool stuff I'm working on.&lt;/p&gt;
&lt;p&gt;After I finished with conference talk, I went by bus to the city and
found a two person (actually 1.5 person) poked and fixed tent in
Decathlon for ~100EUR and a blanket for 5 EUR.  Came back and
negotiated a price (around 800EUR for 4 month) for access to common
area, kitchen and shower.  Found a cozy spot in the woods, pitched a
tent and started to work even harder on my FOSS projects.&lt;/p&gt;
&lt;h2&gt;Surviving&lt;/h2&gt;
&lt;p&gt;I had around 100$ left, the visa allowed to stay for 3 month in a half
a year (with mandatory visa run after first 2 months), the winter was
comming, the rains and the winds were getting stronger. That was only
the beginning.&lt;/p&gt;
</content></entry><entry><title>I Crashed on the Road Bike at 50km/h, and I ...</title><id>https://trop.in/blog/i-crashed-on-the-road-bike-at-50km-h-and-i.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2025-08-31T06:00:00Z</updated><link href="https://trop.in/blog/i-crashed-on-the-road-bike-at-50km-h-and-i.html" rel="alternate" /><content type="html">&lt;p&gt;I liked the feeling afterwards. Don't get me wrong, I don't like to be
hurt or being in pain and luckily I didn't get any serious
injuries. There is just a thought that stroke me after a fall: I'm
finally safe and healthy. A very long-awaited and desired thought, and
it's not even related to the cycling. You probably have a WTF moment
reading this, but let me explain.&lt;/p&gt;
&lt;h2&gt;Struggles, Ilness, Anexiety&lt;/h2&gt;
&lt;p&gt;It was two extremely tough months for me.  Everything started with the
wrong appartment pick: we rented one that looked new, pretty and
comfy.  At first everything was ok, we moved in, bought some houshold
bits and pieces, and everything seemed nice and settled.&lt;/p&gt;
&lt;p&gt;Somewhere around this time I started to work on &lt;a href=&quot;https://arxiv.org/abs/2508.02176&quot;&gt;my first scientific
paper&lt;/a&gt;. I didn't have supervisor, I
didn't have much time before submission deadline and much previous
experience either, so I was not expecting it to be easy for sure.
Also, I was very motivated to write and submit the paper as it were a
few coincidences that made this conf a very good match for me:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I had a couple of projects, which are very much ontopic for this
conf.&lt;/li&gt;
&lt;li&gt;I got an email from one of conference organaizers inviting me to
send a paper to it.  This is a funny one, I thought I didn't know him,
but later I realized that a couple of weeks before I downloaded one of
his talks to watch later.&lt;/li&gt;
&lt;li&gt;The conference was in accessible location (Singapore): it's nearby
and most importantly getting a visa to it for me is easy.  You
probably can't imagine what bureaucracy acrobatics I had to pull off
to be able to recieve payments for my FOSS work after I left my
country 3 years ago.  Getting a shengen visa is even more
challenging. Toxic passport is not a joke.&lt;/li&gt;
&lt;li&gt;Recently, I started to learn about Japan, there culture and
language and with every day I enjoy and willing to visit this country
more and more.  There were at least two professors from Japan, so I
could easily make connections and potentially get an invite to teach
Computer Science in Japan.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Obviously, I started to work hard to get it done. It was like a 10-16
hours a day, and in addition to that I cut almost all social and
physical activities (they took too much time and attention).  I was
going out to see people maybe once in a week, had about 2-5 hours of
yoga and a bit of walking from cafe to cafe, the rest was the paper.
I kept my sleeping schedule as usual, from ~22 to sunrise, and added
day naps to reset and be more productive in the second part of the
day.&lt;/p&gt;
&lt;p&gt;It's not a surprise that by sitting so much in front of a laptop, I
developed a wrist and back pain quite quickly.  But what surprised me
that I got ill.  I haven't been any seriously ill for 2 years and here
I got the whole set: fever, cough, phlegm, foggy mind, extreme
tiredness.  I thought it could be the result of overwork and I stepped
back a bit: relaxed my working schedule, got a lot of additional
sleep, drank a lot of water and did more walks and ... It didn't help.&lt;/p&gt;
&lt;p&gt;My condition was varying from manageable to bad.  A week and a half
later I went to the hospital, got X-ray, blood test, bronchitis
diagnosis and antibiotics prescription.  Started to take pills and an
hour later got an extreme abdominal pain, so bad that I couldn't sit
or stand.  I had so sever pain in the stomach area only once in my
life, in the childhood, from allergy to wild strawberry.  A few days
later I noticed a rash on legs and arms.  Turned out I'm allergic to
this antibiotic.&lt;/p&gt;
&lt;p&gt;I still finished the prescribed course and it seems I got better.
Meantime, my friend &lt;a href=&quot;https://shilin.ca&quot;&gt;Shilin&lt;/a&gt; (athletic and healthy
guy, who I rent apartment with) got exactly the same respiratory
sympthoms (maybe just a little bit less intense). Ok, maybe it's a
local vietnamese virus or bacteria we don't have immune response
against?  The hypothesis is sound.&lt;/p&gt;
&lt;p&gt;A few days later, I had an early morning date, but around 4am I had to
write that I won't come cause... I'm ill.  Again.  The same way as
before.  It's already became very annoying and stressful.  It's hard
to work, it's hard to see people, it's hard to exist.  At this point
of time, I assumed that it can be environment/air quality problem.&lt;/p&gt;
&lt;p&gt;It turned out that apartment has a horrible (non-existing) ventilation
and also I started to suspect that it can have mold, but I didn't have
any way to quantify the problem.  I tried to find an agency, who can
do air quality tests and measure, but found only one organization,
which unfortunatelly operates only in the other city. The research
brough air quality monitors to my attention and I order one of them
from Japan (it still on the way).  In the meantime I also ordered
hygrometer, air purifier, pulseoximeter and N95 masks.&lt;/p&gt;
&lt;p&gt;A few weeks fast forward.  I tried to temporary move to hotel and it
made me feel a bit better, but the sleep quality was far from perfect
and it was hard to tell if it really helps or I just got better
overall at the moment.  I came back to the apartment. Sleeping in the
mask and using air purifier subjectively eased the sympthoms a bit,
but hard to tell how much.&lt;/p&gt;
&lt;p&gt;I finally finished my work on the paper, submitted it, and was very
keen to troubleshoot this extremely annoying and dangerous problem.  I
went to the hospital for the third time (this time the other hospital,
the fanciest in Da Nang).  The X-ray didn't show nor positive
dynamics, neither negative.  Still bronchitis.  I got corticosteroids
and a third type of antibiotics prescribed in the span of less than
two month!  Shillin was also ill for almost the whole time of the
story and also took prescribed to him antibiotic without much
improvement.&lt;/p&gt;
&lt;p&gt;A day later, I noticed that we have a round patch of the mold appeared
in the shower.  I asked an apartment manager to help us with mold
issues and... they just painted the mold with white paint 🤦.  We
didn't get any other collaboration on this problem.  My good and
caring friend noticed my post on &lt;a href=&quot;/contact&quot;&gt;fediverse&lt;/a&gt; about air
quality, and dropped an email advising to move out ASAP.&lt;/p&gt;
&lt;p&gt;I also noticed a few dark spots appeared on the wall in my room. And
it stroke me: they probably painted the black mold in my room and it
just grows trough the paint!  I wanted to deconstruct the wall to
check my guess, but at first I started to explore every possible
corner of the room and found white and green mold in the toppest and
almost inaccessible shelves of the wardrobe.  There were signs that it
was swept into the corner (probably by cleaner), but it continued to
grow of course.&lt;/p&gt;
&lt;p&gt;This was the last bit.  It was not only a hypothesis, it was a fact
that there are huge mold problems in addition to ventilation and
humidity issues.  At this moment, I was already very anxious and
stressed.  I couldn't properly sleep, I couldn't work.  In addition to
that I got a reject for my conference paper.  I tried to comfort
myself with food and cozy kind movies.  I tried to distract myself
with intense workouts: boxing, kettlebells, yoga, swimming, cycling.
As you can guess, it didn't help much: I still was anxious, but also
extremely tired at the same time 😄&lt;/p&gt;
&lt;p&gt;I realized that I'm to anxious, I can't fall asleep, I can't do a
thing for my current projects. I spent two days going through all
possible apartments for rent nearby, found a manageable one, gave up
on deposit and moved out the same day.  The next day I found an
apartment for my friend and helped him to move out as well.&lt;/p&gt;
&lt;p&gt;Even after the move I felt extreme anexiety, every small inconvinience
triggered me, I was worried about any tiny failure or interaction I
had. I was afraid that I could be wrong about the cause of my health
problems and it won't get better, I was afraid that my bus for visarun
will be cancelled, I was worried that I annoyed and gave troubles to a
bartender by asking to redo matcha without sugar.&lt;/p&gt;
&lt;p&gt;Before visarun I got only 2 hours of sleep and woke up at 2:20am to
get to the car at 3.  The visarun itself went smoothly, I came back
around afternoon.  Had a walk and dinner with my vietnamese friend and
fall asleep around 20:00, and got 10+ hours of uninterrupted sleep!
The next day I was feeling good and rested, confirmed the next morning
ride with local cycling shop owner, and had a relatively calm and good
day and manageable amount of good sleep.  This is where the story
about the crash starts! :)&lt;/p&gt;
&lt;h2&gt;The Crash&lt;/h2&gt;
&lt;p&gt;I woke at 5:15, brushed my teeth and at 5:50 was at the bike shop.  It
was my 3rd ride in the last years, and it was intermediate-advanced
level ride with average tempo about 20km/h and quite steep climbing.
I was invited to it as I showed enough level of fitness on the
previous casual ride.  I got a very basic rental road bike and we went
for a run.&lt;/p&gt;
&lt;p&gt;The run was not easy, on the flat surface it was hard to keep up with
the group, on climbes I was getting around 190 pulse, but still could
clearly talk and even oversustain some of the fellows.  The
athmosphere was very friendly and nice.  Everyone was smiling, chill
and having fun and good gigs. Somewhere around 20km mark we turned
back, and on the way back we started to decend the steepest climb.  We
were notified by the lead of the group to be careful.&lt;/p&gt;
&lt;p&gt;The first few guys with fancy carbon bikes went 80km/h or maybe more
and quickly dissapeared from the sight.  As for myself I went fast
(for me), but controlled, around 50km/h.  Another advanced guy in
tight shorts and on the the fancy bike bypassed me with ease, but I
didn't try to keep up, I instead started to check my breaks and
preparing for the turn.&lt;/p&gt;
&lt;p&gt;A couple of seconds later, I was watching this guy losing control and
going head first into the stream near the road.  My first thought was:
if he got unconsiousness, he can drawn!  I need to stop quickly and
help him out.  I pressed breaks.  Probably pressed them too hard.  At
the same time I've reached the wet asphalt section.  I'm not used to
ride narrow tires and didn't expect how they will interact with the
surface.  You can guess what happened next.&lt;/p&gt;
&lt;p&gt;Instead of gradually slowing down, my bike started to wiggle like a
crazy.  For a few second instead of riding I was sweeping with the
tail of my bike like a newbie snowboarder in their first days learning
edging.  It could last forever and at some point I lost the control
and flew away.&lt;/p&gt;
&lt;p&gt;It's funny, but I didn't feel any worried before the crash. I was
certain that everything is under control and I can easily stop at any
moment.  Even when the bike started to wiggle I was more surprised
than stressed.  I don't even get any noticeable adrenaline kick.&lt;/p&gt;
&lt;p&gt;The crash itself was quite comfortable: I hit the ground and slided a
bit on my back, I was in a helmet, but I didn't touch the ground with
my head.  Probably the wet asphalt helped to slide and avoid serious
skin damage.  I started to feel the parts of the body I hit straight
after the crash and it wasn't any bad.  The next day I could feel
brusies and soar neck muscles.  It was hard to do mobility workout
cause of soar bun, but the next day it was good.&lt;/p&gt;
&lt;p&gt;I already had 2 yoga and kettlebell workouts and it's fine. The only
worrying part is that a part of the elbow bruise has a sharp pain on
press.  I guess I need to do an X-ray to check there is no bone
fracture.  BTW, the guy went head first into the stream had even less
bruises and scratches, but much more mud on him :)&lt;/p&gt;
&lt;p&gt;I hope you enjoyed the details of my fun morning on the monkey
mountain, but the best part came later. We ensured that everyone is
ok, and hot fixed our bikes and I realized that I'm finally healthy
and safe, I'm in a good and supporting community, I don't have cough,
I don't have anexiety.  I feel so much better now.&lt;/p&gt;
&lt;p&gt;Stay away from fungies, ride more, find great people, have a piece 🤟&lt;/p&gt;
</content></entry><entry><title>Actually Useful Stack Traces</title><id>https://trop.in/blog/actually-useful-stack-traces.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2025-06-20T12:00:00Z</updated><link href="https://trop.in/blog/actually-useful-stack-traces.html" rel="alternate" /><content type="html">&lt;p&gt;Stack traces, backtraces—whatever you call them, I never liked
them. Every time I saw one, the only thought in my mind was: &amp;quot;And
now what?&amp;quot; Then I discovered a particular implementation, and I felt
like a kid in a candy store. I guess I know what made it so useful for
me, and thus, so exciting. We will take a look at a visual demo and
talk about how one can build an interactive debugger on top of it;
but first, we need to discuss three issues.&lt;/p&gt;
&lt;p&gt;The problem is simple: the usability of stack traces sucks.  Let's take
a look at primary suck points, and we will quickly understand how to
avoid them and how to get a good UX:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stack trace is a text&lt;/li&gt;
&lt;li&gt;It's either convoluted or lacking&lt;/li&gt;
&lt;li&gt;It's upside down&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why is it upside down?  That's a very good question at the very right
time.  As you can see, the narrative of this post goes from top to
bottom and most recent things appear … at the bottom.  Your focus
naturally follows it.  The same happens, when you read output of the
program.  Let's take a look at this:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;/home/bob/tmp/demo.js:10
  notDefined();
  ^

ReferenceError: notDefined is not defined
    at thirdFunction (/home/bob/tmp/demo.js:10:3)
    at secondFunction (/home/bob/tmp/demo.js:6:3)
    at firstFunction (/home/bob/tmp/demo.js:2:3)
    at Object.&amp;lt;anonymous&amp;gt; (/home/bob/tmp/demo.js:13:1)
    at Module._compile (node:internal/modules/cjs/loader:1565:14)
    at Object..js (node:internal/modules/cjs/loader:1708:10)
    at Module.load (node:internal/modules/cjs/loader:1318:32)
    at Function._load (node:internal/modules/cjs/loader:1128:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:219:24)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recency and relevance of the information drops rapidly with every
new line.  Which is kind of the opposite of the reading flow direction
we were discussing a couple of sentences ago.  Surprisingly, most of
the mainstream languages (except maybe Python) do this.  From
usability standpoint it doesn't make much sense: when program finished
printing the trace, I have to scroll up to get to the most interesting
stack frames.  In this tiny example it may not seem that critical, but
in real world scenarios stack traces can become beefy very quickly, so
it will become a pain in the back.  And last but not least: this
order of frames is important for making a debugger, but we will cover
it closer to the end of the post.&lt;/p&gt;
&lt;p&gt;Are there any cases, when it makes sense to print it upside down?
Yes.  Will I tell you about them?  &lt;a href=&quot;/contact&quot;&gt;No&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Okay, we covered the printing direction, now let's talk about the
content.  To represent the state of the stack we need to communicate a
lot of information: function names, values of arguments, frame
numbers, local variables, corresponding source code location.
Presenting it to the developer in a meaningful way is an impossible
challenge: it will be always a tradeoff between comprehensibility and
readability.&lt;/p&gt;
&lt;p&gt;Yes, there is no universal solution here: you either provide too much
and it becomes noisy incomprehensible mess or not enough and dear
stack trace reader can't do a thing with it.  And here we come to a
very important point: I want to do a thing with it and don't want to
be flooded with wall of text at the same time.&lt;/p&gt;
&lt;p&gt;Let's take a look at almost good stack trace and explore this exact
problem.  One small note here, for function calls it uses a slightly
special notation &lt;code&gt;(function-name arg1 arg2)&lt;/code&gt; instead of more usual
&lt;code&gt;function_name(arg1, arg2)&lt;/code&gt;, be aware.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;In ice-9/boot-9.scm:
  1755:12 11 (with-exception-handler _ _ #:unwind? _ # _)
In unknown file:
          10 (apply-smob/0 #&amp;lt;thunk 7f46684f4300&amp;gt;)
In ice-9/boot-9.scm:
    724:2  9 (call-with-prompt _ _ #&amp;lt;procedure default-prompt-handle…&amp;gt;)
In ice-9/eval.scm:
    619:8  8 (_ #(#(#&amp;lt;directory (guile-user) 7f46684f7c80&amp;gt;)))
In ice-9/command-line.scm:
   185:19  7 (_ #&amp;lt;input: custom-port 7f46684f1850&amp;gt;)
In unknown file:
           6 (eval (begin (use-modules (ares suitbl)) (#)) #&amp;lt;directo…&amp;gt;)
In ares/suitbl.scm:
    499:6  5 (test-runner _)
   264:31  4 (_ ((type . print-test-suite) (test-suite #&amp;lt;proced…&amp;gt; …)))
In ice-9/boot-9.scm:
   260:13  3 (for-each #&amp;lt;procedure 7f465be50c60 at ares/suitbl.scm:…&amp;gt; …)
In ares/suitbl.scm:
   325:13  2 (test-reporter-hierarchy ((type . print-test-suite) (…)))
    291:2  1 (tests-&amp;gt;pretty-string #f)
In ice-9/boot-9.scm:
    218:9  0 (map #&amp;lt;procedure 7f465bbab160 at ares/suitbl.scm:292:3…&amp;gt; …)

ice-9/boot-9.scm:218:9: In procedure map:
In procedure map: Not a list: #f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First of all, it's printed from the beginning to the end, the most
recent and relevant calls are at the bottom of the output, just above
the problem message, and this is already a huge improvement over the
initial example.  The ordering will matter even more for debugger:
when you jump from break point to break point the first part of the
stack remains the same and only the tail changes and this order allows
to easily redraw the tail.&lt;/p&gt;
&lt;p&gt;Second of all, I can see arguments to the function. Here &lt;code&gt;#f&lt;/code&gt; means
&lt;code&gt;False&lt;/code&gt;, &lt;code&gt;#&amp;lt;procedure ...&amp;gt;&lt;/code&gt; is an object of type &lt;code&gt;procedure&lt;/code&gt; and
&lt;code&gt;((type . print-test-suite) (test-suite #&amp;lt;proced…&amp;gt; …))&lt;/code&gt; is a list of
two pairs: &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;print-test-suite&lt;/code&gt; and &lt;code&gt;test-suite&lt;/code&gt; to the list
of procedures (you probably noticed omitted dot and missing
parenthesis, yeah).  Not the most convenient notation, but at least
the arguments are here.&lt;/p&gt;
&lt;p&gt;If you stare at this stack trace long enough, you can even understand
(to some degree) what was happening before we got into troubles.&lt;/p&gt;
&lt;p&gt;BTW, do you find it a bit noisy?  Those file names, line and column
numbers — visual clutter I don't care about.  Yes, I would like to
jump to the source code and look around, but we can do so without
clogging the output.  Let's go back for a moment and try to debug our
problem.&lt;/p&gt;
&lt;p&gt;From the exception message I can guess that we passed &lt;code&gt;#f&lt;/code&gt; instead of
a list to the &lt;code&gt;map&lt;/code&gt; function.  And here is the first issue: we are
lucky to have useful exception message, but we don't see arguments
passed to the &lt;code&gt;map&lt;/code&gt; in the stack trace, because they were truncated to
fit the terminal width.&lt;/p&gt;
&lt;p&gt;We still can guess that the root cause is somewhere earlier because
&lt;code&gt;tests-&amp;gt;pretty-string&lt;/code&gt; also got #f as an argument.  The problem may be
in the &lt;code&gt;test-reporter-hierarchy&lt;/code&gt; function, and again, we can't see
arguments passed to it, moreover, we don't see local variables.  Thus
it's unclear why &lt;code&gt;tests-&amp;gt;pretty-string&lt;/code&gt; got called with &lt;code&gt;#f&lt;/code&gt;, so we
are basically stuck and can't proceed our investigation using only the
stack trace.&lt;/p&gt;
&lt;p&gt;We try to make stack trace useful — it gets overcrowded, we try to
make it readable — it becomes lacking.  Are we doomed and can't do
anything about it?  No.  The primary problem is that we try to fit
everything into the text representation, but computers are much more
than just text printing machines.&lt;/p&gt;
&lt;p&gt;I will skip the part where we discuss obvious solution: to make stack
trace ultimately informative and create a special tool, which parses
it and provides a rich interactive GUI/TUI.  Instead, we can go
straight and store a structured data about stack frames in a format
more suitable than the plain text and continue the discussion from
here.&lt;/p&gt;
&lt;p&gt;Okay, imagine, we discarded the text-only representation, now let's
make a slick interactive UI.  That means, while we still store all
auxiliary information, we don't need to make it visible to the readers
and flood their brain with it.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;(test-runner ((type . run-scheduled-tests)))
(loop (#&amp;lt;procedure test-reporter-hierarchy (message)&amp;gt;))
(for-each #&amp;lt;procedure 7fd42192d3a0 at ./ares/suitbl.scm:257:14 (r)&amp;gt; _)
(test-reporter-hierarchy ((type . print-test-suite) (test-suite . (simple-test))))
(prettify-list #f)
(map _ #f)

Origin: &amp;quot;map&amp;quot;
Not a list: #f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much cleaner version of the previous stack trace.  The click
on the function name leads to the source code location, where this
function gets called.  I can use keybindings to jump back and forth
between stack frames and now can easily understand the flow of the
code.  We've reduced feedback loop (the time from seeing a stack trace
and finding a place of potential failure) from seconds and minutes to
fractions of a second.  No hassle, no guessing, just pure usability.&lt;/p&gt;
&lt;p&gt;It's not a theoretical reasoning, my very smart and diligent
&lt;a href=&quot;https://xn--no-cja.eu/post/announcing-my-internship-with-rde.html&quot;&gt;intern&lt;/a&gt;
&lt;a href=&quot;https://piaille.fr/@baleine&quot;&gt;Noé&lt;/a&gt; and I already implemented it in
Ares/Arei IDE and this is a video of how it works: &lt;a href=&quot;/videos/actually-useful-stack-traces-in-action&quot;&gt;the
video&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The tool has already saved me hours of my life and I enjoy every
moment I spend with it.  And yes, it has some room for improvement, so
let's quickly go through the things around the corner:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Value Inspector :: instead of using a text representation for highly
nested data structures, we can just put a clickable link that will
bring up a value inspector.&lt;/li&gt;
&lt;li&gt;Local Variables :: when we jump from frame to frame, it would be
cool to see the lexical scope of the function in that stack frame.&lt;/li&gt;
&lt;li&gt;Code Evaluation :: evaluating code in the context of exception is a
sick functionality, almost a miracle, but you won't believe it's
possible anyway.  When we get it implemented, this &amp;quot;just a stack
viewer&amp;quot; will turn into the most powerful debugging machine known to
human race, hehe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://git.sr.ht/~abcdw/guile-ares-rs&quot;&gt;Ares&lt;/a&gt;/&lt;a href=&quot;https://git.sr.ht/~abcdw/emacs-arei/&quot;&gt;Arei&lt;/a&gt;
Guile Scheme IDE is a Free and Open Source Software, so feel free to
play around, study, improve or whatever you want to do with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt; and my recommendation: if you have to make text-only stack
traces, provide as much information as possible and sacrifice
readability: it's easy to filter out the noise, but extremely hard to
get the needed info when data is missing.  If you have access to
computers not only through a teletype console, just provide a nice
interactive UI and make your stack traces readable, actionable and
actually useful.&lt;/p&gt;
</content></entry><entry><title>No JS, No BS Ethical Web Analytics</title><id>https://trop.in/blog/no-js-no-bs-ethical-web-analytics.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2025-05-31T12:00:00Z</updated><link href="https://trop.in/blog/no-js-no-bs-ethical-web-analytics.html" rel="alternate" /><content type="html">&lt;p&gt;I had two goals: to count AI crawlers DDoSing my nginx
&lt;a href=&quot;/guix&quot;&gt;infrastructure&lt;/a&gt; and to see if anybody reads at least one of my
three posts in the &lt;a href=&quot;/blog&quot;&gt;blog&lt;/a&gt;.  To achieve both, I needed to gather
data and transform it into meaningful insights, so basically I needed
web analytics.&lt;/p&gt;
&lt;p&gt;I don't think I need to explain why I wanted an ethical solution.  If
you are here, you likely have your own reasons.  If you follow my
work, you might also have some clues, but of course, you can always
&lt;a href=&quot;/contact&quot;&gt;ask me&lt;/a&gt; for more.&lt;/p&gt;
&lt;p&gt;In addition to ethical reasons, there are at least three more
technical issues with convenient JS-based analytics.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The setup is complicated: deploying a database and backend,
injecting js into every response, maintaining all of that.&lt;/li&gt;
&lt;li&gt;JS won't count people with ad-blockers, NoScript, or RSS, and I bet
most of my readers use at least one of them.&lt;/li&gt;
&lt;li&gt;It won't count crawlers and bots that have limited or absent js
evaluators.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Yes, without JS I won't be able to track the eye movement and body
temperature of the reader.&lt;/p&gt;
&lt;p&gt;The funny part is that I already have a lot of data for analytics in a
web server's &lt;code&gt;access.log&lt;/code&gt;.  It's quite surprising how much useful
information we can extract from it; we just need to provide a cute
representation for the extracted info.&lt;/p&gt;
&lt;p&gt;Luckily, there is a &lt;a href=&quot;https://goaccess.io/&quot;&gt;GoAccess&lt;/a&gt; project, which
does exactly that.  I could stop right here, and this post would already
be useful, but I'll try to save you a few more hours of your life
by covering its rough edges and sharing my tricks and findings.&lt;/p&gt;
&lt;p&gt;A three-sentence introduction to GoAccess: it takes an arbitrary log
file and generates an HTML dashboard with panels having various
beautiful plots and tables (&lt;a href=&quot;https://rt.goaccess.io/&quot;&gt;try demo&lt;/a&gt; or
search for &lt;code&gt;goaccess screenshots&lt;/code&gt;).  You can adjust its behavior with
CLI options and persist them in a &lt;a href=&quot;https://github.com/allinurl/goaccess/blob/master/config/goaccess.conf&quot;&gt;configuration
file&lt;/a&gt;.
The rest is done by &lt;a href=&quot;https://goaccess.io/man#examples&quot;&gt;tweaking&lt;/a&gt;
({grep,awk,sed}-ing) a log file.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;cat ./access.log | goaccess - --config-file ./goaccess.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We already have a lot in the default nginx's log file: timings,
referers, requests, user agents; however, one thing is missing.  I
have multiple domains served by my nginx server, and to distinguish
requests to different hosts I enriched my nginx's log file with
&lt;code&gt;'$host:$server_port '&lt;/code&gt; by setting log_format:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;log_format vcombined '$host:$server_port '
        '$remote_addr $remote_user [$time_local] '
        '&amp;quot;$request&amp;quot; $status $body_bytes_sent '
        '&amp;quot;$http_referer&amp;quot; '
        '&amp;quot;$http_user_agent&amp;quot;';

access_log access.log vcombined;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sample log entry below is from my click on the blog link (I
adjusted indentation to mimic newlines from the configuration above,
but in a real log, it's one line).&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;trop.in:443
171.225.184.136 - [31/May/2025:05:52:48 +0200]
&amp;quot;GET /blog HTTP/1.1&amp;quot; 200 1411
&amp;quot;https://trop.in/blog/modern-writers-block-or-how-to-blog&amp;quot;
&amp;quot;Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's clear that to get to the blog page, I clicked the link in &lt;a href=&quot;/blog/modern-writers-block-or-how-to-blog&quot;&gt;Modern
Writer's
Block&lt;/a&gt; post
and was using &lt;code&gt;trop.in&lt;/code&gt; host and https port.&lt;/p&gt;
&lt;p&gt;Now I can grep the log file by host[s] and select data for domains or
sites I'm interested in. To parse the updated log file format, I added
&lt;code&gt;--log-format=vcombined&lt;/code&gt; to goaccess.  I'll show a complete
configuration at the end.&lt;/p&gt;
&lt;p&gt;Also, I was curious about how many people read a particular page from
&lt;a href=&quot;https://yggdrasil-network.github.io/&quot;&gt;Yggdrasil Network&lt;/a&gt; and how many
from the Clearnet, so I added &lt;code&gt;host:port&lt;/code&gt; into &lt;code&gt;&amp;quot;$request&amp;quot;&lt;/code&gt; to the
beginning of the URI with &lt;code&gt;awk '$7=$1$7' access.log&lt;/code&gt;, so the resulting
log entry looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;trop.in:443
171.225.184.136 - [31/May/2025:05:52:48 +0200]
&amp;quot;GET trop.in:443/blog HTTP/1.1&amp;quot; 200 1411
&amp;quot;https://trop.in/blog/modern-writers-block-or-how-to-blog&amp;quot;
&amp;quot;Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to this modification, I can build a separate report where I
have two distinct entries &lt;code&gt;trop.in:443/blog&lt;/code&gt; and &lt;code&gt;ygg.trop.in:80/blog&lt;/code&gt;
instead of one &lt;code&gt;/blog&lt;/code&gt;. I don't use this report often, but I satisfied
my curiosity.&lt;/p&gt;
&lt;p&gt;After that, I realized that I rarely need information about all the
hosts at once in the reports, so I decided to &lt;a href=&quot;https://git.sr.ht/~abcdw/trop.in/commit/cfe80b8&quot;&gt;create a separate
log&lt;/a&gt; for each server
context.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;access-in-trop-files.log
access-in-trop-genenetwork.log
access-in-trop-guix-ci.log
access-in-trop.log
access-local.log
access-wildcard.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logs for related hosts like &lt;code&gt;ci.guix.trop.in&lt;/code&gt; and
&lt;code&gt;ci.guix.ygg.trop.in&lt;/code&gt; are grouped in &lt;code&gt;access-in-trop-guix-ci.log&lt;/code&gt;, for
&lt;code&gt;trop.in&lt;/code&gt; and &lt;code&gt;ygg.trop.in&lt;/code&gt; in &lt;code&gt;access-in-trop.log&lt;/code&gt;, and the rest goes
to wildcard.&lt;/p&gt;
&lt;p&gt;Let's talk about the &lt;a href=&quot;https://rt.goaccess.io/#geolocation&quot;&gt;world map
view&lt;/a&gt;.  To understand the
geography of readers and the ISPs of bots, I wanted Geo Location and
ASN panels.  To make them work, you
&lt;a href=&quot;https://goaccess.io/faq#installation&quot;&gt;need&lt;/a&gt; geodatabase files with IP
to location and ASN mappings. I searched the internet for both
&lt;code&gt;GeoLite2-City.mmdb&lt;/code&gt; and &lt;code&gt;GeoLite2-ASN.mmdb&lt;/code&gt; files, downloaded them to
the server and added to the goaccess's
&lt;a href=&quot;https://git.sr.ht/~abcdw/trop.in/tree/bd8c202/src/guile/tropin/machines.scm#L243&quot;&gt;configuration&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The last tweak is somewhat naughty, but I wanted real-time analytics,
and there is a built-in option for it: goaccess can spawn a WebSocket
to constantly update data for the dashboard.  Of course, I don't want
to expose it to the whole internet, so I made it listen only on
localhost.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;--real-time-html --host=localhost --port=17001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I need to expose both generated HTML and WebSocket to my laptop to
conveniently access it.  For this, I made a &lt;a href=&quot;https://git.sr.ht/~abcdw/trop.in/tree/bd8c202/src/guile/tropin/machines.scm#L149&quot;&gt;local server
context&lt;/a&gt;
in nginx config:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;server {
  listen localhost:80;

  access_log &amp;quot;logs/access-local.log&amp;quot; vcombined;
  location /websocket/goaccess/in-trop {
    proxy_pass http://localhost:17001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &amp;quot;upgrade&amp;quot;;
  }
  location / {
    root /srv/nginx/local;
    autoindex on;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and provided an access to it for my laptop on http://localhost:8880
through an &lt;a href=&quot;https://iximiuz.com/en/posts/ssh-tunnels/&quot;&gt;SSH tunnel&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;ssh -N -L localhost:8880:localhost:80 pinky-ygg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole setup, now it's time to run goaccess and enjoy the
view.  Here is a report I use for analytics on my primary site.  The
report focuses on my flesh-and-blood readers, so I excluded &lt;code&gt;Unknown&lt;/code&gt;
and &lt;code&gt;Crawlers&lt;/code&gt; user agents.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;goaccess \
/var/run/nginx/logs/access-in-trop.log --log-format=vcombined \
-o /srv/nginx/local/analytics/trop.in.html \
--real-time-html --port=17001 --host=localhost \
--ws-url=localhost:8880/websocket/goaccess/trop.in \
--geoip-database=GeoLite2-ASN.mmdb --geoip-database=GeoLite2-City.mmdb \
--unknowns-as-crawlers --ignore-crawlers \
--enable-panel=REFERRERS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, I have a bot-fighting report and am still playing with
parameters, but I already know where they come from, at what time, and
what they are looking for. That means, I accomplished both goals: I
counted evil crawlers and nicest hoomans.&lt;/p&gt;
&lt;p&gt;One missing feature is the ability to splice and filter the data in
runtime.  I'd like to have an overview for an adjustable time frame
and to filter out requests by regex interactively.  However, from
goaccess and the web server log, I already get more insights than I
wished for—and much more than I could get from &amp;quot;convenient&amp;quot; web
analytics.&lt;/p&gt;
&lt;p&gt;Have any thoughts or comments?  Publish them on Fediverse and
reference &lt;a href=&quot;/contact&quot;&gt;me&lt;/a&gt; or publish them somewhere else on the
internet and send me a link.&lt;/p&gt;
</content></entry><entry><title>Modern Writer's Block or How to Blog</title><id>https://trop.in/blog/modern-writer-s-block-or-how-to-blog.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2025-05-23T12:00:00Z</updated><link href="https://trop.in/blog/modern-writer-s-block-or-how-to-blog.html" rel="alternate" /><content type="html">&lt;p&gt;Through the course of a couple of decades I've been online, I tried to
run my blog multiple times, used different blogging engines from
blogspot.com to self-hosted SSGs (Jekyll, Hugo, etc) and failed
miserably in all attempts except two. For the last 7.5 years I
published around 1000 small-to-medium-sized good-quality original
posts, and this is a recap of what I learned from this journey and how
I would do it in the future.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The further the publish button is, the harder it is to … publish.&lt;/em&gt;
It's always tempting for me to tweak CSS, URL scheme, post layout,
RSS/ActivityPub integrations, and that makes me further from writing
and publishing. I've read this idea multiple times in other people's
blogs and always thought that I'm beyond that and it wouldn't stop me
from posting, but I was wrong.&lt;/p&gt;
&lt;p&gt;My most productive blogging setup turned out to be a Telegram channel.
It had subscribe functionality, basic markup, a number of views under
the published post and a publish button - that's it.  No analytics, no
comments, no integrations, nothing.  I could just write down the
thought, maybe highlight a couple words with bold/italic and press the
send button.&lt;/p&gt;
&lt;p&gt;What?  pro-FOSS person blogging on Telegram?  Yes.  Do I have reasons
for it?  Absolutely, and you can &lt;a href=&quot;/contact&quot;&gt;ask&lt;/a&gt; me about them.  Do I
recommend you to do so?  No.  Will I continue to blog on it?  Probably
no.  The most important thing that you can take from it is that: to
become consistent in blogging I needed a good enough publish button
and a place, where people can sanely read what I've published.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Feedback is important; comments are overrated.&lt;/em&gt;  I could get far on
my initial motivation, but if I didn't see people read my stuff I
would stop writing before I make a writing habit.  Thus seeing the
growing number of views is already huge, and getting questions,
constructive critique and kind words is enormous.&lt;/p&gt;
&lt;p&gt;Fun fact: I don't need the comments functionality for it. People can
discuss my posts in other corners of the internet (and sometimes I'm
not even aware of those discussions, which is totally ok). People find
the way to &lt;a href=&quot;/contact&quot;&gt;contact&lt;/a&gt; me directly or send the feedback to my
inbox.  Moreover, implementing a good comment system in the blog is
really hard.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;It's easier to write regularly.&lt;/em&gt;  When I skip writing a post for a
couple of weeks, it's hard to come back to writing the next one,
however, when I publish consistently I always get more ideas than I
can write down.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Better good enough than nothing.&lt;/em&gt; We can learn and grow only when we
have enough practice, and it's inevitable that we make imperfect
things during it. So I came up with a throw-away mindset: start
writing a text that you will discard, then improve this text until it
becomes good enough.  Publish it, profit!  I apply a similar principle
for tasks and projects, which feels hard and daunting and it usually
produces impressively good results.&lt;/p&gt;
&lt;p&gt;As you can tell, I already spent some time
&lt;a href=&quot;https://git.sr.ht/~abcdw/trop.in&quot;&gt;over-engineering&lt;/a&gt; the &lt;a href=&quot;/blog&quot;&gt;blog&lt;/a&gt;
setup, but this time I stopped at a good enough state: I have a decent
&lt;a href=&quot;https://git.sr.ht/~abcdw/trop.in/tree/c4366fd/item/assets/&quot;&gt;CSS&lt;/a&gt;,
RSS/Atom &lt;a href=&quot;/feed&quot;&gt;feed&lt;/a&gt;, minimal analytics and a publish
&amp;quot;&lt;a href=&quot;https://git.sr.ht/~abcdw/trop.in/tree/c4366fd/Makefile#L67&quot;&gt;button&lt;/a&gt;&amp;quot;
nearby.  In addition to that, I have a writing habit and skills under
my belt and better understanding of what is essential and what is
secondary. And of course the most important thing: I published this
post.&lt;/p&gt;
</content></entry><entry><title>Continuations Brief Summary</title><id>https://trop.in/blog/continuations-brief-summary.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2023-06-09T12:00:00Z</updated><link href="https://trop.in/blog/continuations-brief-summary.html" rel="alternate" /><content type="html">&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Continuations are quite flexible and powerful mechanism and can be
used in a good number of different scenarios: &lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Prompt-Primitives.html#index-call_002fec&quot;&gt;early
return&lt;/a&gt;,
proper tail-recursion, generators, &lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Exceptions.html&quot;&gt;exception
handling&lt;/a&gt;,
&lt;a href=&quot;https://github.com/wingo/fibers&quot;&gt;coroutines&lt;/a&gt;, &lt;a href=&quot;https://stackoverflow.com/a/45380365&quot;&gt;multiple return
values&lt;/a&gt;, etc.&lt;/p&gt;
&lt;p&gt;It's a &lt;code&gt;GOTO&lt;/code&gt; in the world of functional programming: they give a
power to control the flow of execution, but feels much more high
level, more involved and require time to understand.&lt;/p&gt;
&lt;p&gt;Continuations were first
&lt;a href=&quot;https://www.cs.ru.nl/~freek/courses/tt-2011/papers/cps/histcont.pdf&quot;&gt;discovered&lt;/a&gt;
in 1964 and later rediscovered a few times in different settings by
different people.  They are first-class citizens and a
&lt;a href=&quot;https://conservatory.scheme.org/schemers/Documents/Standards/R5RS/r5rs.pdf&quot;&gt;part&lt;/a&gt;
of Scheme Language and appears one way or another here and there.  It
make sense to learn this concept to get a better understanding of
Scheme Language and its ecosystem and this article is a brief sum up
on the topic.&lt;/p&gt;
&lt;h2&gt;Continuations&lt;/h2&gt;
&lt;p&gt;An expression's
&lt;a href=&quot;https://courses.cs.washington.edu/courses/cse341/04wi/lectures/15-scheme-continuations.html&quot;&gt;continuation&lt;/a&gt;
is &amp;quot;the computation that will receive the result of that expression&amp;quot;.
Also, it can be perceived as a checkpoint or quick-save, where you can
come back later.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(+ 4 (+ 1 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The continuation of the expression &lt;code&gt;(+ 1 2)&lt;/code&gt; will be something that
adds 4 to the value, like &lt;code&gt;(lambda (x) (+ 4 x))&lt;/code&gt; would do, BUT!
continuation is not a function, it doesn't return a value.  When
resumed (called), it jumps to the place, where it was captured and
continue the execution of the program from that point and never jumps
back.  Let's rewrite the example above, but storing continuation in
the variable.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define kont)
(+ 4 (call/cc (lambda (k) (set! kont k) (k (+ 1 2))))) ; (+ 4 (+ 1 2))

kont ;; =&amp;gt; #&amp;lt;continuation 7f7a1cc269c0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Contiunations implicitly exist everywhere, but to get it we need to
capture it with &lt;code&gt;call/cc&lt;/code&gt; and store it somewhere (with &lt;code&gt;set!&lt;/code&gt; for
example).  The &lt;code&gt;call/cc&lt;/code&gt; is a shorthand for
&lt;code&gt;call-with-current-continuation&lt;/code&gt;.  Now we can use the value of &lt;code&gt;kont&lt;/code&gt;
and resume a continuation in a similiar way as we call one-argument
functions (by passing one argument to it, which will be used in place
of &lt;code&gt;(call/cc ...)&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(kont 6) ;; =&amp;gt; 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, the big difference here is that continuations change the flow
of the execution and doesn't return (as functions do) and thus can't be
composed.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(kont (kont 6)) ;; =&amp;gt; 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;kont&lt;/code&gt; was a function it would return &lt;code&gt;16&lt;/code&gt; in the example above,
but the continuations are NOT functions.  To demonstrate it better,
let's add a few side effects, which print strings.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(begin
 (display &amp;quot;hi\n&amp;quot;)
 (+ 4 (call/cc (lambda (k) (set! kont k) (k (+ 1 2)))))
 (display &amp;quot;hello\n&amp;quot;)
 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After evaluation of this expression &lt;code&gt;hi&lt;/code&gt; and &lt;code&gt;hello&lt;/code&gt; will be printed
and return value of the whole expression will be &lt;code&gt;5&lt;/code&gt;, however when we
resume continuation &lt;code&gt;(kont 10)&lt;/code&gt; only &lt;code&gt;hello&lt;/code&gt; will be printed and
return value will be &lt;code&gt;5&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;While [undelimited] continuations provide a lot of flexibility and
power, the programs written with them are hard to reason about, the
perfomance and memory usage can be
&lt;a href=&quot;https://wiki.c2.com/?ContinuationImplementation&quot;&gt;suboptimal&lt;/a&gt;, the
continuations themselves are not composable, and better, more
generalized alternatives exist.  Read more about potential problems in
&lt;a href=&quot;https://okmij.org/ftp/continuations/against-callcc.html&quot;&gt;An argument against
call/cc&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The simple code snippets above should give a basic understanding of
continuations, but more advanced examples available in &lt;a href=&quot;https://en.wikipedia.org/wiki/Continuation&quot;&gt;wikipedia
article&lt;/a&gt;, Functional
Programming in Scheme &lt;a href=&quot;https://homes.cs.aau.dk/~normark/prog3-03/html/notes/fu-intr-2_themes-continuation-sec.html#fu-intr-2_list-ex_title_1&quot;&gt;relevant
chapter&lt;/a&gt;
and &lt;a href=&quot;https://cleare.st/code/call-cc-yin-yang-puzzle&quot;&gt;The call/cc Yin-Yang
Puzzle&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Continuation-Passing Style&lt;/h2&gt;
&lt;p&gt;The style of programming in which the execution flow is controlled
explicitly through continuations passed as function argument.  The
analogy of callbacks is floating somewhere around.  Let's write a
simple &lt;code&gt;(* 3 (+ 1 2))&lt;/code&gt; expression in CPS:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define return)
(display (call/cc (lambda (k) (set! return k) (k &amp;quot;hi&amp;quot;))))
;; You can think about it somehow like this:
;; (define return (lambda (x) (display x) (exit))

(define (+&amp;amp; a b k)
  (k (+ a b)))

(define (*&amp;amp; a b k)
  (k (* a b)))

(+&amp;amp; 1 2 (lambda (x) (*&amp;amp; x 3 return))) ;; prints 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are more
&lt;a href=&quot;https://en.wikipedia.org/wiki/Continuation-passing_style#Examples&quot;&gt;examples&lt;/a&gt;
comparing the direct and continuation-passing styles.  In CPS some
things, which were implicit in direct style become explicit (returning
a value for example), but the code becomes somewhat turned inside-out,
more verbose and harder to reason about, so this style of programming
is quite uncommon among developers, however it can be useful for
&lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Continuation_002dPassing-Style.html&quot;&gt;some&lt;/a&gt;
&lt;a href=&quot;https://wingolog.org/archives/2023/05/20/approaching-cps-soup&quot;&gt;use
case&lt;/a&gt;
(representing intermediate compilation targets, programming language
researches, etc).&lt;/p&gt;
&lt;h2&gt;Delimited Continuations&lt;/h2&gt;
&lt;p&gt;Delimited continuation (aka prompt) is a generalization of
continuation, instead of capturing the context up to the beginning of
expression, it captures the context only up to delimiter, also, it
returns a value and can be composed in contrast to usual one.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(use-modules (ice-9 control)) ;; for reset and shift

(define kont)
(* 2 (reset (+ 1 (shift k (set! kont k) (k 5))))) ;; =&amp;gt; 12
kont ;; =&amp;gt; #&amp;lt;procedure 7f29fdf05040 at &amp;lt;unknown port&amp;gt;:64:17 vals&amp;gt;
(kont 3) ;; =&amp;gt; 4
(kont (kont 3)) ;; =&amp;gt; 5

;; Example with side effects
(* 2 (reset
      (begin
        (display &amp;quot;hi\n&amp;quot;)
        (let ((r (+ 1 (shift k (set! kont k) (k 5)))))
          (display &amp;quot;hello\n&amp;quot;)
          r)))) ;; =&amp;gt; 12 ; prints hi hello

(kont 3) ;; =&amp;gt; 4 ; prints hello
(kont (kont 3)) ;; =&amp;gt; 5 ; prints hello two times
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's clear from example that &lt;code&gt;(* 2 &amp;lt;&amp;gt;)&lt;/code&gt; is not captured as a part of
continuation and delimited continuation are functions, can be used as
such and thus can be composed.  There are different names and
interfaces in different languages, even Guile Scheme in addition to
&lt;code&gt;shift&lt;/code&gt; and &lt;code&gt;reset&lt;/code&gt; has
&lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Prompts.html&quot;&gt;call-with-prompt&lt;/a&gt;
and friends.&lt;/p&gt;
&lt;h2&gt;Dynamic Wind and Continuation Barriers&lt;/h2&gt;
&lt;p&gt;There are two mechanism, which are tightly related to non-local
enters, exits and reenters.  &lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Dynamic-Wind.html&quot;&gt;Dynamic
Wind&lt;/a&gt;
allows to trigger actions on [re-]enter and exits, it can be useful
for setting up or cleaning up resources and maybe some other use
cases.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(* 2 (reset (+ 1
               (dynamic-wind
                 (lambda () (display &amp;quot;entered\n&amp;quot;)) ; on-enter hook
                 (lambda () (shift k (set! kont k) (k 5)))
                 (lambda () (display &amp;quot;exited\n&amp;quot;)))))) ; on-exit hook
;; =&amp;gt; 12, prints entered exited two times because we call k in shift

(kont 3) ;; =&amp;gt; 4, prints entered exited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Continuation-Barriers.html&quot;&gt;Continuation
Barriers&lt;/a&gt;
prevents non-local enters and exits by erecting the fence, impassable
for continuations.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(with-continuation-barrier
 (lambda ()
   (define tmp #t)
   (display (+ 1 (call/cc (lambda (k) (set! kont k) 3))))
   (when tmp
     (set! tmp #f)
     (kont 6)))) ;; prints 4 7

;; In procedure %continuation-call: invoking continuation would cross continuation
;; barrier: #&amp;lt;continuation 171 @ 7faffed6d800&amp;gt;
(kont 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It only applies to non-local enters and exits, which tries to cross
the barier, if they jump around inside barier they are good and will
work without problems.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It's good and sometimes necessary to know about &lt;code&gt;call/cc&lt;/code&gt; to better
understand how things works in Scheme in particular and in programming
in general, however it usually should not be used in production code.
The first-class support for it is a controversial design decision, but
it's already a part of Scheme, so let's just embrace it.&lt;/p&gt;
&lt;p&gt;Delimited continuations are a generalization and overall a better
alternative to undelimited continuations, however they should be used
with a great care as well.&lt;/p&gt;
&lt;p&gt;The good entry point for in-depth materials on the topic are on Oleg
Kiselyov's
&lt;a href=&quot;https://okmij.org/ftp/continuations/index.html&quot;&gt;site&lt;/a&gt;. More links and
examples in Andrew Tropin's personal
&lt;a href=&quot;https://github.com/abcdw/notes/blob/88680bd6c81926cf1e1b437e321d618151c38f18/notes/20230224184049-continuations.org#L6&quot;&gt;notes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Acknowledgments.&lt;/strong&gt; Thanks to all the great Computer Scientists,
Engineers and Writers, who bring the light of knowledge and such
power.&lt;/p&gt;
</content></entry><entry><title>Scheme Static Site Generators Review</title><id>https://trop.in/blog/scheme-static-site-generators-review.html</id><author><name>Andrew Tropin</name><email>andrew@trop.in</email></author><updated>2023-05-23T12:00:00Z</updated><link href="https://trop.in/blog/scheme-static-site-generators-review.html" rel="alternate" /><content type="html">&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Static site generator is a program, which accepts text files as input
and produces static web pages as output.  It can be useful in various
scenarios: for building blog, book, documentation, project or personal
page for example.&lt;/p&gt;
&lt;p&gt;There are a few SSGs
&lt;a href=&quot;https://github.com/pinceladasdaweb/Static-Site-Generators#scheme&quot;&gt;written&lt;/a&gt;
in Scheme available in the wild, namely
&lt;a href=&quot;https://dthompson.us/projects/haunt.html&quot;&gt;Haunt&lt;/a&gt;,
&lt;a href=&quot;https://www.nongnu.org/skribilo/&quot;&gt;Skribilo&lt;/a&gt; and
&lt;a href=&quot;http://wiki.call-cc.org/eggref/5/hyde&quot;&gt;Hyde&lt;/a&gt; and two more for Racket:
&lt;a href=&quot;https://docs.racket-lang.org/pollen/&quot;&gt;Pollen&lt;/a&gt; and
&lt;a href=&quot;https://docs.racket-lang.org/frog/index.html&quot;&gt;Frog&lt;/a&gt;, which are
outside of Guile ecosystem, but still quite close and can be taken as
a source of inspiration.&lt;/p&gt;
&lt;p&gt;Hyde doesn't seem to be maintained, but the &lt;a href=&quot;https://code.call-cc.org/svn/chicken-eggs/release/4/hyde/trunk/&quot;&gt;source
code&lt;/a&gt;
still available and even &lt;a href=&quot;https://bitbucket.org/DerGuteMoritz/ate&quot;&gt;an
attempt&lt;/a&gt; to go
further/reincarnate the project exists.&lt;/p&gt;
&lt;p&gt;Skribilo is documentation production toolkit and capable of much more
and provides a lot of functionality outside of SSG scope, so we don't
cover it in this writing.&lt;/p&gt;
&lt;p&gt;Basically we have only one option left at the moment: Haunt and the
further discussion will be related to it, but before exploring it, we
need to get to common ground and cover the topic of different markup
languages.&lt;/p&gt;
&lt;h2&gt;Markup Languages&lt;/h2&gt;
&lt;p&gt;Markup languages are used for defining documentation structure,
formatting, and relationship between its parts.  They play an
important role in SSGs, different languages can suite better for
different tasks: simple and expressive for human convenience, powerful
and capable for intermediate representation and manipulation,
compatible and wide-spread for distribution.&lt;/p&gt;
&lt;h3&gt;SGML, XML, HTML, XHTML&lt;/h3&gt;
&lt;p&gt;This is a probably most widespread family of markup languages,
currently used all over the web.  Not always, but usually SSGs create
HTML or XHTML documents as an output.  Also, it is good to know the
relationship between those languages to understand some technical
issues we will face later.&lt;/p&gt;
&lt;p&gt;SGML (Standard Generalized Markup Language) appeared in 1986 and
highly influenced HTML and XML.&lt;/p&gt;
&lt;p&gt;XML (Extensible Markup Language) is a meta language, which allows to
create new languages (like XHTML), originially developed as a
simplification of SGML with rigid and not open for confusion syntax,
it's defined in the SGML Doctype language.  Often used for
representing and exchange data.&lt;/p&gt;
&lt;p&gt;HTML is a more user friendly markup language, it's defined in plain
english, has more forgiving parsers and interpreters, allows things
like uppercased tags, tags without matching closing tag.  Such
flexibilities can be convenient for users, but it makes it harder to
programmaticaly operate on it (parse, process and serialize).&lt;/p&gt;
&lt;p&gt;XHTML (XML serialization of HTML) is a version of HTML, which is
compliant with an XML grammar.  XHTML can be used with XML parsers,
tools for querying, transformation should work as well.&lt;/p&gt;
&lt;p&gt;While both HTML and XML are influenced by SGML, there is no direct
relationship between them and tools for XML can't be used for HTML in
general case.&lt;/p&gt;
&lt;h3&gt;Lightweight Markup Languages&lt;/h3&gt;
&lt;p&gt;This is another family of markup languages, which are simpler, less
verbose, and more human-oriented in general.  The notable members are
Wiki, Markdown, Org-mode, reStructuredText, BBCode, AsciiDoc.&lt;/p&gt;
&lt;p&gt;Often SSGs use those languages for representing the content of pages,
posts, etc.  Later it is combined with other parts and templates and
final output is produced, usually in the form of (X)HTML documents.&lt;/p&gt;
&lt;h3&gt;Other Markup Languages&lt;/h3&gt;
&lt;p&gt;There are a number of languages and typesetting systems, which are not
covered by the previous two sections: Texinfo, LaTeX, Skribe, Hiccup,
SXML.  The goals for them can be different: preparing hardcopies,
use as an intermediate format, or better suitability for specific needs
like writing documentation.&lt;/p&gt;
&lt;h2&gt;Haunt Overview&lt;/h2&gt;
&lt;p&gt;Haunt is a simple and hackable SSG written in Scheme, it tries to
apply functional programming ideas and the usual approach for building
a site with it: prepare SXML page templates, read the content from
HTML, Markdown or any other markup files and convert it to SXML,
insert the content into templates and serialize resulting pages to
HTML.  Let's discuss various parts of this process in more details.&lt;/p&gt;
&lt;h3&gt;SXML&lt;/h3&gt;
&lt;p&gt;SXML is a representation of XML using S-expressions: lists, symbols
and strings, which can be less verbose than the original representation
and much easier to work with in Scheme.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://okmij.org/ftp/Scheme/xml.html#SXML-spec&quot;&gt;SXML&lt;/a&gt; is used as an
intermediate format for pages and their parts in Haunt, which is
relatively easy to process, manipulate and later serialize to target
formats like XHTML.  It can be crafted by creating s-expressions from
Scheme code manually, or programmatically, or with a mix of both.  It
looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define n 3)

(define slide-content
  (get-html-part &amp;quot;./slide3.html&amp;quot; &amp;quot;body&amp;gt;div.content&amp;quot;))

(define (sxml-slide n slide-content)
  `((h2 ,(format #f &amp;quot;Slide number: ~a&amp;quot; n))
    (div (@ (class &amp;quot;slide-content&amp;quot;))
         ,slide-content
         (p &amp;quot;the additional text of the slide&amp;quot;))))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As it was mentioned in the introduction there is no direct
relationship between XML and HTML, and while we usually can parse
arbitrary HTML and convert it to SXML without losing significant
information, we can't directly use XML parsers for that.  For example
this HTML is not valid XML:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;html&quot;&gt;&amp;lt;input type=&amp;quot;checkbox&amp;quot; checked /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luckily, we can present boolean attributes in full form as
&lt;code&gt;hidden=&amp;quot;hidden&amp;quot;&lt;/code&gt;, which is valid both in HTML&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML&quot;&gt;^4&lt;/a&gt; and XML.&lt;/p&gt;
&lt;p&gt;Most lightweight markup languages as well as SSGs usually target HTML,
but SSG needs to combine the content, templates and data from various
sources and merge them together, so SXML looks like a solid choice for
intermediate representation.&lt;/p&gt;
&lt;h3&gt;The Transformation Workflow&lt;/h3&gt;
&lt;p&gt;Each site page is built out of a series of subsequently applied
trasformations. The transformation is basically a function, which
accepts some metadata and data and returns another data (usually SXML)
and sometimes additional metadata.  Because this transformation is
a pure function, a few transformations can be composed in one bigger
transformation.&lt;/p&gt;
&lt;p&gt;We will cover it in more details in the next section, but readers,
templates, layouts, serializers, builders are all just transformations.
For example the top level template, called layout just produces SXML
for the final page, which can be serialized to the target format.
To demonstrate the workflow we will take a bottom-up approach.&lt;/p&gt;
&lt;p&gt;Let's take a simple Markdown file, where one wants to write the
content of a blog post in human-friendly markup language and let's
add metadata to the top of this file: title, publish date, tags.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;Markdown&quot;&gt;title: Hello, CommonMark!
date: 2023-05-09 12:00
tags: markdown, commonmark
---

## This is a CommonMark post

CommonMark is a **strongly** defined, *highly* compatible
specification of Markdown. Learn more about CommomMark
[here](http://commonmark.org/).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can be parsed into metadata
(&lt;a href=&quot;https://www.gnu.org/software/guile/manual/html_node/Association-Lists.html&quot;&gt;alist&lt;/a&gt;) +
data (SXML).&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;=&amp;gt; ((tags &amp;quot;markdown&amp;quot; &amp;quot;commonmark&amp;quot;)
    (date . #&amp;lt;date nanosecond: 12 day: 9 month: 5 year: 2023 zone-offset: 14400&amp;gt;)
    (title . &amp;quot;Hello, CommonMark!&amp;quot;))
=&amp;gt; ((h2 &amp;quot;This is a CommonMark post&amp;quot;)
    (p &amp;quot;CommonMark is a &amp;quot; (strong &amp;quot;strongly&amp;quot;) &amp;quot; defined, &amp;quot;
       (em &amp;quot;highly&amp;quot;) &amp;quot; compatible&amp;quot; &amp;quot;\n&amp;quot;
       &amp;quot;specification of Markdown, learn more about CommomMark&amp;quot; &amp;quot;\n&amp;quot;
       (a (@ (href &amp;quot;http://commonmark.org/&amp;quot;)) &amp;quot;here&amp;quot;) &amp;quot;.&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Metadata+data representing one post is a good unit of operation.  With
one more transformation (it can be just a template, function adding
&lt;code&gt;html&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; tags and a few more minor things) SSG can
produce almost ready for serialization SXML.  After deciding on the
resulting file name and serialization step, the final HTML is produced.&lt;/p&gt;
&lt;p&gt;Some additional transformations can be desirable. For example,
substituting relative links to source markup files in the generated html
files or something else, but overall it fits this general trasformation
workflow well.&lt;/p&gt;
&lt;p&gt;Let's zoom out a little and take a look at the directory structure,
rather than a single file.  Usually, SSGs operate on a number of files
and in addition to simple pages can generate composite pages like a
list of articles, rss feeds, etc.  For this purpose our unit of
operation become a list of data+metadata objects: instead of parsing
one markup file, SSGs traverses the whole directory and generate a
list of objects for future transformation. The overall idea is still
the same, but instead many output files get produced from many input
files. SSGs produces a list containing only a few or even one output
file.&lt;/p&gt;
&lt;h3&gt;The Implementation&lt;/h3&gt;
&lt;h4&gt;The Entry Point&lt;/h4&gt;
&lt;p&gt;The entry point in haunt is a &lt;code&gt;site&lt;/code&gt; record, which can be created with
a function that has the following docstring:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;&quot;&gt;Create a new site object.  All arguments are optional:

TITLE: The name of the site
DOMAIN: The domain that will host the site
SCHEME: Either 'https' or 'http' ('https' by default)
POSTS-DIRECTORY: The directory where posts are found
FILE-FILTER: A predicate procedure that returns #f when a post file
should be ignored, and #f otherwise.  Emacs temp files are ignored by
default.
BUILD-DIRECTORY: The directory that generated pages are stored in
DEFAULT-METADATA: An alist of arbitrary default metadata for posts
whose keys are symbols
MAKE-SLUG: A procedure generating a file name slug from a post
READERS: A list of reader objects for processing posts
BUILDERS: A list of procedures for building pages from posts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The primary thing here is a list of builders. As previously mentioned,
a builder is a special case of complex transformation, which does all the
work of parsing, templating, generating collections, serialization, etc.&lt;/p&gt;
&lt;p&gt;The rest of the list is basically metadata or auxiliary functions.
While many of those values can be useful, almost none of them are needed
in many cases.  &lt;code&gt;scheme&lt;/code&gt; and &lt;code&gt;domain&lt;/code&gt; are used for rss/atom feeds, which
are rare for personal or landing pages. Similiar logic is applicable
to the rest of the function arguments, except for maybe &lt;code&gt;build-directory&lt;/code&gt;,
which almost always make sense.&lt;/p&gt;
&lt;p&gt;Providing default values for them is convenient, but making them fields
of &lt;code&gt;site&lt;/code&gt; records incorporates unecessary assumptions about the nature
of the blog and can negatively impact the rest of the implementation by
adding unwanted coupling as well as reducing its composability.  One of
the options to avoid it is to make them values in the default-metadata
rather than fields in the record.&lt;/p&gt;
&lt;h4&gt;Builders, Themes and Readers&lt;/h4&gt;
&lt;p&gt;Builders are functions, which accept &lt;code&gt;site&lt;/code&gt; and &lt;code&gt;posts&lt;/code&gt;, apply series
of transformations and returns a list of artifacts.  Themes and Readers
are basically transformations used in the build process.  Artifacts are
records, which have &lt;code&gt;artifact-writer&lt;/code&gt; field, containing a closure writing
the actual output file.  There are a number of different builders provided
out of the box, but the most basic one (static-page) is missing, luckily
it's not hard to implement it, so let's do it.&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define* (page-theme #:key (footer %default-footer))
  (theme
   #:layout
   (lambda (site title body)
     `((doctype &amp;quot;html&amp;quot;)
       (head
        (meta (@ (charset &amp;quot;utf-8&amp;quot;)))
        (title ,(string-append title &amp;quot; — &amp;quot; (site-title site))))
       (body
        (div (@ (class &amp;quot;container&amp;quot;))
             ,body
             ,footer))))
   #:post-template
   (lambda (post)
     `((div ,(post-sxml post))))))

(define* (static-page file destination
                      #:key
                      (theme (page-theme))
                      (reader commonmark-reader))
  &amp;quot;Return a builder procedure that reads FILE into SXML, adjusts it
according to the THEME and serialize to HTML and put it to
build-directory of the site.  DESTINATION is a relative resulting file
path.&amp;quot;
  (lambda (site posts)
    (list
     (serialized-artifact
      destination
      (render-post theme site (read-post reader file '()))
      sxml-&amp;gt;html))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As described in a section about transformations, the series of
transformations happens here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read-post&lt;/code&gt; basically parses markdown and returns SXML + metadata.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;render-post&lt;/code&gt; uses post-template from &lt;code&gt;theme&lt;/code&gt; to produce SXML post body.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;render-post&lt;/code&gt; uses layout from &lt;code&gt;theme&lt;/code&gt; to produce SXML post body.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serialized-artifact&lt;/code&gt; creates a closure, which wraps &lt;code&gt;sxml-&amp;gt;html&lt;/code&gt;
and will later serialize obtained SXML for the page to HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implementation using already existing APIs is quite easy, but
unfortunately not perfect.  While functions and records are composable
enough to produce desired results, names are quite confusing and
tightly related to blogs, but doesn't make much sense in the context
of other site types.&lt;/p&gt;
&lt;p&gt;Every builder always accepts a list of posts, which were read and
transformed into sxml ahead of time. This transformation is implicit
and again blog related, which makes the implementation less generic.
It could be implemented in the &lt;code&gt;blog&lt;/code&gt; builder, but this way other
builders like atom-feed won't be able to reuse readed posts from from
&lt;code&gt;blog&lt;/code&gt; builder and would need to read them again.  This is due to the
fact, that the build process has three primary steps and looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;;; 1. Prepare site and posts

;; 2. Build artifacts
(builder1 site posts) ;; =&amp;gt; artifacts-1
(builder2 site posts) ;; =&amp;gt; artifacts-2
(builder3 site posts) ;; =&amp;gt; artifacts-3

;; 3. Produce actual site:
(serialize-artifacts
 (append artifacts-1 artifacts-2 artifacts-3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It makes a build process rigid and makes it harder to compose
procedures.  The alternative more streamlined process could look like
this:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define readers (list ...))
;; threading macro passes the result of the form
;; as a first argument to the next form
(-&amp;gt;
 (make-site ...) ;;=&amp;gt; ((site . &amp;lt;site-record&amp;gt;))
 (read-posts
  &amp;quot;posts/&amp;quot; readers) ;;=&amp;gt; ((posts . &amp;lt;list-of-posts&amp;gt;) (site . &amp;lt;site-record&amp;gt;))
 (static-page &amp;quot;index.md&amp;quot; &amp;quot;index.html&amp;quot;) ;;=&amp;gt; ((artifacts &amp;lt;index-artifact&amp;gt;) ...)
 (blog-posts theme) ;;=&amp;gt; ((artifacts &amp;lt;post1-artifact&amp;gt; &amp;lt;index-artifact&amp;gt;) ...)
 (collection &amp;quot;main&amp;quot;) ;;=&amp;gt; ((artifacts &amp;lt;coll-artifact&amp;gt; &amp;lt;post1-artifact&amp;gt; ...) ...)
 (atom) ;; takes value from posts and appends a few more artifacts
 (atom-by-tags)
 (serialize-artifacts!))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just a series of transformations, which enriches one associative data
structure.  Moreover it makes the implementation of such
transformations much more composable:&lt;/p&gt;
&lt;pre&gt;&lt;code infostring=&quot;scheme&quot;&gt;(define (read-posts o dir readers)
  (let* (;; (dir (site-posts-dir (assoc-ref x 'site))) ; could be
         (posts (map (read-with-readers readers) (files-in dir))))
    (alist-update o 'posts (lambda (x) (append x posts)))))

(define* (static-page o file destination
                      #:key
                      (reader commonmark-reader)
                      (page-layout default-page-layout))
  (let* ((sxml-body (get-sxml (reader from)))
         (sxml-page (page-layout sxml-body))
         (page (serialized-artifact destination sxml-page sxml-&amp;gt;html)))
    (alist-update o 'artifacts (lambda (x) (append x (list page))))))

(define* (blog-posts o destination-dir
                     #:key
                     (page-layout default-page-layout)
                     (post-layout post-layout))
  &amp;quot;Implementation for the first posts here is to clearer demonstrate the
idea of reusability.&amp;quot;
  (let* ((post (first (assoc-ref o 'posts)))
         (destination (string-append destination-dir (post-file post)))
         (sxml-content (get-sxml post))
         (sxml-body (post-layout from))
         (sxml-page (page-layout sxml-body))
         (page (serialized-artifact destination sxml-page sxml-&amp;gt;html)))
    (alist-update o 'artifacts (lambda (x) (append x (list page))))))

(define* (collection o name
                     #:key
                     (filter-function identity)
                     (collection-generator default-collection-generator)
                     (page-layout default-page-layout))
  (let* ((posts (filter-function (assoc-ref o 'posts)))
         (file (string-append name &amp;quot;.html&amp;quot;))
         (sxml-body (collection-generator posts))
         (sxml-page (page-layout sxml-body))
         (collection (serialized-artifact file sxml-page sxml-&amp;gt;html)))
    (alist-update o 'artifacts (lambda (x) (append x (list collection))))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The naming of intermediate transformations is much more suitable (no
notion of the post in static-page builder), the transformations are
more atomic and it's easier to reuse them (page-layout and similiar)
and there is no need to combine them into records like &lt;code&gt;theme&lt;/code&gt;, it's
easier to restructure complex transformations, for example there is an
option to make a collection a part of &lt;code&gt;blog&lt;/code&gt; builder or be a separate
step as in example above, there is no need to special case read and
serialize steps, the read step can skip posts, which are flagged as
drafts or have some other advanced logic, now it's possible to build a
page, which relies on the content of previous steps, for example a
collection of generated rss/atom links.&lt;/p&gt;
&lt;p&gt;However, such implementation has its own flaws: more flexibility and
less rigid structure can lead to more user mistakes and a steeper
learning curve. The original implementation could theoretically run
builders in parallel, but one will need to implement it on the
user or builder side.&lt;/p&gt;
&lt;h3&gt;Readers&lt;/h3&gt;
&lt;p&gt;As a component of the build process we encountered a step, where the
file within the markup language is read by readers.  There are two
parts for it: reading metadata and reading actual content.  Let's cover
implementation details for them.&lt;/p&gt;
&lt;h4&gt;Metadata&lt;/h4&gt;
&lt;p&gt;As shown in the example code snippet in the section related to
transformation, one can provide additional metadata in a simple
key-value format delimited by &lt;code&gt;---&lt;/code&gt; from the content of the markup
file.  There are two main issues with the implementation, let's
discuss them.&lt;/p&gt;
&lt;p&gt;The metadata is required for built-in readers and even if one doesn't
want to set any values, they have to add &lt;code&gt;---&lt;/code&gt; at the beginning of the
file.  This requirement is not needed and could be easily avoided.&lt;/p&gt;
&lt;p&gt;The metadata reader simply accepts colon-delimited key-value pairs.
It is potentially not be as flexible as yaml frontmatter.  Metadata in
such format usually is not a part of the markup grammar and that means
files are written in an invalid markup.  However, it's not a big deal,
as readers can use custom metadata parsers.&lt;/p&gt;
&lt;h4&gt;Guile-Commonmark and Tree-Sitter&lt;/h4&gt;
&lt;p&gt;Guile-Commonmark is used in Haunt by default to parse markdown files
in SXML, it doesn't support embedded html, tables, footnotes and
comments, so it can be quite inconvenient for many use cases.  It
somehow works and serves basic needs and more advanced use cases can
be potentially implemented with more feature full libraries like
a hypothetical &lt;code&gt;guile-ts-markdown&lt;/code&gt;
(&lt;a href=&quot;https://tree-sitter.github.io/&quot;&gt;tree-sitter&lt;/a&gt; based markdown parser).&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Haunt is the primary player in the Scheme static site generators arena
at the moment of this writing.  It gives all the basics to get up and
running.  The number of available learning resources in the wild are
much smaller than for similiar solutions from other languages
ecosystems, but provided documentation and source code is enough for a
seasoned schemer to start with.  Exploring the whole project is just a
matter of hours, this is not possible with codebase of &lt;code&gt;hugo&lt;/code&gt; or
&lt;code&gt;jekyll&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The functionality can be lacking in some cases, but due to the hackable
nature of the project, it is possible to gradually build upon the basics
and as well as any future needs.  Unfortunately, the current state of the
Scheme ecosystem and Guile in particular feels behind more mainstream
languages, but hopefully the popularity of Guile will reach a higher
level and the ecosystem will start growing in the nearest future.&lt;/p&gt;
&lt;h3&gt;Future Work&lt;/h3&gt;
&lt;p&gt;There are a number of improvement points for Haunt in particular, and
Guile Scheme in general. We need more complete tooling for working with
markup languages like org, md, html, yaml, etc.  As a generic solution,
tree-sitter seems like a good candidate to quickly cover this huge area.&lt;/p&gt;
&lt;p&gt;More streamlined and composable build processes for Haunt as described
in the Builders section could add to haunt's flexibility in general as
well as encouraging the use of reusable components.&lt;/p&gt;
&lt;p&gt;Possible integrations with other tools like Guix, REPL, Emacs for
easier deployment, better caching, more interactive development and
other goodies.&lt;/p&gt;
&lt;p&gt;More documentation, materials and tools for possible workflows and use
cases from citation capabilites and automatic url resolution to
on-huge-file workflows and org-roam integration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Acknowledgments.&lt;/strong&gt; Kudos to &lt;a href=&quot;https://dthompson.us/about.html&quot;&gt;David
Thompson&lt;/a&gt; for making Haunt, &lt;a href=&quot;http://www.erikedrosa.com/&quot;&gt;Erik
Edrosa&lt;/a&gt; for making guile-commonmark and
&lt;a href=&quot;https://git.sr.ht/~jgart&quot;&gt;jgart&lt;/a&gt; for extensive and careful editing of
the post.&lt;/p&gt;
</content></entry></feed>