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.
Separation of concerns between warp and masto servers
WAI warp side
Reads registered client creds from OS store tool and stores them in
AppState, notDATABASE, 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.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.
Note on data manipulation
I am concatenating
TEXTin myHTMLwith Data.ByteString.Lazy, which provides a way to handle large or unbounded streams of data efficiently inHaskell, 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.Notes on record field values
In Haskell, when you declare a record like:
data ApiClient = ApiClient { acMastodonInstance :: String -- other fields }the name
acMastodonInstanceis a function of type:acMastodonInstance :: ApiClient -> StringIt needs to be applied to a value of type
ApiClientto obtain theString. That’s why you ought to supply a value of typeApiClientfor the field accessoracMastodonInstanceto produce aString.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
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.Use liftIO for lesser runtime penalty
When mixing
IOwith transformer stacks,liftIOlifts anIOaction into the monad transformer stack without re-enteringIOunnecessarily.Associative concat, applicative style, monadic chaining
Even when I wrote pure functions 3 for more than one type of data, or used
liftIOto reduce runtime penalty, HLS code-completion guided the correct style.Code for callback routing
Uses both function binding and pattern match on
Maybewithcase. Using the WAI app interface isolates logic 014 which is as critical as separation of concerns 014 without needing the DSL ofservantfor declarative code.So it also illustrates how a beginner can start with just
case, just conditionals,Maybemonad, and pattern match and advance towards higher concepts withormoluand 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
https://academy.fpblock.com/blog/rust-haskell-reflections/↩︎
I pass
ServerStateas a shared resource (viaIORef,MVar, orReaderT) to request handlers and they access the client secret and ID from the shared state when needed, before prompting the user or performing OAuth.↩︎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:
- Define record with server state
- IO action for runtime access to fields (loading the record fields)
- Define server app which takes server state or its loading as an argument
- A main function to tie everything together
Leave a comment
Comments are verified via IndieAuth. You will be redirected to authenticate before your comment is published.