what's the correct way to have template haskell wrap a function with source information (e.g. line number)

StackOverflow https://stackoverflow.com/questions/7073471

  •  17-12-2020
  •  | 
  •  

Question

Suppose I start with a function

fromJust Nothing = error "fromJust got Nothing!"
fromJust (Just x) = x

Then, I want to add source information via Template Haskell for better error messages. Let's imagine that I could add an extra parameter to the function

fromJust' loc Nothing = error $ "fromJust got Nothing at " ++ (loc_filename loc)
fromJust' loc (Just x) = x

and then have some fromJust macro that I could use in source code like,

x = $fromJust $ Map.lookup k m

hack

I did manage to hack it, by using quasiquotes and lifting the string of the source filename. It seems that Loc doesn't have a Lift instance. Is there a better way?

fromJustErr' l (Nothing) =
    error $ printf "[internal] fromJust error\
        \\n    (in file %s)" l
fromJustErr' l (Just x) = x
fromJustErr = do
    l <- location
    let fn = loc_filename l
        fnl :: Q Exp = TH.lift fn
    [| fromJustErr' $fnl |]

Thanks!

(I know it's nicer to fmap stuff via the Maybe functor than use fromJust, but I need to hack sometimes.)

Was it helpful?

Solution

Here's an attempt at making this pattern somewhat more reusable.

The key idea is to pass a customized error to our function which will include the location in the error message. You'd use it like this:

fromJust' :: (String -> a) -> Maybe a -> a
fromJust' error Nothing = error "fromJust got Nothing!"
fromJust' error (Just x) = x

fromJust :: Q Exp
fromJust = withLocatedError [| fromJust' |]

Using this function is similar to your original approach:

main = print (1 + $fromJust Nothing)

Now, for the Template Haskell that makes this work:

withLocatedError :: Q Exp -> Q Exp
withLocatedError f = do
    let error = locatedError =<< location
    appE f error

locatedError :: Loc -> Q Exp
locatedError loc = do
    let postfix = " at " ++ formatLoc loc
    [| \msg -> error (msg ++ $(litE $ stringL postfix)) |]

formatLoc :: Loc -> String
formatLoc loc = let file = loc_filename loc
                    (line, col) = loc_start loc
                in concat [file, ":", show line, ":", show col]

locatedError produces the customized error function, given a location. withLocatedError feeds this to fromJust' to hook everything together. formatLoc just formats the location nicely into a string.

Running this gives us the result we wanted:

FromJustTest: fromJust got Nothing! at FromJustTest.hs:5:19

OTHER TIPS

How about making a new error function?

locError :: Q Exp
locError = do
    loc <- location
    msgName <- newName "msg"
    eError <- [|error|]
    eCat <- [|(++)|]
    let
        locStr = loc_filename loc
        locLit = LitE (StringL locStr)
        pat    = VarP msgName
        body   = AppE eError locLit
    return $ LamE [pat] body

Then use it like

foo :: Int
foo = $(locError) "This is an error"

(It's incomplete -- doesn't give the message, just the file, but you get the idea)

EDIT

On re-reading your question, I realize this isn't what you're trying to do. It's an interesting idea -- you're trying to get caller location information -- sort of like a stack trace but only one layer deep. I have no idea how that would work.

Though I guess you could use the same trick of locError to make locFromJust -- but you wanted a general way, which this isn't.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top