Factoring out common constants in pattern synonym code?

93 Views Asked by At

I had some code like this:

newtype T = T Text

pattern Alice = T "Alice" 

This is fine, but I was using "Alice" in other places. So I decided to factor it out like so:

alice :: Text
alice = "Alice"

pattern Alice = T alice

But when I built this with warnings on, I got a warning about an unused variable.

I then realised that:

pattern Alice = T alice

actually matches everything, as opposed just T "Alice"

It then seemed weird that even T "Alice" was allowed, as "Alice" is Text, something which is computed.

But then I saw on the Overloaded Strings docs that:

String literals behave very much like integer literals, i.e., they can be used in both expressions and patterns. If used in a pattern the literal will be replaced by an equality test, in the same way as an integer literal is.

So this raises a few questions:

  1. Could I even write the pattern Alice without enabling the overloaded strings extension?
  2. Can I create pattern synonyms where the RHS requires some computation, and have GHC use Eq to match, just like it does for numeric and string literals? Or are numeric and string literals a special case and GHC doesn't allow one to generalise that functionality?
3

There are 3 best solutions below

0
On

You can use view patterns to perform any computation in the pattern and then use the result to match anything else.

ghci> :set -XPatternSynonyms
ghci> :set -XViewPatterns
alice = "Alice"
ghci> pattern Alice <- ((==alice) -> True)

Here you apply (==alice) to the argument and match the result against True.

1
On

Could I even write the pattern Alice without enabling the overloaded strings extension?

No, since you need to OverloadedStrings to also adapt the string literals in patterns. If you don't, then it will not work, or at least not without some unpacking. Behind the curtains it will use fromString on the value of the pattern and check equivalence, so:

f "Alice" = "foo"
f _ = "bar"

is essentially equivalent to:

f x | x == fromString "Alice" = "foo"
f _ = "bar"

As for factoring out subpatterns, why not just make an AliceText pattern:

pattern AliceText :: Text
pattern AliceText = "Alice"

pattern Alice = T AliceText

and thus create different pattern synonyms that can refer to each other. Furthermore a pattern can also be used as expression here, so AliceText can be used wherever you need Text with "Alice" as value.

0
On

This is not really a solution I'd recommend, but studying it might be useful to better understand how string/text patterns work.

We can use Template Haskell to generate patterns that correspond to your original string-literal T "Alice", but with a variable string:

module StrLitPat where

import Language.Haskell.TH

stringLiteralPattern :: String -> Q Pat
stringLiteralPattern = pure . LitP . StringL

alice :: String
alice = "Alice"

This can be spliced into a pattern, including pattern synonym, like this:

{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms   #-}

import Data.String (IsString(..))
import Data.Text (Text)

import StrLitPat

newtype T = T Text

pattern Alice = T $(stringLiteralPattern alice)

main :: IO ()
main = putStrLn $ case (T "Alice", T "Bob") of
   (_, Alice) -> "Alice on the right"
   (Alice, _) -> "Alice on the left"
   _          -> "Alice isn't here"

Notice how alice stands in for a Text pattern here, even though its type was declared as String! (You could also have made it IsString s => s.) This is because Template Haskell uses strings, at compile time, and only in the actual concrete pattern does the OverloadedStrings mechanism come into play and allow for the conversion to and from Text.