I got a few questions about Yesod and its advantages. Because of time constraints (a lot of things that need to be done), let's cut the chase.
Runtime error removal is not a Yesod specific feature because Yesod is written and developed in Haskell. Even when using this framework, one should use Haskell and its pros and cons. For example, immutable data, lazy evaluation, pure computation, and strong and static typing.
To give a clear cut example, let's see a real world example from stackoverflow.
The problem in question was caused by a mismatched type, where id
should not
be a nullable thing, but the method that should provide id
didn't consider about.
Now, if that part of the project should be written in Yesod, it will resemble like the following snippet:
data Post = Post
{ pId :: Int
, pUserId :: Int
, pContent :: String
-- etc
}
data FormPost = FormPost
{ fpUserId :: Int
, fpContent :: String
}
getUserId :: Handler Int
getUserId = do
maut <- maybeAuth -- this is a function to get the authentication status.
case maut of
Nothing -> permissionDenied "you are not allowed to something."
Just uid -> pure uid
formPost :: Int -> Form FormPost
formPost userid = renderDivs $ FormPost <$> areq textField "userid" (Just userid) <*> areq textField "content" Nothing
getPostR :: Handler Html
getPostR = do
userid <- getUserId
(form, encodingtype) <- generateFormPost $ formPost userid
defaultLayout form
Because Handler
is a shortcutting computational context, whenever an unauthorised
user want to access /post/
via GET
request, we don't even have to execute
generateFormPost $ formPost userid
(which is the equivalent of respond_to do |format| format.html
)
because the result getUserId
is not an Int
or ID of a User.
If we force Yesod's flow to follow that stackoverflow question's flow like the following:
getPostR :: Handler Html
getPostR = do
userid <- maybeAuth
(form, encodingtype) <- generateFormPost $ formPost userid
defaultLayout form
Won't even compile because formPost
only accept argument which is an Int
while userid
from the result of maybeAuth
is Maybe Int
.
Yesod removes potential runtime error by relying on Haskell's strong type system.
By design, Yesod is a framework that consists from a few libraries. Not only this decision leads to a modular framework but also eases the adoption of plugin system.
For example, in another post of this blog, that
project uses a plugin for yesod-auth
package
which provides securely hashed password that is stored in database.
To reduce repetition, let's review Authentication, Prelude
section in that post.
HashDBUser
for User
. Why we should do that?
Because we have to define what are userPasswordHash
(what should be hashed)
and setPasswordHash
(how to replace plain text password with a hashed one).App
an instance of YesodAuth
. The most crucial part
of this step is the authenticate creds
line. That part is where we determine
whether a request with a certain session cookies matches with the information
in the database.Still using the same blog post, we have a few user groups:
where whatever Normal user can do, Moderator can do it too. But not the other way around. Same goes with what Moderator can do. Administrator can do it too but not the other way around. In other words, we make a clear definitions of authorisation based on user groups.
And we achieve that by using the following steps:
getUserAndGrouping
function that interacts to database.allowedToAdmin
function
which filter non-Administrator.getAdmCategoryR
function
which is a handler for request to GET
category administration page. Here,
Only request which has session cookies that matches to the user who are
in Administrator group can be processed further.
Otherwise, they will be greeted by 401 unauthorised HTTP status page.You can look at moot project by Chris. Allen if you want to see more refined authorisation process.
Let's step back a little. In Yesod (or Haskell in general) whenever a computation
related to outside world is executed, we have to provide a matching computational
context (monad) to it.
For example, when handling a GET
request to, let's say, /aoeui
we have to
process that request in HandlerFor
computational monad. Same goes with database
interaction, we may need a different context when we are executing sql query.
Not only that, we can't execute a piece of code that have computational
context in a block which have different computational context with the said piece.
To give you a concrete example, let's start with registerUser
function from
the previous blog post where we get current time, group id, hash password, etc.
registerUser :: Text -> Text -> Text -> Handler (Key Users)
registerUser username password email = do
now <- liftIO getCurrentTime
gids <- liftHandler $ runDb $ selectGroupByGrouping Member
-- snip...
As you can see, there are two functions that its computational context need to be
"lifted", getCurrentTime
and runDb ...
.
getCurrentTime
is used to get current time by talking to the host system while
runDb ...
is used to get group ids by talking to database backend and where we
will talk about it in a moment.
Both of them have different computational context that is IO
and HandlerFor site a
(where site
and a
is just a generic type that can be replaced by anything)
respectively. And, by using liftIO
and liftHandler
, we can change (or perhaps
even wrap) their computational context into Handler
.
Now, let's disect runDb
first by inspecting its type signature.
runDb :: YesodDB site a -> HandlerFor site a
The signature above means that runDb1
takes a value which has YesodDB site a
and returns HandlerFor site a
. Before we step further, let me remind you that
you can use stackage to check the documentation of haskell
code.
Now, let's look at selectGroupByGrouping
function's type signature.
selectGroupByGrouping :: Grouping -> DB [Entity Group]
type DB a = forall (m :: * -> *). (MonadIO m) => ReaderT SqlBackend m a
I will refrain myself from discussing ReaderT SqlBackend m a
further than
it is what persistent
and esqueleto
's computational context.
Now, it's getting clearer, I guess. So, whenever we want to execute a database
query in Yesod's computational context, we should "lift" that query's context using
functions which start with lift
so it matches with Yesod's.
To recap.
used in Yesod's computational context.
^
|
"lift"ed using liftHandler
|
runDB ------- has context ---- HandlerFor
^
|
taken as param by
|
persistent/esqueleto query ---- has context --- ReaderT SqlBackend m a
Hope it helps. And if you have further questions, you can reach me by looking at the footer.