Search code examples
haskelltemplate-haskell

Is there a (Template) Haskell library that would allow me to print/dump a few local bindings with their respective names?


For instance:

let x = 1 in putStrLn [dump|x, x+1|]

would print something like

x=1, (x+1)=2

And even if there isn't anything like this currently, would it be possible to write something similar?


Solution

  • TL;DR There is this package which contains a complete solution.

    Example usage:

    {-# LANGUAGE QuasiQuotes #-}
    
    import Debug.Dump
    
    main = print [dd|a, a+1, map (+a) [1..3]|]
      where a = 2
    

    which prints:

    (a) = 2   (a+1) = 3       (map (+a) [1..3]) = [3,4,5]
    

    by turnint this String

    "a, a+1, map (+a) [1..3]"
    

    into this expression

    ( "(a) = " ++ show (a)            ++ "\t  " ++
      "(a+1) = " ++ show (a + 1)      ++ "\t  " ++
      "(map (+a) [1..3]) = " ++ show (map (+ a) [1 .. 3])
    )
    

    Background

    Basically, I found that there are two ways to solve this problem:

    1. Exp -> String The bottleneck here is pretty-printing haskell source code from Exp and cumbersome syntax upon usage.
    2. String -> Exp The bottleneck here is parsing haskell to Exp.

    Exp -> String

    I started out with what @kqr put together, and tried to write a parser to turn this

    ["GHC.Classes.not x_1627412787 = False","x_1627412787 = True","x_1627412787 GHC.Classes.== GHC.Types.True = True"]
    

    into this

    ["not x = False","x = True","x == True = True"]
    

    But after trying for a day, my parsec-debugging-skills have proven insufficient to date, so instead I went with a simple regular expression:

    simplify :: String -> String
    simplify s = subRegex (mkRegex "_[0-9]+|([a-zA-Z]+\\.)+") s ""
    

    For most cases, the output is greatly improved. However, I suspect this to likely mistakenly remove things it shouldn't.

    For example:

    $(dump [|(elem 'a' "a.b.c", True)|])
    

    Would likely return:

    ["elem 'a' \"c\" = True","True = True"]
    

    But this could be solved with proper parsing.

    Here is the version that works with the regex-aided simplification: https://github.com/Wizek/kqr-stackoverflow/blob/master/Th.hs

    Here is a list of downsides / unresolved issues I've found with the Exp -> String solution:

    • As far as I know, not using Quasi Quotation requires cumbersome syntax upon usage, like: $(dd [|(a, b)|]) -- as opposed to the more succinct [dd|a, b|]. If you know a way to simplify this, please do tell!
    • As far as I know, [||] needs to contain fully valid Haskell, which pretty much necessitates the use of a tuple inside further exacerbating the syntactic situation. There is some upside to this too, however: at least we don't need to scratch our had where to split the expressions since GHC does that for us.
    • For some reason, the tuple only seemed to accept Booleans. Weird, I suspect this should be possible to fix somehow.
    • Pretty pretty-printing Exp is not very straight-forward. A more complete solution does require a parser after all.
    • Printing an AST scrubs the original formatting for a more uniform looks. I hoped to preserve the expressions letter-by-letter in the output.

    The deal-breaker was the syntactic over-head. I knew I could get to a simpler solution like [dd|a, a+1|] because I have seen that API provided in other packages. I was trying to remember where I saw that syntax. What is the name...?

    String -> Exp

    Quasi Quotation is the name, I remember!

    I remembered seeing packages with heredocs and interpolated strings, like:

    string = [qq|The quick {"brown"} $f {"jumps " ++ o} the $num ...|]
      where f = "fox"; o = "over"; num = 3
    

    Which, as far as I knew, during compile-time, turns into

    string = "The quick " ++ "brown" ++ " " ++ $f ++ "jumps " ++ o ++ " the" ++ show num ++ " ..."
      where f = "fox"; o = "over"; num = 3
    

    And I thought to myself: if they can do it, I should be able to do it too!

    A bit of digging in their source code revealed the QuasiQuoter type.

    data QuasiQuoter = QuasiQuoter {quoteExp :: String -> Q Exp}
    

    Bingo, this is what I want! Give me the source code as string! Ideally, I wouldn't mind returning string either, but maybe this will work. At this point I still know quite little about Q Exp.

    After all, in theory, I would just need to split the string on commas, map over it, duplicate the elements so that first part stays string and the second part becomes Haskell source code, which is passed to show.

    Turning this:

    [dd|a+1|]
    

    into this:

    "a+1" ++ " = " ++ show (a+1)
    

    Sounds easy, right?

    Well, it turns out that even though GHC most obviously is capable to parse haskell source code, it doesn't expose that function. Or not in any way we know of.

    I find it strange that we need a third-party package (which thankfully there is at least one called haskell-src-meta) to parse haskell source code for meta programming. Looks to me such an obvious duplication of logic, and potential source of mismatch -- resulting in bugs.

    Reluctantly, I started looking into it. After all, if it is good enough for the interpolated-string folks (those packaged did rely on haskell-src-meta) then maybe it will work okay for me too for the time being.

    And alas, it does contain the desired function:

    Language.Haskell.Meta.Parse.parseExp :: String -> Either String Exp
    

    Language.Haskell.Meta.Parse

    From this point it was rather straightforward, except for splitting on commas.

    Right now, I do a very simple split on all commas, but that doesn't account for this case:

    [dd|(1, 2), 3|]
    

    Which fails unfortunatelly. To handle this, I begun writing a parsec parser (again) which turned out to be more difficult than anticipated (again). At this point, I am open to suggestions. Maybe you know of a simple parser that handles the different edge-cases? If so, tell me in a comment, please! I plan on resolving this issue with or without parsec.

    But for the most use-cases: it works.

    Update at 2015-06-20

    Version 0.2.1 and later correctly parses expressions even if they contain commas inside them. Meaning [dd|(1, 2), 3|] and similar expressions are now supported.

    You can

    Conclusion

    During the last week I've learnt quite a bit of Template Haskell and QuasiQuotation, cabal sandboxes, publishing a package to hackage, building haddock docs and publishing them, and some things about Haskell too. It's been fun.

    And perhaps most importantly, I now am able to use this tool for debugging and development, the absence of which has been bugging me for some time. Peace at last.

    Thank you @kqr, your engagement with my original question and attempt at solving it gave me enough spark and motivation to continue writing up a full solution.