This content originally appeared on DEV Community and was authored by digitallyinduced
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.
This content originally appeared on DEV Community and was authored by digitallyinduced
digitallyinduced | Sciencx (2021-08-04T12:56:30+00:00) How IHP uses Haskell’s Type System to enforce good patterns. Retrieved from https://www.scien.cx/2021/08/04/how-ihp-uses-haskells-type-system-to-enforce-good-patterns/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.