Question

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:

  1. 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), but
  2. Converting from Maybe 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!

Was it helpful?

Solution

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top