haskellyesodyesod-forms

How to parse yesod-form parameters into Haskell values


The code below is from the Home.hs file created by the yesod-simple scaffold. I like to do simple string manipulation on text input but don't know how to parse it into a Text value. How, for example, can I use toUpper on fileDescription? I've tried using lookupPostParam but I'm struggling with it's type signature:

lookupPostParam :: MonadHandler m => Text -> m (Maybe Text)

Home.hs

module Handler.Home where

import Import
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))

data FileForm = FileForm
    { fileInfo :: FileInfo
    , fileDescription :: Text
    }

getHomeR :: Handler Html
getHomeR = do
    (formWidget, formEnctype) <- generateFormPost sampleForm
    let submission = Nothing :: Maybe FileForm
        handlerName = "getHomeR" :: Text
    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

postHomeR :: Handler Html
postHomeR = do
    ((result, formWidget), formEnctype) <- runFormPost sampleForm
    let handlerName = "postHomeR" :: Text
        submission = case result of
            FormSuccess res -> Just res
            _ -> Nothing

    defaultLayout $ do
        let (commentFormId, commentTextareaId, commentListId) = commentIds
        aDomId <- newIdent
        setTitle "Welcome To Yesod!"
        $(widgetFile "homepage")

sampleForm :: Form FileForm
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
    <$> fileAFormReq "Choose a file"
    <*> areq textField textSettings Nothing
    where textSettings = FieldSettings
            { fsLabel = "What's on the file?"
            , fsTooltip = Nothing
            , fsId = Nothing
            , fsName = Nothing
                 , fsAttrs =
                    [ ("class", "form-control")
                    , ("placeholder", "File description")
                    ]
            }

commentIds :: (Text, Text, Text)
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-
commentList")

Solution

  • This is unfortunately a fault in documentation and communication.

    Given

    lookupPostParam :: (MonadResource m, MonadHandler m) => Text -> m (Maybe Text)
    

    the reader is meant to infer that m is not only a MonadResouce and a MonadHandler but also Monad. This tiny little line of code packs up a lot of intent into a very small sentence; it's a wart that so much of Haskell library usage is left implicit and subtextual. For example, to call toUpper on the Text inside this type you are meant to do this:

    {-# language OverloadedStrings #-}
    foo :: (MonadResource m, MonadHandler m) => m (Maybe Text)
    foo = do
      valueMaybe <- lookupPostParam "key"
      case valueMaybe of
        Just value ->
          pure (toUpper value)
        Nothing ->
          Nothing
    

    Note that the monad stack (MonadHandler, MonadResource) has "infected" your code. This is meant to be intentional, so as to constrain you via the type checker to only run this function in the intended Yesod environment/state machine/context/whatever.

    However

    You are using yesod-forms and it would be nice to do the same thing within that framework. As with lookupPostParam, we can take advantage of the monad-applicative-functor typeclasses.

    We can adapt this to the Form FileForm value that you have.

    sampleForm :: AForm Handler FileForm
    sampleForm =
      FileForm <$> fileAFormReq "Choose a file"
               <*> (toUpper <$> areq textField textSettings Nothing)
    

    I think the types of yesod-forms changed between releases. I'm copying my types off the latest version as of writing, 1.4.11.

    Here we take advantage of the Monad m => Functor (AForm m) instance. Knowing that we are indeed in a monad (the Handler monad) means we can use fmap and its infixed sibling <$> on the value returned by areq textField textSettings Nothing. This allows us to lift arbitrary functions acting on Text into the AForm m stack. For example, here we went from Text -> Text to AForm Handler Text -> AForm Handler Text.

    Hope that helps.