Using Rust and Elm to create Kronuz
Recently, I released kronuz into the daedalus family of products. Kronuz is an easy-to-use job scheduling app, focused on resource based scheduling. The app idea came from seeing a need for a job scheduler aimed at consulting businesses, where utilisation of staff and visibility on the job pipeline is paramount. The idea also provided a good project to get my hands dirty with Elm.
I had read much about Elm, and it interested me with its functional approach to a UI. The language itself transpiles to Javascript, and has packages that can control the DOM. There seemed to be a lot of positive case studies within the Elm community, and I wanted to try it out as an alternative to the DOM API I had created in Rust which powers daedalus' web stuff. So I took the plunge and built the kronuz front-end in Elm.
It was very easy to get started. The Elm devs have put a lot of effort into error messages and the guide to getting started was easy to follow. In no time at all, one can get a counter app working and have a good understanding of how the 'Elm Architecture' works. The guide is well-designed in how it introduces new subjects, layering on top of previous examples. Elm is very functional, with a syntax similar to Haskell/ML and everything is a value! It has some great syntax for pipelining and composing functions, and the currying support is fantastic (maybe a feature for ogma!?). Elm also sports a strong type system.
As with any project, especially when trying a new design, framework, or language, there
are growing pains. Kronuz consists of around 9000 lines of Elm code, so the need for modularisation
arose. Elm's modules are a bit confronting, especially once you have experienced Rust's.
Submodules are stored in the module's directory (such as Foo/Bar.elm
and Foo/Zog.elm
),
but the parent module would be stored in the root as Foo.elm
. This makes the source
folder contain many adjacent Elm files to Main.elm
, and a little disconnected to the
child modules. The terms parent/child are a little loose here as well, whereas I am used
to having a child module access to a parent's items (this is great for shared helper
functions), Elm's modules must form a strict DAG1. In fact, nesting modules does not
really create a hierarchy of access, rather it is just a naming exercise.
Another pain point was the whitespace required for certain syntax. I found that my initial
prototype of a function would usually need refactoring, making use of the let .. in
syntax. Unfortunately, items under a let
need to be indented another tab, and Elm will
immediately complain that it does not parse (usually with fairly poor error messages). So
the workflow was, copy the items out of the function, run elm-format
, run elm make
, go
back to editing. It was only a minor paper cut, but did make me reluctant to refactor
sometimes. The typically formatting is also heavy on new lines, so whilst the code is
quite concise, the files quickly become quite large. Grepping around is also difficult
without the use of keyword prefixes.
I also found myself avoiding decorating functions with their type signatures.
The need was twofold, to allow for faster prototyping, but to also pass through functions
as arguments, where the
type signature was unwieldy to type. For me, being able to forgo a type signature
constraint was a boon, but I could imagine a library author getting frustrated with trying
to concoct the proper signature.
The Elm Architecture
Elm's shining feature is the 'Elm Architecture'. At its core, it is a recommended way of templating a module that will interact with the UI. It is not a strictly enforced framework, but following the template gives a nice structure to how callbacks are handled, and, in my opinion, is the biggest challenge facing UI frameworks. The architecture consists of the model (your application state), the view (a function to generate a DOM), and an update procedure (a function that updates your model based on some message). The Elm guide gives a more in depth description. Since most of Elm's standard libraries are based around this architecture, a project will end up following it. It neatly separates the UI view concerns from the model itself, and by going through a single channel of updates, tightly controls how an update would occur. This architecture, coupled with having to pass everything around as values, gives a pretty strong guarantee that your model is never 'out-of-sync' with the view.
Much of the view is based around Elm's html
package, a set of DOM functions which build
a DOM tree to in turn build some HTML. I found it was pretty easy to create the UI I
wanted, with minimal need for libraries. Actually, there seemed to be a distinct lack of
libraries for widgets, I suspect because of the ease of creating your own. I did use a few
(for charts, date pickers, colour pickers), but the Gantt chart implementation, which was
one of the more complex views, really wasn't too hard using SVG.
Adhering to the architecture was sometimes a bit arduous, I found the syntax to update fields of
a record type heavy-handed and does not lend itself to pipelining very well, so setters end up in
little helper functions.
The Rust Backend
To get the app online, I built a server in Rust. It was interesting to contrast the
languages immediately, especially given that a web server is very much within Rust's
domain.
Immediately I noticed the prototyping speed that Rust allows for. Things like deriving
serialisation and a module system that can organically grow as the project grows really
accelerate prototyping. I used warp for the server
framework, which I always recommend, it is an excellent library, and really lets you
spin up a web server quickly.
Another language aspect that struck me is Rust's balance between functional and imperative
paradigms. I always had thought of Rust as fairly functional, with its 'immutable as
standard' stance. I realised that much of the APIs and libraries out there are incredibly
imperative. The compiler's smarts to only allow single mutable aliasing acts as a bit of a
lever to avoid having to program functionally. The experience in Elm made me rethink
this, opting to pass values around more, and being happy to cop some allocator performance
to avoid passing mutable references to functions.
After my experience in Elm, I decided to avoid using a shared Mutex
state for
the backing database, opting instead to run the database in its own (tokio) thread, and
pass messages to interact with it.
I liked the paradigm, and will probably use it more in the future.
Summary
I have enjoyed building an app using Elm. The language's functional design and focus on values make refactoring easy and code concise. The Elm Architecture is fantastic for callback infrastructure, and the type aliasing of record types allows for decent composability. I can imagine using Elm more for front-end applications, especially if a generic set of CSS styles was employed. For algorithmically heavy applications, I still prefer to use Rust. Rust's type system is second to none, and the module system and syntax is favourable.
For those interested, this is a tokei dump of my project:
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
CSS 2 1804 1444 37 323
Elm 40 11978 9166 255 2557
JSON 1 57 57 0 0
Shell 2 58 37 2 19
SVG 12 2587 2563 12 12
TOML 1 20 18 0 2
-------------------------------------------------------------------------------
HTML 1 26 21 0 5
|- JavaScript 1 35 28 2 5
(Total) 61 49 2 10
-------------------------------------------------------------------------------
Rust 8 1280 1079 13 188
|- Markdown 5 10 0 10 0
(Total) 1290 1079 23 188
===============================================================================
Total 67 17810 14385 319 3106
===============================================================================