Search code examples
haskelltemplate-haskellquasiquotes

How can I write a pattern quasi quoter in Haskell?


I use quasi quoters to create my smart-constructed data types at compile time. This looks something like:

import qualified Data.Text as T
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Language.Haskell.TH (Q, Exp, Pat(..), Lit(..))
import Language.Haskell.TH.Syntax (Lift(..))
import qualified Language.Haskell.TH.Syntax as TH
import Instances.TH.Lift () -- th-lift-instances package

newtype NonEmptyText = NonEmptyText Text

textIsWhitespace :: Text -> Bool
textIsWhitespace = T.all (== ' ')

mkNonEmptyText :: Text -> Maybe NonEmptyText
mkNonEmptyText t = if textIsWhitespace t then Nothing else (Just (NonEmptyText t))

compileNonEmptyText :: QuasiQuoter
compileNonEmptyText = QuasiQuoter
  { quoteExp = compileNonEmptyText'
  , quotePat = error "NonEmptyText is not supported as a pattern"
  , quoteDec = error "NonEmptyText is not supported at top-level"
  , quoteType = error "NonEmptyText is not supported as a type"
  }
  where
    compileNonEmptyText' :: String -> Q Exp
    compileNonEmptyText' s = case mkNonEmptyText (pack s) of
      Nothing -> fail $ "Invalid NonEmptyText: " ++ s
      Just txt -> [| txt |]

(I can provide a standalone working example if necessary—I just pulled this example out of a larger codebase)

Essentially, by just deriving Lift for my newtypes, I can place the data type in an expression quasi quoter [| txt |] to implement quoteExp.

But I'm having trouble with quotePat. If I do e.g.:

Just txt -> [p| txt |]

Then I get a warning that the first txt is unused, and the second shadows the first. I'm pretty sure that that pattern is just creating a new name txt rather than splicing in the in-scope txt like the expression quasi quoter did, since when I do:

f :: NonEmptyText -> Bool
f [compileNonEmptyText|test|] = True
f _ = False

everything matches the first statement.


Solution

  • Alright I think I've got it. Starting from the base string s, I can wrap that in StringL and LitP to get a literal string, which because of Text's IsString instance will become a Text. From there I need to apply the NonEmptyText constructor using ConP:

    compileNonEmptyTextPattern' :: String -> Q TH.Pat
    compileNonEmptyTextPattern' s = case mkNonEmptyText (pack s) of
      Nothing -> fail $ "Invalid NonEmptyText: " ++ s
      Just (NonEmptyText txt) -> pure $ ConP 'NonEmptyText [(LitP (StringL (T.unpack txt)))]
    

    It's unfortunate that this is so much more verbose than the expression version, though! I wonder if there could be a typeclass for Q Pat like Lift is for Q Exp?