Some Things about Yesod


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.

Removing Potential Runtime Error

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.

TL;DR

Yesod removes potential runtime error by relying on Haskell's strong type system.

Managing Authentication and Authorisation

Authentication

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.

Authorisation

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:

  1. Getting user's id, name, and group. We do it using getUserAndGrouping function that interacts to database.
  2. Defining what can be done by the user. For example, allowedToAdmin function which filter non-Administrator.
  3. Using the function from the previous step. For example, 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.

Database Interaction

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.



This material is shared under the CC-BY License.