DEV Community

digitallyinduced
digitallyinduced

Posted on • Updated on • Originally published at ihp.digitallyinduced.com

How IHP uses Haskell's Type System to enforce good patterns

Good patterns and clean code are what differentiates a production application from a legacy application. In a lot of cases, many production applications become legacy applications with time, because patterns aren't enforced and therefore ignored, wrongly interpreted, or otherwise abandoned.

IHP uses Haskell as its language of choice, and one big reason for this is the typesafety that Haskell provides. After reading this article you'll hopefully understand how IHP is making use of Haskell's strong typesystem to enforce proper use of patterns shared between all IHP applications, which prevents your production webapp from becoming a legacy webapp.

Sidenote: at digitally induced we have multiple older IHP apps, none of which we consider "legacy", even if they've been running for quite some time. If we need to make changes to them, it is very easy to get back into them and understand what is going on, as all IHP apps follow similar patterns. We know what to expect, and where to find the code we're looking for.

Model-View-Controller

Let's get the biggest point out of the way first. IHP uses the popular Model-View-Controller pattern, which is characterized by the three parts giving the pattern its name:

The Model is the data of the application, which has a static structure when running, but variable content, as the content is user-generated. In IHP all data types are auto-generated from the database schema, ensuring that the code you write is always compatible with the database.

The View is a simple mapping that turns data into Html. Using Haskell's type system, this is enforced by defining every view as a pure function (a function without side-effects that always produces the same output if it receives the same input). You might have heard that that's the core idea of React as well, and it's a reason React is so popular: the render function should simply take the current component's state and render Html based on that. However, React has the problem that everything else is also possible in the view, and in a way even requires it to be there, including updating state. In IHP, the view really fulfills this promise, and it's enforced by the type system.

The Controller is the part of the application that contains the actual business logic, and should be the only place in the application that is able to interact with the outside world, including the database (also known as IO: Input and Output). In Haskell, doing IO isn't possible everywhere, only in functions that have been declared to be able to do it. IHP makes use of that by defining the actions (endpoints of controllers) as the only functions that can run IO things. Even if someone is tempted to fetch data from the database in the View, they can't, because the controller is simply going to prevent it from working.

Fetch all required information

As described above, the View is a function mapping data to Html. However, this data is different from view to view of course. Since the data has to be fetched by the controller, the view defines a data structure which the controller needs to completely fetch the information for to render the view, which makes sure that no information is ever missing in the view, not even accidentally. And when some information is not necessary anymore, you can just remove it from the view data structure, which will cause compiler errors everywhere where you're still fetching it (where there could now be unnecessary code).

To read more about how passing the data from controller actions to the view works, read the documentation here.

No missing information for links

When the user wants to interact with the website, they mainly do so via links. Usually links are simply strings, which means that if parameters are required but missing, this can only be detected via trial-and-error. Using IHP's pathTo and urlTo functions, you can build the links between pages of your application in a typesafe way, which means you're not going to forget to send new necessary information when an endpoint requires it, and will not forget to remove it when it's unnecessary anymore. Renaming is a non-issue as well, and typos are (again) compiler errors.

IDs cannot be used to query the wrong database table

In IHP, IDs are (by default) UUIDs. But even if you use Integer-based IDs instead, you could run into a situation where you'd accidentally use a user-ID for querying a different table, and wouldn't be any wiser, since both are UUIDs. In IHP on the other hand, all IDs are wrapped once more, making the type of the ID Id User for example. You then can't use this ID to for example fetch a product.

If for some obscure reason you still need to do this, or you get the ID as another type and need to convert it to this special type, that's of course possible.

This special type also allows the fetch function that queries the database for a single row with the given ID to be super simple to call: since the ID already contains the information of which table it's for, you don't have to do any more work than passing the ID to the function, and it will take care of the rest.

Ensuring proper HTML

Views in IHP are written in Haskell, using something called HSX, which is the same basic idea as JSX in React. That means you write the HTML you would normally write, and can easily include dynamic Haskell code where needed.

Since HSX is just syntactic sugar for other Haskell functions though, it is typesafe! That means you can't use attributes for elements where the spec doesn't allow for it, and many markup errors (like forgetting to close a tag) are caught at compile-time.

If you need to use custom attributes, that's what data- attributes are for, and they are fully supported. Just like custom web components.

Bonus: beginners in React often want to quickly output the content of some data they have, and try to just inline the variable in their JSX. They are then often surprised to see [Object object], since converting an object to a string in JS will lead to this result. In HSX, this will call show on the provided data if possible, leading to the expected result.

Maybe and the dreaded NullPointerException (or TypeError: variable is undefined)

While technically not IHP-exclusive, null and undefined do not exist in Haskell. Instead, if you need to represent something not being there, you can use Nothing, which is a value for the Maybe type. Using this, you can represent that something might not be there, which will force you to handle that case. But once you've handled that case, you don't have to handle it again - something that I've seen a lot in medium to larger codebases, where it's not always entirely clear where a value might come from.

What this means in essence is that you will never get a NullPointerException or a TypeError: yourVariable is undefined when using IHP!

Conclusion

IHP makes as much use of Haskell's types as possible, leading to less bugs and an easier-to-grasp codebase that won't become legacy. If this article peaked your interest in IHP, you can get started using the Guide.

Discussion (4)

Collapse
codinghusi profile image
Gerrit Weiermann

Wow that sounds very interesting!
I'll take a look at your follow ups :)

If someone is interested in catching the most errors in compile time (without NullPointerException), you may be also interested in Rust.

Collapse
gabrielfallen profile image
Alexander Chichigin

Unfortunately Rust pretty much stops at "NullPointerExceptions". As long as Rust does not separate pure code from effectful one (on the type level) it's impossible to guarantee purity of view functions or any other functions. Thus the ability to "enforce proper patterns" at compile-time is limited.

Collapse
digitallyinduced profile image
digitallyinduced Author

You hit the nail on the head!

Collapse
digitallyinduced profile image
digitallyinduced Author

I agree that Rust is also a great language. Many of its features have been heavily influenced by functional languages. In my personal (limited) experience using Rust, it still caused more runtime errors than Haskell code. Mainly due to what dev.to/gabrielfallen said: not being able to declare on the type level which functions have side effects makes it a lot harder to reason about code than when that information is included in the type. It also means that Rust couldn't enforce the MVP pattern as well as Haskell can.