I'm working through making a site using the snap framework for haskell, and I'm still new to haskell (and snap). I am hoping to find a "better" way to write this route handler.
possibleMatches :: Snap ()
possibleMatches = do
peptideSequence <- getParam "peptide_sequence"
foundWeight <- getParam "weight"
let results = calculationResults (read . C8.unpack $ (fromJust foundWeight)) (fromJust peptideSequence)
maybe (writeBS "must specify params in URL")
writeJSON $ Just (results)
There are a couple things here:
calculationResults
has signature :: Float -> ByteString
. I realize i have to do something to get peptideSequence
from Maybe ByteString
to ByteString
, so that seems necessary (and not terribly painful to do), butMaybe ByteString
to Float
seems a bit ridiculous. Is there a better way to handle this? Or is this just something that needs to be pushed down into the calculationResults
function, and have it handle the conversion?I suppose I'm trying to expand from my "learning of haskell in a bubble" to include how it's actually done, instead of hammering at the compiler until it finally gives up and says "fine i'll let it through".
Thanks in advance for your input!
A few things.
fromJust
is pretty evil. It's tantamount to unsafePerformIO
in the world of pure code. You're extracting a value out of a Maybe
monad w/o pattern matching.
fromJust :: Maybe a -> a
unsafePerformIO :: IO a -> a
> fromJust Nothing
*** Exception: Maybe.fromJust: Nothing
Now, odds are your HTML won't be manipulated maliciously so that those parameters will return Nothing
. BUT, in case that does happen you should use Snap's built in failure mechanism, it's Snaps implementation of fail
from the Monad class. It's safer than fromJust and is triggered by a pattern matching failure. You use it by pattern matching on getParam. (Just peptideSequence <- getParam "peptide_sequence"
)
instance Monad Snap where
(>>=) = snapBind
return = snapReturn
fail = snapFail
snapFail is implemented as
snapFail :: String -> Snap a
snapFail !m = Snap $! return $! PassOnProcessing m
PassOnProcessing
will handle pattern match failure gracefully.
More info is in the code:
http://hackage.haskell.org/package/snap-core-0.8.0.1/docs/src/Snap-Internal-Types.html
Side note:
All monads have a default implementation of fail, but if not overridden the result is often undesired. Any by undesired I mean it will throw an exception that can only be caught inside of the IO monad, but if you're not operating in the IO monad than you're straight out of luck. Snap has overridden the default implementation of fail.
From RWH:
Beware of fail. Many Monad instances don't override the default implementation of fail that we show here, so in those monads, fail uses error. Calling error is usually highly undesirable, since it throws an exception that callers either cannot catch or will not expect.
Even if you know that right now you're executing in a monad that has fail do something more sensible, we still recommend avoiding it. It's far too easy to cause yourself a problem later when you refactor your code and forget that a previously safe use of fail might be dangerous in its new context.
I still use it though, since it makes sense in this context (Unless a Snap author wants to correct me)
I would have the result of CalculationResults
return a JSON
result. I would also handle the Float
type conversion inside the CalculationResults
function, might make it cleaner
possibleMatches :: Snap ()
possibleMatches = do
Just peptideSequence <- getParam "peptide_sequence"
Just foundWeight <- getParam "weight"
writeJSON $ calculationResults (read $ C8.unpack foundWeight) peptideSequence
or
possibleMatches :: Handler App (AuthManager App) ()
possibleMatches = do
(peptideSequence, foundWeight) <- (,) <$> getParam "peptide_sequence" <*> getParam "weight"
writeJSON $ calculationResults (read $ C8.unpack foundWeight) peptideSequence
UPDATE:
For more robust error handling in Snap you can use the following code below:
catchError :: HasHeist b => ByteString -> Handler b v () -> Handler b v ()
catchError msg action = action `catch` \(e::SomeException) -> go
where go = do logError msg
modifyResponse $ setResponseCode 500
render "500"
Where "500" is the name of the file "500.tpl"
located in snaplets/heist/templates/500.tpl
To apply it to one of your handlers you would do something like this:
handleNewUser :: Handler App (AuthManager App) ()
handleNewUser = method GET handleGet <|> method POST handlePost
where
handleGet = currentUser >>= maybe the404 (\_ -> render "profile")
handlePost = catchError "Error during login" $ do
setTimeout 100
Just login <- getParam "login"
if | isValid login -> do user <- registerUser "login" "password"
either (\_ -> render "error") (handleUser login) user
| otherwise -> render "error"
handleUser = -- etc etc