Mastodon Icon GitHub Icon LinkedIn Icon RSS Icon

A Dwarf Fortress calendar in PureScript + Halogen

My last week project involves PureScript and Halogen and the Dwarf Fortress calendar. I wanted to give a first-hand experience with some pure functional language for web front-end and, after discarding Elm, I ended with PureScript. I will not go on a comparison between PureScript and the rest of the world. If you want a comparison among the other candidates, you can look at this very detailed article. (There is ClojureScript too, if Clojure will ever came back from the graveyard).

To test PureScript I decided to implement a very simple project: a page showing today’s date according the calendar used in Dwarf Fortress. It is easy enough to be tackled without me knowing nothing about PureScript and Halogen in a week: you take today’s date, you apply some math, and you print your result on an HTML widget. At the same time, I think it is complex enough to have a grasp of PureScript potential (at least, in the allocated time).

You can find the result here. (Github Repository) Now, we can go on.

The Algorithm

This is the least interesting part, but should be covered. The algorithm is outlined in the following steps

  1. Take today’s date.
  2. Find the amount of days from the first day of spring (21st March) of 2006 (Dwarf Fortress release year).
  3. Scale the day duration to map Dwarf Fortress year.
  4. Use this number to compute the date in the new calendar.

[caption id=“attachment_2092” align=“aligncenter” width=“574”]The Dwarf Fortress calendar table The Dwarf Fortress calendar as described on the wiki.[/caption]

Point 3 is required because a Dwarf Fortress year is composed by 12 months of exactly 28 days. So, in total, a Dwarf Fortress year is composed by 336 days. This will quickly make seasons out-of-phase with our Human Seasons, therefore I scaled down a Dwarf Fortress day to be (365/336) = 8% longer than our day in order to make the year sync.

For a similar reason, I choose to make the new year start on 21st March because in the Dwarf Fortress world the year starts in spring (and I live in the northern hemisphere, so…).

Setting up the project

PureScript can be installed easily with npm:

1
npm install -g purescript

This is not enough. You also need pulp, the PureScript build tool, and bower, the standard front-end package manager.

1
npm install -g pulp bower

To create an empty project, create a folder and use the command pulp init.

Writing the Algorithm in PureScript

PureScript is Haskell for the web. It is very similar to Haskell, but it is not. It has several small but important differences. Some of them are improvements due to the fact that PureScript can simply drop many “wrong” legacy decisions of Haskell. Some of them are different design choices given that PureScript must output JavaScript code as clean as possible. Other are just missing features.

As a Haskell user, I quickly feel comfortable with the language. However, you need to pay attentions to some important difference between Haskell and PureScript:

  • First, PureScript is not lazy. It is strictly evaluated so that it is possible to avoid complex runtime in the JS output.
  • PureScript requires an explicit forall in polymorphic type/function declaration. For instance length :: [a] -> Int is valid in Haskell. However, in PureScript this must be declared as length :: forall a. Array a -> Int .
  • There is no derive functionality. (UPDATE 2nd Oct) I dug a bit more on this topic. PureScript does not support the “classic” Haskell deriving but a StandaloneDeriving-like functionality (in the form of derive instance for every type class we want to derive). This can be enabled in Haskell to using language extensions. This is more verbose, but it is a good thing. More info here.
  • Function composition is represented by <<< instead of .. This was a big source of confusion for me because using the . is not a syntax error (but it tries to do something different from composition). Why is that? Because composition can be done right-to-left or left-to-right and the . operator is ambiguous on which kind of composition we are applying.

Another important difference is that the Haskell’s IO monad is replaced by the Eff monad. This has the same function of IO but it can be mad more granular. That is, while IO can be used for any non-pure I/O operation, the Eff monad can be defined for different kinds of side-effects: console interaction, logging, databases, random values, and so on.

For instance a type Eff (fs :: FS, trace :: Trace, process :: Process | e) can be found on functions that access the file system, trace something on the console and get data from the current process (in Node.js). While Eff (fs :: FS) works in function that only access the file system.

This makes everything more confusing and complicated, especially for people who already have problems with the concept of IO monad.

There are more differences, of course, but these are the main one.

After I become confident with these changes, my Haskell motor started working on PureScript code and writing the algorithm went smooth.

Building the user interface with Halogen

Writing the user interface has been the hardest part. Even if my interface consists only in a computed text string, the lack in UI tools documentation made everything more complex. I started with Flare. This is an amazing powerful library, but very crippled in documentation and community support. So, after struggling to find a suitable example for a widget without inputs controls, I gave up.

