lessons from tiny hakyll blog fixes in haskell and lua

2026-03-18
, ,

Did your function just mod the state?

Context

In Emacs lisp

shell-command-to-string function in Emacs is not a pure function, because it has side effects, such as executing a shell command and potentially altering the state of the system. In contrast, adding to the exec-path is considered an impure function since it modifies the environment by changing the list of directories where Emacs looks for executable files.

This post will also observe the interoperability aspect.1

Deferred exec of Emacs lisp

Emacs lisp defers execution via macros and lazy evaluation 014 the expression is not evaluated until its value is needed, or until explicitly forced.

Macros vs lazy eval

defmacro defines Lisp macros that transform and generate code at compile time or before evaluation. It operates on the unevaluated code expressions (arguments as code), rewriting them into new Lisp expressions that get evaluated later. This allows defining new syntactic constructs and control over evaluation order.

Lazy evaluation means delaying the evaluation of expressions until their values are actually needed at runtime. Lazy evaluation controls when an expression is computed to avoid unnecessary work but does not transform or rewrite the code itself.

Caveat: the error when trying to splice whole keyword argument lists directly in a backquoted use-package form produces invalid syntax for the macro expansion. The correct approach is to build and combine the argument list first, then call use-package via apply.

Lisp macro rewrites vs Haskell’s automatic derivation

In Haskell, automatic derivation is a type-system feature that automatically generates instances of certain type classes (like Eq, Show, Ord) for data types based on their structure. This happens at compile time, producing boilerplate code so programmers don’t have to write repetitive instance declarations. It relies on Haskell’s strong static type system and compiler extensions.

Lisp’s defmacro defines a macro that transforms Lisp code before evaluation. Macros in Lisp manipulate code as data (unevaluated s-expressions) and can generate new code with arbitrary complexity, enabling metaprogramming and custom syntactic constructs. defmacro works at the syntactic level producing new Lisp expressions.

In Haskell

The analogy needed sufficient context. So I wrote a masto client app running on warp server, with a single feature to post a toot to my instance from localhost.

  1. Separation of concerns between warp and masto servers

    1. WAI warp side

      Reads registered client creds from OS store tool and stores them in AppState, not DATABASE, which is loaded to the warp server instance as the server starts to receive and send HTTP (GET, POST) requests to the masto server. The WAI app receives auth details request from masto server, handles them and responds with HTML forms, text inputs from user, does the JSON parsing of ASCII string text manipulation 014 these responses are sent back to masto server.

    2. Masto server

      Upon receiving any of these requests, verifies if it’s a credential, sends back the HTTP response over TLS and WAI, routes and loads them accordingly.

  2. Note on data manipulation

    I am concatenating TEXT in my HTML with Data.ByteString.Lazy, which provides a way to handle large or unbounded streams of data efficiently in Haskell, while Data.ByteString.Lazy.Char8 offers a character-based interface for manipulating lazy ByteStrings, specifically for 8-bit characters. Both are useful for different tasks, with the former focusing on general byte manipulation and the latter on character operations, making them suitable for tasks like handling ASCII and HTML data.

  3. Notes on record field values

    In Haskell, when you declare a record like:

    data ApiClient = ApiClient
      { acMastodonInstance :: String
      -- other fields
      }

    the name acMastodonInstance is a function of type:

    acMastodonInstance :: ApiClient -> String

    It needs to be applied to a value of type ApiClient to obtain the String. That’s why you ought to supply a value of type ApiClient for the field accessor acMastodonInstance to produce a String.

    1. Function application and Lambda Calculus

      Once you’ve instantiated this data constructor record field as its value, say client, and you apply a function to it, the field accessor behaves as a regular function 014 this is the essence of Lambda Calculus applied to record types.2

  4. Namespace pollution and ambiguity

    Which functions can’t be exported from an imported module 014 and why qualifed imports (import qualified Data.Map as Map) are the idiomatic solution.

  5. Use liftIO for lesser runtime penalty

    When mixing IO with transformer stacks, liftIO lifts an IO action into the monad transformer stack without re-entering IO unnecessarily.

  6. Associative concat, applicative style, monadic chaining

    Even when I wrote pure functions 3 for more than one type of data, or used liftIO to reduce runtime penalty, HLS code-completion guided the correct style.

  7. Code for callback routing

    Uses both function binding and pattern match on Maybe with case. Using the WAI app interface isolates logic 014 which is as critical as separation of concerns 014 without needing the DSL of servant for declarative code.

    So it also illustrates how a beginner can start with just case, just conditionals, Maybe monad, and pattern match and advance towards higher concepts with ormolu and documentation. Beware of AI though 014 there are cases when even ormolu can throw you in a chicken-and-egg loop of errors in just one block, and AI will fail to resolve it. Instead of being lost in patterns or textbooks that don’t tell you which tooling to use, build incrementally.

    Here’s the working program for the above, but do check the previous commit for the simpler boilerplate code if you’re learning.

Footnotes


  1. https://academy.fpblock.com/blog/rust-haskell-reflections/↩︎

  2. I pass ServerState as a shared resource (via IORef, MVar, or ReaderT) to request handlers and they access the client secret and ID from the shared state when needed, before prompting the user or performing OAuth.↩︎

  3. If you wrote any Nix, this is like those configuration files 014 also pure with worse or no typecheck, until you insist on building it in some impure way. It also means that my src does not need to access the record fields at compile time, and I should not get “unused fields” warnings if I set up warp correctly:

    1. Define record with server state
    2. IO action for runtime access to fields (loading the record fields)
    3. Define server app which takes server state or its loading as an argument
    4. A main function to tie everything together
    ↩︎

Webmentions

Leave a comment

Comments are verified via IndieAuth. You will be redirected to authenticate before your comment is published.