Notes on MuniHac 2025 talks
Sep 19, 2025 · 8 minute read · CodingMuniHac has happened again. I enjoyed it a lot. Below you can find my personal notes on the talks. Free free to contact me if you have any comments!
- Rodrigo Mesquita: A modern step-through debugger for Haskell
- Gaël Deest: Hindsight - Type-safe, evolvable event sourcing
- Andrew Lelechenko: Linear Haskell for string builders
- Steve Shuck: The
pcre2
regular expression library - Tommy Engström:
domaindriven
- Type-safe event sourcing in Haskell - Joe Warren: How I use Haskell for 3D printing
- Mike Sperber: Six years of FUNAR - Teaching software architecture and functional programming to the uninitiated
Rodrigo Mesquita: A modern step-through debugger for Haskell
Rodrigo gives a pitch about haskell-debugger
:
- Uses Debug Adapter Protocol (DAP); dap-mode for Emacs.
- Can show evaluation order (helps understand lazy evaluation).
We also observe tail call optimization, a fundamental feature of functional languages, where no stack frame is created for intermediate function calls.
Gaël Deest: Hindsight - Type-safe, evolvable event sourcing
Event sourcing:
- Events are the single source of truth.
- Program is fold over events
applyEvent :: State -> Event -> State
Drawbacks of event sourcing
- Complex (indirection, cognitive load)
- Asynchronous events may lead to problems in consistency
- Schema design is hard
- Tests
Hindsight - The library
- Opinionated event store abstraction
- Handles versioned events (e.g.,
MoneyDepositedV0
) using type families - The code base contains all event versions, but see the talk of Tommy Engström:
domaindriven
- Type-safe event sourcing in Haskell
Other notes
- Events are single source of truth. If they are inserted into the database, they happened. That’s why it is key to distinguish between requests (e.g., `DoThis`) and commands (e.g., `DoneThis`).
Andrew Lelechenko: Linear Haskell for string builders
String
- String concatenation
(++)
is linear in the length of the left argument. - Even for long strings, this is not too much of a problem, when concatenation happens once.
- However, it does make a big difference when folding over lists (fast
foldr
vs slowfoldl
). - Often, left concatenation is hidden, for example, with hierarchical
Show
instances. That’s part of the reason, why we useshows :: String -> String
. - For function composition, left or right association does not matter much
(i.e., only by a constant amount of time)!
f . (g . h) ~ (f . g) . h -- In time.
- Difference lists
ensure that we always use function composition. However, using functions everywhere does have a performance impact.
newtype DList = DList (String -> String)
Text
- We want to use
Text
anyways.Left and right concatenation have the same runtime, but are both slow! (It is linear in both of its arguments; we have to copy both byte arrays).data Text = Text { buffer :: ByteArray , offset :: Int , length :: Int }
- We’d have to know the final length of concatenated pieces of text, so we can allocate the final length, and copy everything in.
- Lazy text sidesteps the issue by managing a list of byte arrays.
- Lazy types can treat with lazy/streamed input/output.
- However, they still do not look into the future and repetitively allocate chunks of, e.g., 4 kb.
TextBuilder
- Strict builder by summing the individual lengths of text fragments before allocating memory.
- Fast, but needs to track (pre-compute) maximum lengths of text fragments.
Java-style string builder
data Buffer = Buffer {
buffer :: ByteArray
, used :: Int
}
(++) :: Buffer -> Text -> Buffer
(++) = ... -- Mutates buffer (`freeze . copy bytes . thaw`).
Unsafe! We can not just mutate buffers, we need to copy them somewhere else.
We can be honest about having a mutable buffer:
data MutBuffers s = MutBuffer {
buffer :: MutableByteArray s
, used :: Int
}
(++) :: MutBuffer s -> Text -> ST s (MutBuffer s)
Impractical. Can not be used with standard library functions.
Linear types
Instead of poisoning the context with ST
, we would like to restrict our
functions in that they can only use the buffer once.
First use hidden in attoparsec
:
data Builder = Builder {
gen :: Int
, buffer :: ByteArray -- Also stores the generation counter `gen` at start.
, used :: Int
}
If somebody is tempering with the mutable buffer
, one can detect it using the
immutable generation counter gen
. (Bryan o Sullivan).
Linear and unlifted types
data Buffer :: TYPE ('BoxedRep 'Unlifted) where
Buffer :: {-# UNPACK #-} !Text -> Buffer
Combines idea of Java and attoparsec
.
(Values of unlifted types are never bottom.)
appendBounded :: Int _> _ -> Buffer %1 -> Buffer
(>) :: Buffer %1 -> Text -> Buffer
runBuffer :: (Buffer %1 -> Buffer) %1 -> Text
runBufferBS :: (Buffer %1 -> Buffer) %1 -> StrictByteString
%1 ->
(also printed as lollipop arrow) means the old value becomes invalid,
i.e., is mutated.
We wrap the Buffer
type into an exposed Builder
type we are used. Affects
performance, again.
data Builder = Builder ) Buffer %1 -> Buffer )
fromText :: Text -> Builder
-- ...
Benchmarks show that the library linear-builder
is extremely fast (especially
the linear interface, not so much the builder interface).
Steve Shuck: The pcre2
regular expression library
In Haskell, it is currently too complicated to use regular expressions
- We need two libraries:
regex-base
and a corresponding backend, e.g.,regex-pcre
. - We need two imports:
Text.Regex.Base
, andText.Regex.PCRE.String
. - Type signatures are complicated and too general.
Usage of pcre2
module Main (main) where
ipmort System.Exit (die)
import Text.Regex.Pcre2
main :: IO ()
main = do
let re = matchOpt (Caseless <> NotEmpty) "a*"
case re "aa b Aaa" of
[] -> die
results -> print results
Compilation of expressions happens once and is stored in a lookup table. (But GHC is smart enough to reuse the compiled regular expressions anyways; i.e., the lookup is not even performed). That is, each regular expression is compiled only once, even when it is matched multiple times.
Also impressive, Steve provides Template Haskell splices:
- In expressions, the splice defines a function that can directly applied to the string to match against.
- The quasi quoter can also be used in patter matching contexts. We can even refer to match groups!
Tommy Engström: domaindriven
- Type-safe event sourcing in Haskell
This talk is a reply to the talk of Gaël Deest: Hindsight - Type-safe, evolvable event sourcing.
Tommy shows how he is handling events in Haskell. He uses servant
, and
domaindriven
, a library he is developing.
If event types changes (e.g., a record is added), Tommy migrates all events in
the store using a function with ShapeCoercible
type class constraint. (You
need to define how to shapeCoerce
EventV1
to EventV2
).
He also argues that the big downside of event sourcing is that the initial design is too important. Months into projects, we usually have collected knowledge about how the initial design can be improved, but major changes are difficult to implement when using event sourcing.
Joe Warren: How I use Haskell for 3D printing
How do 3D printers work:
- Printer follows GCode, which is a list of path instructions (tool path).
- STL format: Describes a raw, unstructured triangulated surface by the unit normal and vertices of triangles. Can be compiled to GCode.
We can construct objects using constructive solid geometry, a Boolean algebra for superimposing 3D shapes.
- 1991
- OpenCascade release (Non-uniform rational B-spline, NURB).
- 2006
- In anticipation of the expiration of the FDM patend, first projects start to design open 3D printers.
- 2009
- FDM patent expires (most available 3D printers use this technology)
- 2010
- OpenSCAD release.
- 2018
- Joe writes CSG library.
- 2023
- Waterfall CAD (bindings to OpenCascade).
Joe makes a point for using programmable CAD frameworks: Abstraction and code reuse.
Producing a number of Christmas logos from SVG images induced Jow to develop an SVG importer into Waterfall CAD, and and exporter from Waterfall CAD to SVG.
Mike Sperber: Six years of FUNAR - Teaching software architecture and functional programming to the uninitiated
- Active group.
- Bob conference March 13 2026, Berlin.
- Haskell Interlude.
- FUNARCH 2025. Online ticket available; still looking for contributions.
What is (specifically) functional software architecture?
Replies from MuniHac audience.
- Pure business logic
- Keep interaction with outside world edges
- Design data types that capture domain logic
- Parse, don’t validate
- Be declarative
- Capture laws/properties of data at design stage
What is software architecture?
- Designs long-lived software, large software involving multiple people teams
Functional design and architecture - Alexander Granin.
FUNAR module: Functional software architecture (iSAQB).
Structure and interpretation of computer programs (SICP) - Abelson, Sussman.
How to design programs - Felleisen, Findler, Flatt, Krishnamurthi.
Schreibe dein Programm - Sperber, Klaeren.
Software architecture in Haskell
Functional programming excels in data modeling:
- Sums (!) and products (algebraic data types).
For example, model the “Hearts” game.
- Data: What is a card, rank, hand, game, and so on.
- Events, commands.
- Domain logic, gameplay workflow.
The modulith - salvation for the monolith?
Structured design: Fundamentals of discipline of computer program and systems design - Yourdon, Constantine.
Managing, changing code is expensive. The more interdependencies/coupling you have in your code, the harder it is to change code. Optimize code to have low coupling.
Immutability, abstraction boundaries, expressive interface languages, expressive effects.
Functional software architecture
- Bottom-up architecture
- Late software architecture / architecture avoidance (so architecture is easy to change)
- Functional qualities first (adapt your code; rest through changes, which are easy; refactor!)
Conclusions
- Positive feedback
- We need good books
- We need good communications
- The ability to teach well does matter; if something works well in teaching, it usually works well in production
- Usability may be more important than efficiency or type-level foo
Questions/comments
- Primitive obsession and Boolean blindness. Use algebraic data types, not primitive data types.
- Abstract data types (information hiding; preserve invariances) vs full export (extensibility).