home

Microdiagram Prototype Description

This is a description of recently released MicroDiagram prototype. Check it out if you haven’t yet. It’s mostly technical and discusses some of the challenges I faced, as it was my first Haskell project.

Moving Parts

  • Backend - single binary written in Haskell, consisting of
    • Piano Diagram Parser + Generator
    • Diagram generating utilities
    • Scotty web server
    • HSpec and Quickcheck tests
    • Code volume is - approx. 1200 non-empty lines of Haskell code
  • Frontend - uvpoly
    • With fragment rendering for the Piano result
  • Deployment
    • Couple Cloudflare Containers load balanced by Cloudflare worker
    • Single not-static binary running on Debian

Average request duration is 7 ms and the median CPU time per request (diagrams are updated on keystroke!) is 0.88 ms.

Development highlights

  • Haskell implementation is the first to reach the deployable prototype phase.
  • That was my first Haskell project and I had no prior Haskell experience.
  • Previous implementations included Clojure, Elixir, OCaml, Go, and Rust.
  • The biggest issue was complexity management, which Haskell excelled at.

Microdiagram Concept

Let me quickly present an idea.

The Microdiagram concept is a concise, purpose-specific Diagram Domain Specific Language (DSL) that can be described in a short, 8-10 line cheatsheet. It is designed for targeted use.

Prototype shows Piano, for discussing various chords or keys. An example of the next implementation would be a diagram with fingering (i.e., which fingers go where). This would require slightly different syntax.

Other classic examples include Work Breakdown Structure diagrams and traditional organizational ones, as well as more complex structures such as Event Calculus 1,scenario alternatives or time-effect diagrams.

Beyond this being a diagramming exercise, there are some cool features that I won’t disclose, but ideally, this is an event-driven system because it has to be highly replayable. Of course, evidently, such a big system requires time to develop and might not spark any interest at all; getting to MVP is more important than implementing all those (in my opinion) cool features in the first place.

Previous prototypes

Clojure

One of the first prototypes was made in Clojure. I didn’t feel, however, that my progress was good. On top of that, I had an amnesia effect. Every single time I got to working on the project (weekend on a weekly/bi-weekly basis), I had no idea where I stopped and had to spend 1-2 hours trying to figure it out.

I was also often lost in implementing channel details, resolving deadlocks or trying being smart in doing transducers.

My guess is that at some point I’d overcome those hurdles, but there was another thing. I was working on a very similar system at the same time and wanted to reuse learnings for my work. Clojure was ruled out for the project, and thus I forfeited the prototype unfinished as non-aligning.

Elixir

I hold a grudge against Elixir. Its event-based system through Erlang/OTP brought promise, but developing in it was… painful (even though I used it professionally).

Quickly, I couldn’t remember what kind of events were sent, I was riddled with typos, Dialyzer couldn’t find anything (I know why, but won’t get deeper about it), and I had tremendous issues with development amnesia. Even though I documented scrupulously, I was lost in my own code after approximately 2 months of development.

OCaml

OCaml was very promising. I did some prototypes at work and was amazed by the tooling. Progress was good and the experience was pleasurable but then I got to the modeling of the Event System, and everything ground to a halt. I spent six weeks trying to figure out the model and just gave up.

To present the problem: I envisioned families of the Events. Some events would carry User info, some would carry Diagram data. Some would be Metaevents that would only work on events themselves (e.g., Rollback event, Rewrite event, Aggregate event, etc.).

The idea was to prevent and idiot (i.e., myself), from making a stupid mistake during implementation, like trying to extract session ID from meta-event or similar.

However, the end results were unsatisfying. I ended up either writing tons of boilerplate code or delving into GADTs of GADTs. In the end, after a couple of weeks, I still didn’t have it, and my project turned its attention toward Go, so I had no longer incentive to move forward with OCaml.

Go

Go was actually a pleasurable experience, but I got scared by the garbage collector. I knew that Diagrams would be server-rendered and fixing a jank caused by garbage collection felt like a tragedy waiting to happen. I checked out Zig for both - performent implementation and solution to CGo headaches, but then was hit personally by an event that completely threw me off track for couple of months.

Haskell

