haskell-servant / servant

Servant is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.83k stars 413 forks source link

HTML templating with external files #297

Open pikajude opened 8 years ago

pikajude commented 8 years ago

I spent the last day or so looking into ways to render HTML templates with Haskell and combine those with servant, and came away unsatisfied. All the solutions I found required a potentially objectionable workaround.

My gut feeling is that MimeRender is just not powerful enough to let users use a templating engine, but since I'm not an expert on the internals or architecture of servant I'd welcome some input on my assessment. Ideally I would like mimeRender maybe to be Proxy ct -> a -> EitherT ServantErr IO ByteString, or for another function mimeRenderM to be added to the typeclass and by default implemented in terms of mimeRender.

Thoughts?

imalsogreg commented 8 years ago

+1 for mimeRenderM and leaving the m abstract.

3noch commented 8 years ago

@pikajude Have you seen servant-lucid?

3noch commented 8 years ago

Also,

No separation of logic and presentation.

I think that's a red herring. You can always separate them if you want to. Formal templating languages are always trying to find the sweet spot between logic and presentation. But it's a very hard spot to find, because it depends a lot on the problem at hand. I personally think it's too diverse for a framework to make that call categorically.

pikajude commented 8 years ago

@3noch Sorry, I'm afraid I don't understand your responses. The whole point of this post was that I want to separate them and I can't unless I'm willing to use either unsafePerformIO or compile-time IO.

I'm certainly open to the idea that separation of logic and presentation is unnecessary, but I would like to have the choice, at least.

3noch commented 8 years ago

@pikajude Sorry for the confusion! I agree with you that it's important to separate logic from presentation. However, I was responding to your objections to using blaze-html or lucid. You claim that they do not allow you to separate logic from presentation. My claim is that they actually pose no threat to separating logic and presentation. The only difference is that they do not necessarily separate logic and presentation. However, you are still quite capable of employing the separation yourself: simply refrain from embedding logic in your templates.

For example, in lucid you could be tempted to not separate logic as is common in PHP apps:

template = html_ $ body_ $
    ul_ $
        forM_ [1..10] $ \item -> do
            queryResult <- liftIO (runComplexQueryFor item)
            li_ queryResult

main = serve template

But because you have the full abstracting power of Haskell at your fingertips, there's no reason you can't refactor this to enforce better separation:

template items = html_ $ body_ $
    ul_ $
        forM_ items li_

main = do
    queryResults <- forM_ [1..10] runComplexQueryFor
    serve (template queryResults)
freezeboy commented 8 years ago

what about servant-ede https://github.com/alpmestan/servant-ede ?

pikajude commented 8 years ago

@freezeboy Don't know what else to add about it.

@3noch I do agree that a certain degree of separation is certainly possible when using blaze or lucid, though whether you can avoid embedding logic in your templates depends on how you define what a "template" is in the first place. In this particular case, the "template" is not something I would expect Web Developer Smith, who might be collaborating with me on a servant project, to be able to maintain without learning Haskell syntax and how to debug the errors that GHC spits out.

jkarni commented 8 years ago

This is a little tricky, semantically. By allowing mimeRenderM to do IO, we are essentially saying there isn't necessarily a pure function from the a to the a rendered as a particular mime type, which I think is problematic conceptually, even if helpful in practice. Indeed, this may have negative consequences, such as documentation generating different timestamps for samples for the same object if part of the IO being done involves looking up the time.

From that perspective, TH or a variation on servant-edes approach is in fact the right solution, since what we want is a function that is pure but takes an extra param (the contents of the template file), but with mimeRenderM we allow for much more. In particular, I think we could provide a TH function that generates a datatype of e.g. TemplateFiles '["file1.tmp", "file2.tmp"], and which contains as values the contents of those files, which then the API is free to use. This improves on servant-ede by not involving unsafePerformIO and by statically ensuring the API only uses templates we know about, and needn't involve significantly longer compilation times since the TH stage would only involve reading files, not processing them.

I think (for reasons similar to those mentioned by @3noch ) that it's a little unfair to dismiss lucid and blaze-html generally - they just have different trade-offs. So if no one objects, I'll change the title of the ticket to HTML templating with external files so as to make clear we're not taking a general stance of the merits of e.g. ede, mustache, and heist vs. blaze-html and lucid.

jjmornim commented 8 years ago

Can I use servant-lucid if I already have an html template (file) and a few html pages? And how?

alpmestan commented 8 years ago

@jjmornim Not sure servant-lucid can do something for you. I know that blaze-html comes with a program that turns HTML pages into the equivalent blaze-html code. You might want to modify it to output lucid code instead?

@pikajude the unsafePerformIO in servant-ede isn't too bad really. I'm however convinced my solution there isn't optimal, as the "compiled template store" is behind an MVar, which costs a little bit, on every request. What I would need, I think, is to use the recent Context machinery to make the template store available to the MimeRender instance. So instead of allowing IO in MimeRender, I'd just need an extra arg. OTOH, with streaming request/response bodies, multipart upload and templating use cases, it looks like we really might want to think about allowing IO in rendering code...?