In my last post, I was writing about the use of coproduct of free monads to do content-type dispatching in a web monad. It was working but it was not the right approach. I changed everything and introduced a lattice of lists at the type level to track hierarchical dependencies between formats and do content-type dispatching thanks to type information. I also added a few other features to my Web Monad.
Here are the requirements I imposed to myself for developing the Web Monad :
- The type inferred from the code of an URL controller shall describe all the MIME formats supported by the controller ;
- The type of a controller shall be used to choose the best format among the ones requested by the client;
- The code in the controller shall forbid execution of code which is not related to the chosen format;
- It shall not be possible to mix code specific to RSS generation with the one specific to HTML generation (generalize the constraint to any format);
- When a page is using secure elements, the page shall be automatically tagged as secure and authentification done in an automatic way;
- No irreversible action shall be possible from an HTTP GET;
2. Type of the Web Monad
A Web monad value has type:
f is a type used to track the formats supported by the controller. s is a type used for security. Let's first talk about f.
2.1. Type for formats
f must be a list of formats. So, I define a type constructor:
data (f :+: g)
With this type constructor, I can write types like :
which is just a list. But the order will not be important so it is really an encoding of a Set of types.
I can convert such a type to a list of MIME types using a class:
contentType :: a -> [MIME]
and defining (f :+: g) as instance of ContentType.
I also define Web f s a as instance of ContentType when f is instance of ContentType.
Then, in the URL handler, I can write something like:
handler controller = do
formats <- contentType controller
I just use the type f to get the list of formats supported by the controller.
Let's define a few formats:
XML is any format made of XML tags. But, we need to make a distinction, for instance, between HTML and RSS. HTML and RSS are containing XML tags but they are more specific. So, I need to define HTML and RSS such that it is possible to use an inclusion relation on types. The following constraints must typecheck:
((:<:) XML RSS) => ...
Since, I am using a list of types, I must encode HTML and RSS as lists containing XML. So, I declare:
type HTML = XML :+: HTMLEXTENSIONS
type RSS = XML :+: RSSEXTENSIONS
I agree it is not very intuitive. But, that's just an encoding of the fact that HTML is more specific and contains XML.
I just need to define the inclusion relation (:<:). First, I must be able to check that an element is member of a list. So, let's define:
instance Elem a a
instance Elem a (a :+: b)
instance Elem a b => Elem a (c :+: b)
a is an element of [a].
a is an element of [a,b]
if a is an element of b then a is an element of c ++ b
Now, it is possible to define (:<:) a b. We just check that all elements of the list a are also elements of the list b.
instance Elem a c => (:<:) a c
instance ((:<:) a c, (:<:) b c) => (:<:) (a :+: b) c
With these constraints, I can start to write useful functions. First, functions for basic XML generation:
The tag function can be used in any controller whose type f is containing XML. So, it is a very general function. With tag, I can write specialized tags for the HTML.
h1 s = tag "h1" (text s)
h1 is tagged with HTML. So, in the definition of h1 we can use any XML tag. But, since HTML is not a subset of RSS and RSS is not a subset of HTML, it won't be possible to use h1 in the code for the RSS.
In my controller, I may want to be able to generate the HTML and the RSS depending of the preferences of the client. So, I need to be able to mix both of them. The controller will have the type Web (HTML :+: RSS) s () so it can contains code for HTML and RSS. But, we don't want the code for HTML to be executed when the chosen format is RSS.
So, I define:
forHTML is the only way to embed HTML code inside any code whose type f in containing HTML.
The Web monad is also a Reader monad and the environment provided by Reader is containing the chosen format. So, forHTML is just checking that the chosen format is corresponding to the HTML format. The code is:
k <- ask
when (k `elem` (contentType $ d)) $ do
ifNotHeaderSet $ do
setHeader "Content-type" (head . contentType $ d)
So, we execute the Web action d, only if the type of d is containing the chosen MIME type. And, if the code is executed, we set the header if it was not set. The code is generic and work for any Web f s a where f is instance of ContentType. The MIME to check for and the header generation are just depending on the type f.
HTML is instance of ContentType but XML is not.
So, now it is possible to write something like:
myController = do
forHTML $ do
forRSS $ do
forHTML $ do
It is not possible to mix the RSS code with the HTML one. Indeed, the HTML tag can only work in a Web HTML s a context. To be able to work in a Web f s a context where f is containing HTML, I need to use fromHTML which is doing the injection and the dynamical tests.
But, we can nevertheless reuse some functions to define HTML functions and RSS functions.
Of course, it can be generalized to any format (AJAX). And, it is possible to specialize even more. You may want to have HTML generation for a web browser and HTML generation for the iPhone. In that case, you would define two new types IPhone and Browser such that the following constraints typecheck:
(:<:) HTML IPhone => ...
You could also do things differently and define an IPhone type containing two specialized type : IphoneHtml and IphoneAjax. The solution is quite flexible.
Now, let's talk about security.
It is too easy to use an element that should be visible only by logged users and forget to do any check to prevent anonymous users from seeing it. The use of a phantom type s in the Web monad can easily track and avoid that problem.
As soon as any part of your controller is setting s to Protected then the whole controller will have the attribute protected :
If you write your generic handler such that it is checking the credentials when s is Protected then you are sure that you will never forget to do any security check when a secure element is used anywhere on your page.
But, you may not want to require authentification for seeing the page. You may just want to hide the secure elements on a page when the person viewing it is not allowed to look at the secure elements. Then, you just have to define a function like:
If the person can view the secure element, then the code for the element will be taken into account and otherwise it won't be displayed. Once the security check has been made, the security tag can be dropped and it can ONLY be dropped by the function ifAllowed.
So, at worst, if you forget to use this function, the whole page will be secure. But, note that when you define your page you can also force the security attribute and write:
Then, if you put a secure element in the controller and forget to use ifAllowed then the code won't typecheck !
For security, we need to define:
instance Security Public
instance Security Protected
Then, for each use of Web f s a , you should add the constraint:
to be sure s is only used with the types Protected and Public.
2.3. Irreversibility and HTTP GET
The method used is similar to the one used for the security. You can refer to an old post about it.
In short, I am using:
The web monad is also a CGI monad and an unique identifier monad since, for form generation, I need to generate unique identifiers. With the types, I think I have now a good tradeoff between security and flexibility.
I have had problem with Exceptions. I find it weird that you can't use catch (whose type is involving IO) in Monads which are instance of MonadIO. By luck, I found some source code proposed for Haskell' that it solving the problem.
Now, I am going to focus on form generation and form management. Then, I'll have to work a little bit on cache. And someday, perhaps, I'll replace my blog with an Haskell implementation ! The initial motivation is not just that Haskell is cool. The initial motivation is that I have too many problems with Ruby On Rails to reach a good level of quality and security. It is not true that you have less work to do with RoR. You can start more quickly but when the project is growing you discover the pain of dynamically typed languages and the incredible amount of details you have to check yourself instead of having a typechecker do it for you.
(This post was imported from my old blog. The date is the date of the import. The comments where not imported.)