Software is hard
Software is hard

Offline-First WebApps with Hoodie and PureScript

13 minutes read

Some time ago I tried to learn Elm. And believe it or not, I failed to compile a simple Hello-World program. Later I figured out that some interfaces have changed between certain versions of Elm making my failure inevitable. Long story short: the tutorial I found on the net was outdated and I didn’t know it. Well, how could I know this? Or more precisely: if a change in the language can hit a beginner so hard will he/she be willing to continue? In my case I ended up with PureScript. Often I hear or read that PureScript is ‘harder’ to learn than Elm or that you ‘need quite a bit of math’ to understand it. Maybe this is true. But at least in my case I can say that even a total beginner like me can very easily create ‘bridges’ to other popular JavaScript libraries and use them within PureScript’s type-safe environment. The greatest ‘selling point’ of PureScript isn’t his functional heritage because the same qualities you can find in Haskell, OCaml, Scala etc. What’s really remarkable and the PureScript community should emphasize even more is the fact that PureScript makes it so easy to ‘dock at’ JavaScript libraries that I often ask myself if I’m programming at all. I see almost no additional logic provided by myself. And the stuff just works™. PureScript is invisible and non-invasive.

So, just keep all of your beloved JavaScript libraries, all these more or less bloated frameworks and just add a certain layer of type-safety for less headache. I’m not the person who should teach others how to think about functional languages or programming at all. But I think I can serve as an example on how a beginner can very easily re-use any of the existing libraries while learning a purely functional language without being drown into Category Theory and other fluffy math stuff. I’m not saying that you’ll never need it. Quite contrary! You’re gonna need it a lot. There’s no YAGNI for Category Theory in purely functional programming. But there’s no need to be struck by a lightning bolt only to learn that electricity produces heat as a side-effect.

The moral of the story is: Take it easy, there’s a plenty of time to learn everything. Even Category Theory, Lemmas, Metaprogramming, Point-Free Style and other more advanced stuff.

Well, this should be enough for an intro. Let’s write some code for docking at a JavaScript-framework called Hoodie that’s being written by a bunch of really nice people.

The Hoodie Bindings for PureScript are here.

Hoodie: going offline with no backend!

Well, the title isn’t something you’d like to use as an advertisement for you latest Web-App, isn’t it? OK, I was a little bit unfair because we’ll not go offline with Hoodie but actually keep the app alive no matter if there’s a network connection available or not. And we do have a fully-fledged backend but simply don’t want to poison our frontend with it’s specifics. We just code the frontend as if there were no backend at all. In Hoodie’s case the backend is powered by the NoSQL database CouchDB, so you have to install it as a prerequisite.  Hoodie itself can be installed as an npm package via:

npm install -g hoodie-cli

However, I’d like to point to the excellent docs of the project for more detailed info. Every Hoodie app comprises of certain settings in package.json, like plugins, dependencies etc. To complete these tasks without headache you’ll use the hoodie-cli we mentioned previously. Give your app a name and wait for Hoodie to create the whole structure for you.

hoodie new myOfflineFirstApp

Then go to the newly created directory and push the start button by typing in:

hoodie start

After a few moments you’ll see a message like this:

hoodie_start_console

Please note that the output here is from Windows and on this OS you’ll run into a problem if your CouchDB installation is not on the default path (C:\Programs…\CouchDB… etc). As you can see in the screenshot my CouchDB is located under C:\bin and the app still runs. This is because I’m using a patched version of the npm package called multicouch that takes care of localizing CouchDB installations. There’s an open issue regarding this problem and my pull request is here. I have no clue if it works on other Windows machines so there’s no guarantee that it’ll work on yours. However, the more people test it the better for us all. If you’re under Linux/OSX you don’t have to take care of this problem as it only affects Windows.

After a successful start from the console Hoodie automatically opens up your default browser with the ubiquitous “ToDo” WebApp. The original app looks like this but in this article we’ll be using a modified version based on PureScript showing us some of the currently implemented Hoodie-API calls.

hoodie_demo_app

Now, it’s important to know that Hoodie doesn’t prescribe which kind of UI-Library or ‘framework’ you’d like to use. The ToDo-App, for example, is based on a bunch of jQuery calls as you can see them in the listing on the right. What’s really important is the fact that Hoodie instantiates a globally available object called hoodie or window.hoodie which is our only connection point to the store API that conveys the data transfers between the local (browser) representation and the backend (CouchDB or compatible databases). The Store itself offers several functions and before we use them we have to properly configure the store itself. The most important part is the declaration of the document’s type we’ll be using in this store instance. There can be many of them and in a ‘real’ web-application there are usually more of them available. In our example the only type we have to take care about is the “todo” document. The Hoodie docs give many examples on how to do this in JavaScript but here we’ll use PureScript to create a store, add, modify and delete documents.

Using Hoodie with PureScript

