[Haskell-beginners] working off a Yesod example file, need help lifting values from one monad into another. (and probably other things too).

Michael Litchard michael at schmong.org
Tue Mar 29 02:28:41 CEST 2011


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368

	

I'm working off of a example file from Yesod, ajax.lhs
I've made an important change in types, and this has resulted in
having to make the old code conform to the change. I will point out
the specifics, then present my question. In the event I failed to
include important information, I will paste in my code as well as the
prototype.

[Original]

> getHomeR :: Handler ()
> getHomeR = do
>   Ajax pages _ <- getYesod
>   let first = head pages
>   redirect RedirectTemporary $ PageR $ pageSlug first

[Changed]

> getHomeR :: Handler ()
> getHomeR = do
>   Tframe pages _ <- getYesod
>   let first = head pages
>   redirect RedirectTemporary $ PageR $ pageSlug first

Error Message

test.lhs:62:4:
    Constructor `Tframe' should have 2 arguments, but has been given 1
    In the pattern: Tframe pages
    In a stmt of a 'do' expression: Tframe pages <- getYesod   ****
This is not what I wrote *****
    In the expression:
        do { Tframe pages <- getYesod;
             content <- widgetToPageContent widget;
             hamletToRepHtml
               (hamlet-0.7.1:Text.Hamlet.Quasi.toHamletValue
                  (do { (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
                       . preEscapedString)
                          "<!DOCTYPE html><html><head><title>";
                        (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
                       . Text.Blaze.toHtml)
                          (Main.pageTitle content);
                        .... })) }

As far as I can tell, I only made a cosmetic change. I don't know
what's going on here.



[Original]

> data Page = Page
>   { pageName :: String
>   , pageSlug :: String
>   , pageContent :: String     ******** I'm going to change this **********
>   }

[Changed]

> data Page = Page
>       { pageTitle :: String
>       , pageSlug :: String -- ^ used in the URL
>       , pageContent :: IO String           ******** This is the change *******
>       }


Here's where I run into trouble

[Original]
>   json page = jsonMap
>       [ ("name", jsonScalar $ pageName page)
>       , ("content", jsonScalar $ pageContent page)   ******** I'm going to change this ********
>       ]



[My changes]

>   json page = jsonMap
>       [ ("name", jsonScalar $ Main.pageTitle page)
>       , ("content", jsonScalar $ liftIO $ pageContent page) ******* This is the change ***********
>       ]


Here's the compiler error

test.lhs:107:35:
    Couldn't match expected type `Char' against inferred type `[Char]'
      Expected type: String
      Inferred type: [String]
    In the second argument of `($)', namely `liftIO $ pageContent page'
    In the expression: jsonScalar $ liftIO $ pageContent page
Failed, modules loaded: none.


I'd appreciate a discussion about why this is wrong, and perhaps clues
as to what is right.


Last problem, stemming from the change in type to IO String. I don't
have a clue as to what change I should make.

test.lhs:100:25:
    No instance for (Text.Blaze.ToHtml (IO String))
      arising from a use of `Text.Blaze.toHtml'
                   at test.lhs:(100,25)-(103,3)
    Possible fix:
      add an instance declaration for (Text.Blaze.ToHtml (IO String))
    In the second argument of `(.)', namely `Text.Blaze.toHtml'
    In a stmt of a 'do' expression:
        (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
       . Text.Blaze.toHtml)
          (pageContent page)
    In the first argument of
`hamlet-0.7.1:Text.Hamlet.Quasi.toHamletValue', nam

                          ely
        `do { (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
             . preEscapedString)
                "<h1>";
              (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
             . Text.Blaze.toHtml)
                (Main.pageTitle page);
              (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
             . preEscapedString)
                "</h1><article>";
              (hamlet-0.7.1:Text.Hamlet.Quasi.htmlToHamletMonad
             . Text.Blaze.toHtml)
                (pageContent page);
              .... }'



And finally, both files can be found below, if it is necessary to look at them.


[Original]

<p>We're going to write a very simple AJAX application. It will be a
simple site with a few pages and a navbar; when you have Javascript,
clicking on the links will load the pages via AJAX. Otherwise, it will
use static HTML.</p>