Haskell started with Prolog, as I wanted something that I could integrate with backward chaining. Prolog itself proved to be a trap that takes a lot of time and so frustrated I queried Google’s Gemini about language that’s statically typed, reasonably terse and have logic capabilities. I was pointed to Haskell.

I was skeptic at first, but having experience with Rust and OCaml I decided to give it a go. Haskell at this time proved to be easier to use than I expected, given prior experience, and easily provided what I needed from it.

On top of it, it was fun working with it, so one could say that the prototype progressed by itself.

Why Haskell Proved to Be the Best Choice

There are several reasons why I believe Haskell is the best choice for such a solution:

Typeclasses - thanks to these, I could easily implement Diagram and Event type classes. I wouldn’t have to worry about boilerplate because I could just ensure my datatypes are implementing specific functions and I was done with it.

Syntax - It might sound funny, but as I get older, all those parentheses, various braces, and double quotes become more difficult to input. A language without them is simply easier on my fingers, which I appreciate.

Simpler complex features - e.g., I found GADTs to be much easier to implement than those in OCaml. The same implementation was conceptually much simpler in Haskell than it was in OCaml. However, I also quickly found that with Typeclasses, I didn’t need many complex features, as they were enough for most things I needed.

Better complexity management - this one is huge. With Hoogle (Haskell search engine that allows searching by signatures), development amnesia wasn’t a thing. I sat at the code, figured out what I wanted, and with the compiler, I was able to figure it out. At some point, I found it funny that I could reach all the goals I wanted. It felt very friction-less and productive.

The hardest hurdles

It wasn’t all sunshine and roses though; I had some difficulties that took more time than I thought to go through.

Stabilizing the setup (with GHCID, Haskell Language Server, Stack, and Cabal) took a couple of days to get working. I encountered crashing HLS, very long compilation times, and more. I’m better with GHCID today, but it still feels like I’m throwing random command line at it, having my fingers crossed that it works. I still don’t have a formatter set, so I need to format stuff mostly by hand.

Then I got to the idiosyncrasies of libraries. I spent a day trying to integrate various template libraries, but those required casting, setup etc., and in the end, I just deemed this non-critical and went to implement HTML generation by hand. I also got to Scotty, which is a transformer over IO Monad, which was the start of a learning detour over Monads composition.

In the end, I grew, but I wanted to get shit done first. :-)

The last and the biggest hurdle was the poly integration (it cached everything so hard that I couldn’t see live output), and deployment.

Deployment

Deployment took one full day to get going, so I’d say it’s not trivial. I knew I had a binary that I “just” had to put “somewhere.”

Since I’m already hosting static pages at Cloudflare, I decided to see if I can run it there. Yes, I can - Cloudflare has a beta of containers which allow running anything in Docker containers while routing to it using TypeScript/JavaScript.

Building image itself wasn’t trivial. I was advised (by LLM) to try building static binary, copy output it to scratch image 2 and deploy scratch image.

Unfortunately, Haskell doesn’t provide static-compileable images so I couldn’t make a static artifact. On top of it, when I tried to use the built one, I ended up with a 3GiB image that was rejected by Cloudflare as too big. Only after colleagues made fun of me and binary size did I decide to inspect and find that “slim” Haskell image is 3.2GiB by itself. Obviously wouldn’t fly.

That gave me an idea, and I decided to try dynamically linked images built on Bullseye but deployed to the Bullseye-Slim variant. It worked! I ended up with a 100MiB image.

From then, I spent another 2-3 hours trying to route traffic correctly and squashing some bugs found in the process.

Final Words

It’s alive! I’m happy about it. It was a pleasurable experience, and after all, I’m cool with it being a complete flop. I might develop it for myself as a desktop app :)

I’m not even sure where it should go if it becomes popular. Should I develop it as a commercial solution? Should I open-source the engine and make commercial plugins for diagrams? Or maybe starve as an exemplary open-source contributor?

It doesn’t matter that much though - I got some joy and knowledge out of it, and that’s more than enough.


  1. Event Calculus is how one can interact in generic way with event, e.g. A -> B -> ROLLBACK(B) -> C ==> A -> C ↩︎

  2. bare, almost nothing in image ↩︎

Przemysław Alexander Kamiński
vel xlii vel exlee

gh | li | rss

Powered by hugo and hugo-theme-nostyleplease.