The bindings are located under src/API/Hoodie and split between the two usual *.purs and *.js files. The first one contains the PureScript counterparts (foreign imports) of the original JavaScript functions while the *.js file takes care of all the effectful stuff with instantiating JS-objects, callbacks, dealing with Promises etc. I’d like to point out that the current version is just a preview as I’m already thinking about providing a more sophisticated version with asynchronous effects (Aff) to better handle Hoodie-Promises. You may have already found out that the most of the Hoodie-APIs return Promises and the current version uses callbacks to handle done/fail. This, however, leads to a problem that one always has to provide these two callbacks which makes the Bindings-API bloated and very ugly. I’d like to have a skinnier API where you can handle all the async stuff in PureScript directly without being forced to provide callbacks just to make JavaScript happy. In short: Expect some deep changes here.

Before we can use any of the available APIs we have to instantiate Hoodie first.

hoodie_instatiating

Here we use hoodie API and provide Nothing as the only parameter because we want to use Hoodie at our current server. If we want to provide it on some other location we’d have to give a URL instead of Nothing. The second line is for defining an EventHandler to listen for certain changes in our Store. The Binding-API calls expect you to provide a valid Hoodie instance as the last parameter. In our case this is the myHoodie variable (or binding in PureScript lingo).

Now we want to define a document and put it on the Store.

defining_a_document

Every document in Hoodie is described by its properties. Here we define a document by its ID (not mandatory as Hoodie would automatically generate a new one for you) and a title. There’s also a possibility to provide an update function if we’re about to change an existing document by executing a certain logic. In general, if we provide an ID and Store finds out that there’s already an existing document with the same ID it’ll update it with new properties. The second option of changing a document is, as mentioned, by providing an update function with some extra logic that’ll update the document. For more detailed info you should definitely consult the nice Hoodie docs. In the above example we also provide a settings object plus a callback to complete the Promise on JavaScript-side.

To add the document to our local database we use the add function.

add_doc_to_database

Again, we have to provide a proper document type, its properties and options. The two callbacks are not mandatory but will be used here to complete the Promise returned from Hoodie. Had we decided to send a Nothing instead of callbacks purescript-hoodie would’ve automatically provided two ’empty’ callbacks to handle the Promise.

Now, open your console and refresh the page.

hoodie_add_document

And before you throw tomatoes at me: refreshing the page isn’t mandatory! We do this here just because we’re using PureScript without any additional UI-Layers or frameworks. So, there’s simply no two-way data-binding, virtual-dom or anything similar. We’re just testing the Hoodie API though PureScript’s lenses. No, dear Haskellers, not the lenses! I’m not a native speaker so my vocabulary isn’t filled with poetic metaphors. It’s actually a wonder that I’m able to write in (broken?) English as I’m not using it as a spoken language. The last time I used it (at work) is more than three years ago. No wonder I still have problems with Present Perfect Tense. Sorry.  :mrgreen:

Of course, we can search for documents in our database. There are several APIs and here we’re searching for all documents of type “todo”.

hoodie_find_all_docs

findAll needs a type and the two callbacks for done and fail parts of  the JS-Promise. The last parameter, as usual, is our Hoodie instance. You’ve surely noticed that we’re using the same callback for both possible outcomes (done/fail). It doesn’t have to be like this but in our case we just want to write the result to the console without any special treatment of return values. Here’s how our callback’s definition looks like:

callback

It accepts any type of data and forwards it to logRaw from API.Hoodie.purs. This function writes everything to the console without any special formatting. A poor man’s debugger.  🙄

The overall strategy is very simple, I think. You use a certain function to do something with a document or a collection of them. The needed parameters are either document properties or the general type of the document you want to work with. The Hoodie instance is mandatory and always the last of the parameters.

Now the question is, what happens to the documents if there’s no connection at all? Well, this where the ‘real’ Hoodie kicks in. You simply don’t have to care about the connectivity as Hoodie will always first save your changes to the local storage in your browser and frequently try to find out if there’s a connection available. And when it finally connects to the backend it’ll immediately update it to reflect the latest changes from your local store. You can also open several browser sessions with the same user logon. When you update a document on one of the browsers the change will immediately reflect on others and vice-versa.

Therefore, I’d recommend to learn about Hoodie even if you don’t like PureScript at all. However, I think that using Hoodie with PureScript one can only gain more stability as the fundamental abstraction of document databases like CouchDB is the TYPE itself. No single document can enter CouchDB without being a member of a certain type class. And we all know that PureScript’s main strength is the strict handling of Types. Having such a versatile backend like CouchDB combined with a syncing-engine like Hoodie almost automatically leads to an equally type-oriented and side-effects-protecting environment like PureScript. If the Types are what you really care about, then why leaving them to JavaScript?

Conclusion

To be honest, the above examples are not very good PureScript idioms. Also, I haven’t touched other aspects of Hoodie like accounts, events, plugins etc. Handling document types by littering the code with countless “todo”-parameters isn’t the best way of maintaining type-safety, in my opinion. It would’ve been better to use constructs like ADTs.

Anyway, I hope I could convince you that there’s no need to stick with only a single language. We’re constantly searching for best possible solutions of such important problems like keeping the App alive without network connection, changing data in a type-safe manner, simultaneously reflecting the storage state to all connected clients and other unavoidable side-effects.

Opening an app for public consumption is actually putting it into a world full of side-effects that could possibly hinder your customers from consuming it at all.

Leave a comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.