At last, here is my follow up to small TODO apps where I created a small CLI TODO app in Rust, Clojure and Haskell to test functional (or functional like) programming languages. Like I said in my previous article, I also wanted to see how these languages behave for the web.
So I decided to create the same API four times: first in Python with Django to have a starting point (that's the tech I'm most at ease with) then in Rust, Clojure and Haskell. This API must be able to add a package, list all packages and search for a given package by name. The creation of a package should be protected by authentification. In the end, I only reached this full scope with Django. I removed authentication in Rust and Clojure to gain time.
As for Haskell, I completely dropped it: I tried the frameworks Yesdo and Servant. I had issues to compile the quickstart project with both of them. I managed to get Servant to work, but struggled with database access. I also found it difficult to get back in Haskell after such a long time. I could have searched more or reached for help on an Haskell forum, but the amount of time I was willing to dedicate to this exercice was running out. So I dropped Haskell and moved on.
All the code is available in my gitlab. In each project, you'll find a README that explains how to launch and use the project. Like in my previous post, I'll go over the main characteristics of each implementation and then try to conclude.
The project is separated in different apps like in all Django project. It's not strictly necessary for a project so small, but it's how you do things. I based the project on the official tutorial instead of something more complete (like this generator) to keep things simple. I merely changed the app organisation a bit to have something a bit more structured. In the end, I have two apps: core to manage users and their authentication and packages for the packages.
My code is structured around three concepts:
- The model for database access.
- The serializer to transform the model into JSON and validate inputs when we create a new package.
- The view to glue everything together. It receives the HTTP request, creates the serializer, appends the current user id to the data when we create a package and filters the data when we do a search. The filters part could have been extracted elsewhere but it felt overkill for such a simple project.
While writing the apps, I wandered what it would be like if I search with a % (it has a special meaning in SQL) character in the same like 100%. I created a package named To 100% and beyond and one named Over 100°C water and search for articles that contained 100% (yes, those names are silly for packages). Django ORM correctly and automatically escaped % from my input and the view only returned the To 100% and beyond package.
For my Rust version I decided to use the actix mostly because I already used it a bit in the past. I was also able to read the example project sqlx-todo which uses the same tech I did: rust, actix and a sqlite database.
While I could have used an ORM with actix, I decided not to: I wanted to test a bit of async and await in Rust and at this time no ORM is compatible with this programming method.
I rely on custom Shell scripts to create the database and its tables. It's more than enough for my small test. Since I have the same usage as the Django version, I relied on Django to generate the SQL. And since I initially planned to add authentication, I create a table for users and a link between users and packages. To simplify my implementation, instead of removing this, I decided to create a sample user which will own all the packages. This way, from the database perspective, I'm closer to the Django version.
The code is divided in two files:
- main.rs which contains all the view logic.
- model.rs where the struct representing a package (the struct can be serialized and deserialized easily with a lib named serde) and all the logic relating to the database.
Overall, the experience was pleasing. There's less boilerplate than in any other version which makes the project easier to understand when you get started. On the downside, it's up to you to organize things correctly when the project grows. The sqlx-todo project also helped me to start correctly faster, which was nice. The only real pain point I had was to correctly serialize the package creation date time: there is documentation on how to do that with the chrono library but I struggled to put it all together like I wanted.
Finally, to answer my question: how does it behave by default with % in the input, I reproduced the steps of the Django version. The answer is: by default % won't be escaped and will behave as the special SQL character it is. It's logical you have to do it yourself since you build the actual query yourself.
For Clojure, I decided to use the Luminus framework because it's easy to get started with it. leinigen (an automation tool for Clojure) makes it really easy to generate a project configured to use a SQLite database. The downside is: the generated project is quite big and for a newbie like me, it's easy to get lost in all this (like a newbie to Django would be I guess).
Luminus has an integrated migration system based on manually written SQL files. So I used that to create my table, once again thanks to SQL generated by Django from its models. All those files are in clj_luminus/resources/migrations/. There is always a .up.sql file to apply the migration and a .down.sql to revert it if needed.
Creating a simple view to list package is simple enough as you can see in clj_luminus/src/clj/clj_luminus/routes/api.clj (function list-packages). It can then easily be linked in the router. What I find peculiar, is how Luminus handles SQL queries:
- You write all your queries in a file named clj_luminus/resources/sql/queries.sql.
- Above the query, you give it a name and describe what it does (query, update, insert) in a comment.
- Based on that, HugSQL (the library used by Luminus to do its SQL requests) will automatically create a function to actually do the query.
It's a bit strange when you are not used to it, but it works quite well. The big downside I didn't manage to overcome though: while a change in the code is immediately reflected in the project and is available with a simple page refresh, changing a SQL query always required me to restart the project. And project startup in Clojure is slow which is a pain. For a serious project, this would be a big downside. I guess there is a work around and my problem is just I didn't find it.
I also struggled a bit to create a package:
- I add to manually add a middleware so the posted JSON would be correctly parsed. That was not obvious at first.
- I'm not convinced by how I pass the date of creation. I think it could fail on a stricter database than SQLite.
- Getting the last inserted ID was way harder than I though. According to the documentation, there is a way to get it, I can see it in the returned data under a key :last_insert_rowid() but I never managed to access it. In the end, I converted the map into a vector and read the proper index from the vector. It's a bit ugly, but it works.
- Unlike with Django and Rust where the data is validated out of the box in the serializer or the struct, I had to do it manually here. It's not a big deal, but you must think of it when you are used to other tools.
To answer my question: how does it behave by default with % in the input, I again reproduced the steps of the Django version. The answer is, just like for Rust: by default % won't be escaped and will behave as the special SQL character it is. Again, it's logical you have to do it yourself since you build the actual query yourself.
Before concluding, I'd like to point out that although I didn't add authentication because I lacked time, Luminus provides tooling to add it out of the box. So it should be fairly straightforward.
To conclude, I'll say I enjoyed writing these small apps and too see different languages and ways to do things. I felt the most at ease with Django since it's the framework I work with day to day. Rust is fun, the compiler does its best to help you and you can find many ressources online. The language (at least the bases, the language itself is actually quite big and complex) is also fairly easy to pick up if you have a programming background. Clojure is fun, there are some good projects and tooling out there, although the documentation could sometimes be better. Since it barely has any syntax, you can forget functions of the standard library after a while but not how to write code in it, despite how different it is from other languages. And Haskell, well, it's probably the language that I would benefit most from learning: it's very different to what I know and its approach probably would make me a better developer. However, it's hard to pick up with its syntax and type system, even with a programming background. And after a while, I forgot most of it. I don't want to dedicate time for it now, so I won't dig any further.