<p>We're going to use jQuery for the Javascript, though anything would
work just fine. Also, the AJAX responses will be served as JSON. Let's
get started.</p>

> {-# LANGUAGE ScopedTypeVariables, TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses #-}
> import Yesod
> import Yesod.Helpers.Static
> import Data.Monoid (mempty)

Like the blog example, we'll define some data first.

> data Page = Page
>   { pageName :: String
>   , pageSlug :: String
>   , pageContent :: String
>   }

> loadPages :: IO [Page]
> loadPages = return
>   [ Page "Page 1" "page-1" "My first page"
>   , Page "Page 2" "page-2" "My second page"
>   , Page "Page 3" "page-3" "My third page"
>   ]

 loadPages :: IO [Page]
 loadPages = do

>
> data Ajax = Ajax
>   { ajaxPages :: [Page]
>   , ajaxStatic :: Static
>   }
> type Handler = GHandler Ajax Ajax

Next we'll generate a function for each file in our static folder.
This way, we get a compiler warning when trying to using a file which
does not exist.

> staticFiles "static/yesod/ajax"

Now the routes; we'll have a homepage, a pattern for the pages, and
use a static subsite for the Javascript and CSS files.

> mkYesod "Ajax" [$parseRoutes|
> /                  HomeR   GET
> /page/#String      PageR   GET
> /static            StaticR Static ajaxStatic
> |]

<p>That third line there is the syntax for a subsite: Static is the
datatype for the subsite argument; siteStatic returns the site itself
(parse, render and dispatch functions); and ajaxStatic gets the
subsite argument from the master argument.</p>

<p>Now, we'll define the Yesod instance. We'll still use a dummy
approot value, but we're also going to define a default layout.</p>

> instance Yesod Ajax where
>   approot _ = ""
>   defaultLayout widget = do
>   Ajax pages _ <- getYesod
>   content <- widgetToPageContent widget
>   hamletToRepHtml [$hamlet|
> \<!DOCTYPE html>
>
> <html>
>   <head>
>     <title>#{pageTitle content}
>     <link rel="stylesheet" href="@{StaticR style_css}">
>     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
>     <script src="@{StaticR script_js}">
>     \^{pageHead content}
>   <body>
>     <ul id="navbar">
>       $forall page <- pages
>         <li>
>           <a href="@{PageR (pageSlug page)}">#{pageName page}
>     <div id="content">
>       \^{pageBody content}
> |]

<p>The Hamlet template refers to style_css and style_js; these were
generated by the call to staticFiles above.  There's nothing
Yesod-specific about the <a
href="/static/yesod/ajax/style.css">style.css</a> and <a
href="/static/yesod/ajax/script.js">script.js</a> files, so I won't
describe them here.</p>

<p>Now we need our handler functions. We'll have the homepage simply
redirect to the first page, so:</p>

> getHomeR :: Handler ()
> getHomeR = do
>   Ajax pages _ <- getYesod
>   let first = head pages
>   redirect RedirectTemporary $ PageR $ pageSlug first

And now the cool part: a handler that returns either HTML or JSON
data, depending on the request headers.

> getPageR :: String -> Handler RepHtmlJson
> getPageR slug = do
>   Ajax pages _ <- getYesod
>   case filter (\e -> pageSlug e == slug) pages of
>       [] -> notFound
>       page:_ -> defaultLayoutJson (do
>           setTitle $ string $ pageName page
>           addHamlet $ html page
>           ) (json page)
>  where
>   html page = [$hamlet|
> <h1>#{pageName page}
> <article>#{pageContent page}
> |]
>   json page = jsonMap
>       [ ("name", jsonScalar $ pageName page)
>       , ("content", jsonScalar $ pageContent page)
>       ]

<p>We first try and find the appropriate Page, returning a 404 if it's
not there. We then use the applyLayoutJson function, which is really
the heart of this example. It allows you an easy way to create
responses that will be either HTML or JSON, and which use the default
layout in the HTML responses. It takes four arguments: 1) the title of
the HTML page, 2) some value, 3) a function from that value to a
Hamlet value, and 4) a function from that value to a Json value.</p>

