Search code examples
haskellroutesboomerang

web-routes-boomerang with simple algebraic data type


I'm a little mixed up about how to properly use boomerang to generate URLs. I have the following:

data State =
  AK | AL | AR | AZ | CA ... WY

data Sitemap
    = Home
    | State State
    | Place State String
      deriving (Eq, Ord, Read, Show, Data, Typeable)

$(derivePrinterParsers ''Sitemap)

sitemap ∷ Router Sitemap
sitemap =
    (  rHome
    <> rState . state
    <> rPlace . (state </> anyString)
    )

state :: PrinterParser StringsError [String] o (State :- o)
state = xmaph read (Just . show) anyString

This seems to work, but when I compare my implementation of state with the one in the documentation for articleId, they seem to be working in opposite fashions:

articleId :: Router ArticleId
articleId = xmaph ArticleId (Just . unArticleId) int

The types are totally different and look like they're going in opposite directions, but my sitemap works and the app correctly handles URLs. I think it should look more like this:

maybeState :: String → Maybe State
maybeState stateString = case reads stateString of
                     [(state, "")] -> Just state
                     _             -> Nothing

stateR :: Router State
stateR = xpure show maybeState

This doesn't type-check, but even substituting undefined for its definition, in sitemap above, rState . stateR would work, but rPlace . (stateR </> anyString) doesn't.

Seems like this would come up often enough there's probably a library function to take care of this for me, but I didn't see one.

Edit: here are some of the type errors I get:

For state = xpure show maybeState:

Main.hs:56:16:
    Couldn't match expected type `State :- ()'
                with actual type `[Char]'
    Expected type: () -> State :- ()
      Actual type: () -> String
    In the first argument of `xpure', namely `show'
    In the expression: xpure show maybeState

For state = undefined :: Router State (this error is in the sitemap definition):

Main.hs:45:18:
    Couldn't match expected type `String :- ()' with actual type `()'
    Expected type: PrinterParser
                     StringsError [String] () (State :- (String :- ()))
      Actual type: Router State
    In the first argument of `(</>)', namely `state'
    In the second argument of `(.)', namely `(state </> anyString)'

Solution

  • The types look different because you use of state in the rPlace line requires a more general type signature than the Router type alias permits. (Your code is fine. But perhaps we should offer a more general alias in boomerang though..)

    If you remove the rPlace line you can change the type signature of state to:

    state :: Router State
    state = xmaph read (Just . show) anyString
    

    If you look more closely, I think you will see that state and articleId actually do go the same direction.

    articleId :: Router ArticleId
    articleId = xmaph ArticleId (Just . unArticleId) int
    

    The third argument of xmaph specifies how to parse some underlying value. In the case of articleId it parses an int and for state it parses anyString.

    The first argument of xmaph specifies how to convert that value to the desired return type. In articleId we simple apply the ArticleId constructor. In state we apply the read function. But in both cases we are going from the underlying value to the desired return type:

    ArticleId :: Int    -> ArticleId
    read      :: String -> State
    

    The second argument to xmaph specifies how to convert the return type back to the underlying value.

    show        :: State     -> String
    unArticleId :: ArticleId -> Int
    

    That said, we should not actually be using 'read' here anyway because 'read' could potentially fail and through an error. It is intended that the first argument to xmaph will be a total function.

    I uploaded boomerang 1.3.1 which adds a new combinator to the Strings module named readshow. This function uses the Read and Show instances correctly. Unfortunately, error reporting is a bit sloppy since when reads fails it tells us nothing about why or where it failed. But it's better than nothing :)

    Using that you could now write:

    state :: PrinterParser StringsError [String] o (State :- o)
    state = readshow
    

    if we supply an invalid state we now get:

    > parseStrings sitemap ["AZ"]
    Right (State AZ)
    > parseStrings sitemap ["FOEU"]
    Left parse error at (0, 0): unexpected FOEU; decoding using 'read' failed.