Instead, I chose a more “stable” library: Halogen. This is a React.js-like UI library for PureScript. Its main advantage is that it is old enough to have some questions answered on StackOverflow and a more solid documentation. It is not perfect, but, at least it covers the building of a simple projects from scratch with detailed and step-by-step examples.

I fought with some monadic aspects of Halogen, but, in the end, I came up with 36 lines of code for the display widget. This seems to me a good amount of lines for such a simple component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type State = ArmokDate

data Query a = Regenerate  a

data Input a = Unit

ui :: forall eff. H.Component HH.HTML Query Unit Void (Aff (now :: NOW | eff))
ui =
    H.lifecycleComponent
     {
         initialState: const initialState
         , render
         , eval
         , receiver: const Nothing
         , initializer:  Just (H.action Regenerate)
         , finalizer:  Just (H.action Regenerate)
     }
    where
        initialState :: State
        initialState = { day: 1, month: Granite, year: 0}

        render :: State -> H.ComponentHTML Query
        render state =
            let 
                string = showArmokDate state
            in
                 HH.div_ $ 
                 [ HH.h1_ [ HH.text string ]
                 ]

        eval :: Query ~> H.ComponentDSL State Query Void (Aff (now :: NOW | eff))
        eval = case _ of
            Regenerate  a -> do
                date <- (H.liftEff nowDate)
                H.put (convertLocal date)
                pure a</pre>

Compiling to JavaScript

Finally, we can compile the project to JavaScript with the command pulp build -O --to dist/app.js .

The result is an uncompressed 341.94KB file (125.87KB after minimization). This is not too bad considering that we are embedding the PureScript runtime (*), a couple of libraries (for datetime manipulation) and, in practice, a React.js pure-script clone. For this project is too much, of course, what’s matter is that the size overhead is good and it will remain stable.

Output JavaScript is verbose and difficult to parse for human being (even if it uses descriptive names). Therefore, I will discard the option “if I want to stop using PureScript I can simply start working on the app.js code”. This approach works in TypeScript, not in PureScript.

Other than that, the output app.js file is completely self-contained. It does not require any specific procedure to make it work. You just have to include it.

(*) UPDATE 2nd Oct: People on Twitter told me that PureScript does not produce a “runtime”. That’s true, it is my fault. I used “runtime” very loosely there. With “runtime”, I referred to a kind of “purescript standard library overhead”. In the generated code you find a lot of code from Prelude or Control.Monad.* modules. This is not technically a runtime, and it is not a “bad thing” by itself. But it is something “more” respect to a plain JavaScript script with equivalent effects.

Conclusions and final opinions

PureScript is a very promising language for front-end web development. It is a perfect choice if you want 90% of Haskell’s capabilities but with a stable compiler (GHCJS, the official Haskell to JS compiler, is still very unstable). In some parts, PureScript is even better than Hakell itself. But watch out for the differences! I spent more time that I’d like to admit on trying to understand why something that I used on Haskell didn’t work on PureScript.

However, I cannot say I am in love with PureScript, yet. Main problem for me is in the documentation. This is a problem for Haskell too. Documentation needs more examples, more small snippets of code, more explanations. For this project, I’ve found myself mentally solving algebraic types equations to guess which function I needed to do something. While the fact that I can do this is one of the biggest advantages of Haskell/PureScript type systems, this doesn’t mean that we can neglect a detailed documentation! I can figure out myself the right function by combining types in my mind, but I will be much happier if that was explained in the documentation somewhere!

Moreover, because the language is so young, it is very hard to find previous developers question answered. So, if you find a hard problem with some library, you are mostly on your own. (*)

PureScript is an extremely promising programming language, but for now, the ecosystem is too poor. I am sure it will improve as fast as it improved in the last years. For now, however, only people with an already solid base on functional programming can really have the strength to use this language in some serious production-ready project.

I will follow the project though. We need this kind of stuff in the front-end world.

(*) UPDATE 2nd Oct: This was a weekend project/challenge. I avoided asking questions on StackOverflow, or chats on purpose. First, I had no time to wait for an answer and I’ve never been really blocked on one issue for a long time. Second, and more important, I think that knowing how many problems you can solve without interacting actively with the community is a good indicator of the state of project’s documentation.

Said that, I feel I was partial in this. It is true that documentation can be greatly improved, but obviously, when documentation fails, there is the community. PureScript community is small, but judging from the amount of feedback on this article, I can assure you that PureScript community is very dedicated!

One of the problem, though, is that there is no link to any community group/forum/chat on the PureScript homepage! (but they are listed at the end of the README on GitHub). For this reason, and to atone for my sins, I will link them here too!

comments powered by Disqus