<p>Under the scenes, the Json monad is really just using the Hamlet
monad, so it gets all of the benefits thereof, namely interleaved IO
and enumerator output. It is pretty straight-forward to generate JSON
output by using the three functions jsonMap, jsonList and jsonMap. One
thing to note: the input to jsonScalar must be HtmlContent; this helps
avoid cross-site scripting attacks, by ensuring that any HTML entities
will be escaped.</p>

<p>And now our typical main function. We need two parameters to build
our Ajax value: the pages, and the static loader. We'll load up from a
local directory.</p>

> main :: IO ()
> main = do
>
>   pages <- loadPages
>   let s = static "static/yesod/ajax"
>   warpDebug 3000 $ Ajax pages s


[My changes]
> {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses
>  #-}
> import Yesod
> import Yesod.Helpers.Static
> import System.Environment
> import System.IO
> import System.Directory
> import System.FilePath.Posix
> import Control.Applicative
> import Data.List.Split



> data Page = Page
>       { pageTitle :: String
>       , pageSlug :: String -- ^ used in the URL
>       , pageContent :: IO String
>       }



> loadPage :: IO [Page]
> loadPage = do
>  let directoryPath = "/home/mlitchard/playground/webTests/files"
>  let processedPath = map (directoryPath </>) . filter (`notElem` [".",".."])
>  pageFileNames <- processedPath <$> getDirectoryContents directoryPath
>  let pageFiles = map readFile pageFileNames
>  return $ zipWith popEntries pageFileNames pageFiles

-- >  return $ zipWith popEntries

> popEntries :: FilePath -> IO String -> Page
> popEntries pageFileName pageFile =
>   let pageT = last $ splitOn "/" pageFileName
>       pageS = "Job" ++ pageT
>   in  Page { Main.pageTitle=pageT,
>              pageSlug=pageS,
>              pageContent=pageFile }

> data Tframe = Tframe
>   { tframePages :: [Page]
>   , tframeStatic :: Static
>   }

> type Handler = GHandler Tframe Tframe

> staticFiles "static/yesod/ajax"

Routes

> mkYesod "Tframe" [$parseRoutes|
> /                    HomeR   GET
> /page/#String        PageR   GET
> /static              StaticR Static tframeStatic
> |]

defining the Yesod instance

> instance Yesod Tframe where
>   approot _ = ""
>   defaultLayout widget = do
>   Tframe pages <- getYesod
>   content <- widgetToPageContent widget
>   hamletToRepHtml [$hamlet|
> \<!DOCTYPE html>
>
> <html>
>   <head>
>     <title>#{Main.pageTitle content}
>     <link rel="stylesheet" href="@{StaticR style_css}">
>     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
>     <script src="@{StaticR script_js}">
>     \^{pageHead content}
>   <body>
>     <ul id="navbar">
>       $forall page <- pages
>         <li>
>           <a href="@{PageR (pageSlug page)}">#{Main.pageTitle page}
>     <div id="content">
>       \^{pageBody content}
> |]


> getHomeR :: Handler ()
> getHomeR = do
>   Tframe pages _ <- getYesod
>   let first = head pages
>   redirect RedirectTemporary $ PageR $ pageSlug first

> getPageR :: String -> Handler RepHtmlJson
> getPageR slug = do
>   Tframe pages _ <- getYesod
>   case filter (\e -> pageSlug e == slug) pages of
>       [] -> notFound
>       page:_ -> defaultLayoutJson (do
>           setTitle $ string $ Main.pageTitle page
>           addHamlet $ html page
>           ) (json page)
>  where
>   html page = [$hamlet|
> <h1>#{Main.pageTitle page}
> <article>#{pageContent page}
> |]

>   json page = jsonMap
>       [ ("name", jsonScalar $ Main.pageTitle page)
>       , ("content", jsonScalar $ liftIO $ pageContent page)
>       ]

> main :: IO ()
> main = do
>
>   pages <- loadPage
>   let s = static "static/yesod/ajax"
>   warpDebug 3000 $ Tframe pages s


If you've read to the bottom, thanks for your patience. I appreciate
any illumination you can send my way.


Michael



More information about the Beginners mailing list