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
===============================================================================

1: I understand the benefits

Previous
Previous

Lock-free webserver using channels in Rust

Next
Next

Ogma v0.5 Release