This is a braindump during my development of a Yesod subsite a month or two ago. It was surprisingly hard to find decent examples that actually worked, and many of the extremely obscure error messages were hard to solve. The source code can be found here. So, I hope that someone finds this useful.
First off, ensure your IDE automatically changes tabs to spaces. 1 tab = 4 spaces.
Second, I googled “yesod subsite” and got this: http://www.yesodweb.com/book/creating-a-subsite.
Where do I place the files? No idea. Let’s google a little more. Nope, no results. I guess I’ll place the new subsite HelloSub.hs
in my newly scaffolded site.
HelloSub.Data
goes in HelloSub/Data.
HelloSub.hs:16:67: Not in scope: type constructor or class `IO'
Off to a good start.
import Prelude (IO)
HelloSub.hs:17:20: Not in scope: `$'
Nope. Let’s try:
import Prelude (IO,($))
Ok, that compiles.
The example seems to overwrite the routes. I guess I’ll copy-paste that into the route config file. Would be nice if there was a tutorial that started from the scaffolded site.
Application.hs:60:36: Warning:
Fields of `App' not initialised: getHelloSub
In the expression: App {..}
In an equation for `mkFoundation':
mkFoundation appConnPool = App {..}
In the expression:
do { appHttpManager <- newManager;
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger;
appStatic <- (if appMutableStatic appSettings then
staticDevel
else
static)
(appStaticDir appSettings);
let mkFoundation appConnPool = ...
tempFoundation
= mkFoundation $ error "connPool forced in tempFoundation"
....;
.... }
Man, I hope that doesn’t break things.
ghc: C:\yesod\datasource\dist\build\HSdatasource-0.0.0.o: unknown symbol `datasourcezm0zi0zi0_HelloSub_zdfYesodSubDispatchHelloSubHandlerT_closure'
Man, it broke things. I guess I’ll google “yesod unknown symbol”. Result: http://stackoverflow.com/questions/11485555/build-failure-during-yesod-devel
Hmm:
“Unknown symbol errors when running yesod devel are often the result of failing to include a module in exposed-modules or other-modules in your application’s cabal file.”
Guess I’ll add the modules to the .cabal file?
Woo, that worked!
Ok, let’s actually start development now.
Let’s try out some form input. http://www.yesodweb.com/book/forms
Copy over a whole bunch of stuff.
This is pretty straightforward, but I haven’t looked at the compilation log yet :D
HelloSub\Data.hs:18:31:
Not in scope: type constructor or class `Int'
Perhaps you meant `Ints' (imported from Yesod)
HelloSub\Data.hs:19:32:
Not in scope: type constructor or class `Int'
Perhaps you meant `Ints' (imported from Yesod)
HelloSub\Data.hs:21:12:
Not in scope: type constructor or class `Show'
Man, this just gets better and better. Doesn’t Yesod use classy-prelude
?
import ClassyPrelude
Ok, fixed.
HelloSub.hs:25:25:
Not in scope: type constructor or class `Handler'
Perhaps you meant `HandlerT' (imported from Yesod)
You tell me!
s/Handler/HandlerT
That works.
HelloSub.hs:19:26:
Not in scope: data constructor `DataSourceInputR'
Perhaps you meant `DataSourceInput' (imported from HelloSub.Data)
I already forgot why this was the case.
Oh right, the mkYesodSubData
thing.
mkYesodSubData "HelloSub" [parseRoutes|
/ SubHomeR GET
/datasource DataSourceInputR POST
|]
HelloSub.hs:29:58: Not in scope: `show'
DAMMIT
Replace import Prelude (IO,($))
with ClassyPrelude
.
HelloSub.hs:25:25:
Expecting two more arguments to `HandlerT Html'
Expected a type, but `HandlerT Html' has kind `(* -> *) -> * -> *'
In the type signature for `postDataSourceInputR':
postDataSourceInputR :: HandlerT Html
Woo, a kind error, something I can probably fix!
currently: postDataSourceInputR :: HandlerT Html
I guess I’ll want something like the example HelloSub thing: getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
Let’s do that.
HelloSub.hs:30:14:
Couldn't match type `IO' with `HandlerT master IO'
Expected type: HandlerT HelloSub (HandlerT master IO) Html
Actual type: HandlerT HelloSub IO Html
Relevant bindings include
postDataSourceInputR :: HandlerT HelloSub (HandlerT master IO) Html
(bound at HelloSub.hs:26:1)
In the expression:
defaultLayout
(do { (asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"<p>Invalid input, let's try again.</p>\n\
\<form method=\"post\" action=\"");
(getUrlRenderParams
>>=
(\ urender_antS
-> (asWidgetT GHC.Base.. toWidget)
(toHtml (\ u_antT -> urender_antS u_antT [] DataSourceInputR))));
(asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"\" enctype=\"");
(asWidgetT GHC.Base.. toWidget) (toHtml enctype);
(asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"\">");
.... })
In a case alternative:
_ -> defaultLayout
(do { (asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"<p>Invalid input, let's try again.</p>\n\
\<form method=\"post\" action=\"");
(getUrlRenderParams
>>=
(\ urender_antS
-> (asWidgetT GHC.Base.. toWidget)
(toHtml (\ u_antT -> urender_antS u_antT [] DataSourceInputR))));
(asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"\" enctype=\"");
(asWidgetT GHC.Base.. toWidget) (toHtml enctype);
JESUS
Couldn't match type `IO' with `HandlerT master IO'
Expected type: HandlerT HelloSub (HandlerT master IO) Html
Oh ok
Let’s fix that:
postDataSourceInputR :: HandlerT HelloSub IO Html
HelloSub.hs:39:26:
Couldn't match type `IO' with `HandlerT parent1 IO'
Expected type: HandlerT HelloSub (HandlerT parent1 IO) Html
Actual type: HandlerT HelloSub IO Html
Relevant bindings include
env6990_ansz :: Yesod.Core.Types.YesodSubRunnerEnv
HelloSub parent1 (HandlerT parent1 IO)
(bound at HelloSub.hs:39:26)
inner :: Yesod.Core.Types.YesodSubRunnerEnv
HelloSub parent1 (HandlerT parent1 IO)
-> wai-3.0.2.3:Network.Wai.Internal.Request
-> (wai-3.0.2.3:Network.Wai.Internal.Response
-> IO wai-3.0.2.3:Network.Wai.Internal.ResponseReceived)
-> IO wai-3.0.2.3:Network.Wai.Internal.ResponseReceived
(bound at HelloSub.hs:39:26)
In the first argument of `yesod-core-1.4.8.3:Yesod.Core.Class.Dispatch.subHe
lper
GHC.Base.. (fmap toTypedContent)', namely
`postDataSourceInputR'
In the expression:
(yesod-core-1.4.8.3:Yesod.Core.Class.Dispatch.subHelper
GHC.Base.. (fmap toTypedContent))
postDataSourceInputR
env6990_ansz
(Just DataSourceInputR)
req6990_ansA
Damn, this is in Template Haskell.
I’ll google for YesodSubDispatch
.
http://www.yesodweb.com/blog/2013/03/big-subsite-rewrite
Ooh, examples.
Ok, so I guess I’ll go back to Yesod master => HandlerT HelloSub (HandlerT master IO) Html
.
Ah, it’s a type error. I’ll replace defaultLayout
with lift $ defaultLayout
in the handlers.
HelloSub.hs:19:33:
Could not deduce (master ~ HelloSub)
from the context (Yesod master)
bound by the type signature for
getSubHomeR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:16:16-74
`master' is a rigid type variable bound by
the type signature for
getSubHomeR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:16:16
Expected type: WidgetT
master IO (Route HelloSub -> [(Text, Text)] -> Text)
Actual type: WidgetT
master
IO
(Route (HandlerSite (WidgetT master IO)) -> [(Text, Text)] -> Text)
Relevant bindings include
getSubHomeR :: HandlerT HelloSub (HandlerT master IO) Html
(bound at HelloSub.hs:17:1)
In the first argument of `(>>=)', namely `getUrlRenderParams'
In a stmt of a 'do' block:
(getUrlRenderParams
>>=
(\ urender_aneD
-> (asWidgetT GHC.Base.. toWidget)
(toHtml (\ u_aneE -> urender_aneD u_aneE [] DataSourceInputR)))
)
HelloSub.hs:31:22:
Could not deduce (master ~ HelloSub)
from the context (Yesod master)
bound by the type signature for
postDataSourceInputR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:25:25-83
`master' is a rigid type variable bound by
the type signature for
postDataSourceInputR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:25:25
Expected type: WidgetT
master IO (Route HelloSub -> [(Text, Text)] -> Text)
Actual type: WidgetT
master
IO
(Route (HandlerSite (WidgetT master IO)) -> [(Text, Text)] -> Text)
Relevant bindings include
postDataSourceInputR :: HandlerT HelloSub (HandlerT master IO) Html
(bound at HelloSub.hs:26:1)
In the first argument of `(>>=)', namely `getUrlRenderParams'
In a stmt of a 'do' block:
(getUrlRenderParams
>>=
(\ urender_anjY
-> (asWidgetT GHC.Base.. toWidget)
(toHtml (\ u_anjZ -> urender_anjY u_anjZ [] DataSourceInputR)))
Man, this just gets better and better, doesn’t it. TH is being a pain in my ASS.
Snoyman has a wiki example in a Github, let’s check that out : https://github.com/yesodweb/yesod/blob/new-subsite/demo/WikiRoutes.hs
I have no clue what the problem is.
Ok, screw it, let’s just go back to the basics:
postDataSourceInputR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
postDataSourceInputR = do lift $ defaultLayout [whamlet|test|]
-- And we'll spell out the handler type signature.
getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
getSubHomeR = do lift $ defaultLayout [whamlet|test|]
Ok, that compiles.
getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
getSubHomeR = do
(widget, enctype) <- generateFormPost simpleSourceForm
lift $ defaultLayout [whamlet|
<form method=post action=@{DataSourceInputR} enctype=#{enctype}>
^{widget}
<button>Submit
|]
simpleSourceForm = renderDivs $ DataSourceInput
<$> areq intField "Start" Nothing
<*> areq intField "End" Nothing
That doesn’t:
HelloSub.hs:25:33:
Could not deduce (master ~ HelloSub)
from the context (Yesod master)
bound by the type signature for
getSubHomeR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:22:16-74
`master' is a rigid type variable bound by
the type signature for
getSubHomeR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:22:16
Expected type: WidgetT
master IO (Route HelloSub -> [(Text, Text)] -> Text)
Actual type: WidgetT
master
IO
(Route (HandlerSite (WidgetT master IO)) -> [(Text, Text)] -> Text)
Relevant bindings include
getSubHomeR :: HandlerT HelloSub (HandlerT master IO) Html
(bound at HelloSub.hs:23:1)
In the first argument of `(>>=)', namely `getUrlRenderParams'
In a stmt of a 'do' block:
(getUrlRenderParams
>>=
(\ urender_anFs
-> (asWidgetT GHC.Base.. toWidget)
(toHtml (\ u_anFt -> urender_anFs u_anFt [] DataSourceInputR))))
Stupid equality constraints.
MORE GOOGLING
https://github.com/yesodweb/yesod/wiki/Intra-subsite-links
AHA
getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
getSubHomeR = do
(widget, enctype) <- generateFormPost simpleSourceForm
toMaster <- getRouteToParent
lift $ defaultLayout [whamlet|
<form method=post action=@{toMaster DataSourceInputR} enctype=#{enctype}>
^{widget}
<button>Submit
|]
Dammit:
HelloSub.hs:26:33:
Could not deduce (master ~ HelloSub)
from the context (Yesod master)
bound by the type signature for
getSubHomeR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:22:16-74
`master' is a rigid type variable bound by
the type signature for
getSubHomeR :: Yesod master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:22:16
Relevant bindings include
toMaster :: Route HelloSub -> Route master
(bound at HelloSub.hs:25:3)
getSubHomeR :: HandlerT HelloSub (HandlerT master IO) Html
(bound at HelloSub.hs:23:1)
In the second argument of `(GHC.Base..)', namely `toWidget'
In the expression: asWidgetT GHC.Base.. toWidget
In a stmt of a 'do' block: (asWidgetT GHC.Base.. toWidget) widget
Reduce example again:
getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
getSubHomeR = do
toMaster <- getRouteToParent
lift $ defaultLayout [whamlet|<a href=@{toMaster DataSourceInputR}> |]
YES THAT WORKS
Internal Server Error
Application.hs:60:36-43: Missing field in record construction Foundation.getHelloSub
NO IT DOESNT
This probably has to do with the warning message it keeps puking out:
Application.hs:60:36: Warning:
Fields of `App' not initialised: getHelloSub
In the expression: App {..}
In an equation for `mkFoundation':
mkFoundation appConnPool = App {..}
In the expression:
do { appHttpManager <- newManager;
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger;
appStatic <- (if appMutableStatic appSettings then
staticDevel
else
static)
(appStaticDir appSettings);
let mkFoundation appConnPool = ...
tempFoundation
= mkFoundation $ error "connPool forced in tempFoundation"
....;
.... }
How do I initialize getHelloSub
? Is that even a solution?
Screw it, I’ll contact Snoyberg.
Snoyberg contacted.
the problem is that you haven’t populated the
getHelloSub
field in themakeFoundation
function.
Ok.
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
-- Some basic initializations: HTTP connection manager, logger, and static
-- subsite.
appHttpManager <- newManager
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
appStatic <-
(if appMutableStatic appSettings then staticDevel else static)
(appStaticDir appSettings)
getHelloSub <- return HelloSub
-- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a
-- logging function. To get out of this loop, we initially create a
-- temporary foundation without a real connection pool, get a log function
-- from there, and then create the real foundation.
let mkFoundation appConnPool = App {..}
tempFoundation = mkFoundation $ error "connPool forced in tempFoundation"
logFunc = messageLoggerSource tempFoundation appLogger
-- Create the database connection pool
pool <- flip runLoggingT logFunc $ createSqlitePool
(sqlDatabase $ appDatabaseConf appSettings)
(sqlPoolSize $ appDatabaseConf appSettings)
-- Perform database migration using our application's logging settings.
runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc
-- Return the foundation
return $ mkFoundation pool
There we go.
It works now. Nice. Let’s add the form stuff now.
getDataSourceInputR :: HelloSubHandler Html
getDataSourceInputR = do
toMaster <- getRouteToParent
(widget, enctype) <- generateFormPost simpleSourceForm
lift $ defaultLayout
[whamlet|
<p>
The widget generated contains only the contents
of the form, not the form tag itself. So...
<form method=post action=@{toMaster DataSourceInputR} enctype=#{enctype}>
^{widget}
<p>It also doesn't include the submit button.
<button>Submit
|]
Added the form stuff, have the equality constraint problem again:
HelloSub.hs:27:22:
Could not deduce (master ~ HelloSub)
from the context (YesodHelloSub master)
bound by the type signature for
getDataSourceInputR :: YesodHelloSub master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:22:24-43
`master' is a rigid type variable bound by
the type signature for
getDataSourceInputR :: YesodHelloSub master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:19:33
Relevant bindings include
toMaster :: Route HelloSub -> Route master
(bound at HelloSub.hs:24:3)
getDataSourceInputR :: HandlerT HelloSub (HandlerT master IO) Html
(bound at HelloSub.hs:23:1)
In the second argument of `(GHC.Base..)', namely `toWidget'
In the expression: asWidgetT GHC.Base.. toWidget
In a stmt of a 'do' block: (asWidgetT GHC.Base.. toWidget) widget
Let’s look at Snoyman’s wiki example again: https://github.com/yesodweb/yesod/blob/new-subsite/demo
Let’s add the type alias, it’s nice.
class (RenderMessage master FormMessage, Yesod master) => YesodHelloSub master where
dummyThing :: HandlerT master IO Bool
dummyThing = return True
type HelloSubHandler a = forall master. YesodHelloSub master
=> HandlerT HelloSub (HandlerT master IO) a
instance YesodHelloSub master => YesodSubDispatch HelloSub (HandlerT master IO) where
yesodSubDispatch = $(mkYesodSubDispatch resourcesHelloSub)
-- In Foundation.hs
instance YesodHelloSub App
(might be fine to put that elsewhere? Who knows.)
getSubHomeR :: HelloSubHandler Html
Ooh, squeaky clean. Didn’t fix the equality constraint type error, though.
It’s got something to do with the widget, apparently? Screw it, I’ll just lift it.
(widget, enctype) <- lift $ generateFormPost simpleSourceForm
WOO THAT WORKED
Ok, let’s introduce the POST part.
postDataSourceInputR :: HelloSubHandler Html
postDataSourceInputR = do
toMaster <- getRouteToParent
((result, widget), enctype) <- runFormPost simpleSourceForm
case result of
FormSuccess datasource -> lift $ defaultLayout [whamlet|<p>#{show datasource}|]
_ -> lift $ defaultLayout
[whamlet|<a href=@{toMaster SubHomeR}> |]
HelloSub.hs:52:36:
Could not deduce (RenderMessage HelloSub FormMessage)
arising from a use of `runFormPost'
from the context (YesodHelloSub master)
bound by the type signature for
postDataSourceInputR :: YesodHelloSub master =>
HandlerT HelloSub (HandlerT master IO) Html
at HelloSub.hs:49:25-44
In a stmt of a 'do' block:
((result, widget), enctype) <- runFormPost simpleSourceForm
In the expression:
do { toMaster <- getRouteToParent;
((result, widget), enctype) <- runFormPost simpleSourceForm;
case result of {
FormSuccess datasource
-> lift
$ defaultLayout
(do { (asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"<p>");
.... })
_ -> lift
$ defaultLayout
(do { (asWidgetT GHC.Base.. toWidget)
((blaze-markup-0.7.0.2:Text.Blaze.Internal.preEscapedText
GHC.Base.. Data.Text.pack)
"<a href=\"");
.... }) } }
In an equation for `postDataSourceInputR':
postDataSourceInputR
= do { toMaster <- getRouteToParent;
((result, widget), enctype) <- runFormPost simpleSourceForm;
case result of {
FormSuccess datasource -> lift $ defaultLayout (do { ... })
_ -> lift $ defaultLayout (do { ... }) } }
Ooh boy.
Hmm: RenderMessage HelloSub FormMessage
Sounds like we need to lift it to the master implementation again.
((result, widget), enctype) <- lift $ runFormPost simpleSourceForm
Works! (finally!)
Let’s make some repos for each package and see how horrible it all plays with cabal sandboxes.
Yesod doesn’t like to play with cabal sandboxes AT ALL. How do I pass sandbox arguments to yesod devel
?
yesod devel -e "--sandbox-config-file=../sandbox/cabal.sandbox.config"
Yesod devel server. Press ENTER to quit
cabal: unrecognized 'configure' option
`--sandbox-config-file=../sandbox/cabal.sandbox.config'
Nope.
yesod devel -e "--sandbox-config-file=\"../sandbox/cabal.sandbox.config\""
Yesod devel server. Press ENTER to quit
cabal: unrecognized 'configure' option
`--sandbox-config-file="../sandbox/cabal.sandbox.config"'
cabal: unrecognized 'configure' option
`--sandbox-config-file="../sandbox/cabal.sandbox.config"'
Nope.
cabal help sandbox
Usage: cabal sandbox init
or: cabal sandbox delete
or: cabal sandbox add-source [PATHS]
or: cabal sandbox hc-pkg -- [ARGS]
or: cabal sandbox list-sources
Flags for sandbox:
-h --help Show this help text
-v --verbose[=n] Control verbosity (n is 0--3, default verbosity level is 1)
--snapshot Take a snapshot instead of creating a link (only applies to
'add-source')
--sandbox=DIR Sandbox location (default: './.cabal-sandbox').
Perhaps…
>yesod devel -e "--sandbox=../sandbox/.cabal-sandbox"
Yesod devel server. Press ENTER to quit
cabal: unrecognized 'configure' option `--sandbox=../sandbox/.cabal-sandbox'
Nope.
yesod devel -e "sandbox --sandbox=../sandbox/.cabal-sandbox"
Yesod devel server. Press ENTER to quit
Resolving dependencies...
cabal: Unrecognised flags: sandbox --sandbox=../sandbox/.cabal-sandbox
Nope.
Google?
http://blog.pangyanhan.com/haskell/2013-11-31-using-cabal-sandbox-libraries.html
Nope.
There is still a lot to be done to make Yesod development productive without boatloads of yak shaving. It was very hard to find examples of the functionality I wanted, or even a usable (and working with the latest stable release!) list of Yesod subsites.