Writing a pattern synonym to hide a constructor

455 views Asked by At

Consider the following:

module MyModule (
  A(FortyTwo), -- Note we don't expose PrivateA
  B(P) -- Nor PrivateB
) where

pattern FortyTwo = A 42

newtype A = PrivateA Int
data B = PrivateB Int Int

pattern P :: Int -> A -> B

How could I write pattern P?

Basically I to be able to say:

f :: B -> String
f (P 2 FortyTwo) = "The meaning of life"

That is, being able to pattern match without ever referencing the private constructors PrivateA and PrivateB directly outside the module where they are defined.

1

There are 1 answers

0
Silvio Mayolo On BEST ANSWER

First, remember that newtype can only be used with data constructors that have a single argument, so your A can be newtype but B can't.

data B = PrivateB Int Int

Now, the pattern syntax you're using for FortyTwo is called implicitly bidirectional. That is,

pattern FortyTwo :: A
pattern FortyTwo = PrivateA 42

We use = and truly mean equality. It says "I can use FortyTwo to construct an A, and if I have an A, I can use FortyTwo to pattern match on it". This is the simplest form, and it's useful in "simple" cases where the arguments are nicely oriented.

But the real world isn't that simple. So GHC provides us with an extended syntax called explicitly bidirectional patterns. In short, we can specify explicitly how we want an expression to behave in pattern context and how we want it to behave in expression context. The compiler doesn't (and can't) check that the two expressions make cohesive sense as a pair, so we could use it to do some nonsense like this

pattern Nonsense :: Int -> Int
pattern Nonsense n <- n where
  Nonsense _ = 42

This defines Nonsense in such a way that let Nonsense x = Nonsense 0 in x returns 42.

But your use case sounds completely reasonable, so we can define it with explicitly bidirectional patterns.

There's one more minor piece we'll need to finish this out, and that's called view patterns. A view pattern is a pattern (hence, we use it in pattern matching) which is really just a function call in disguise. In very brief summary, the following are roughly equivalent

let y = f x in y
let (f -> y) = x in y

It really just moves the function call to the other side of the equal sign, which can be handy for writing terse code in some situations. It's also useful when defining pattern synonyms.

pattern P :: Int -> A -> B
pattern P n a <- PrivateB n (PrivateA -> a) where
  P n (PrivateA a) = PrivateB n a

The first line, of course, is the type declaration. The second line says "when I see a pattern of the form P n a, pretend it says PrivateB n (PrivateA -> a)". The last line says "when I see an expression that says P n (PrivateA a), construct a PrivateB n a". This defines a Haskell function (and the function is exhaustive since A only has one constructor, and we've handled it).

Complete runnable example:

{-# LANGUAGE PatternSynonyms, ViewPatterns #-}

module Main where

pattern FortyTwo :: A
pattern FortyTwo = PrivateA 42

newtype A = PrivateA Int
data B = PrivateB Int Int

pattern P :: Int -> A -> B
pattern P n a <- PrivateB n (PrivateA -> a) where
  P n (PrivateA a) = PrivateB n a

f :: B -> String
f (P 2 FortyTwo) = "The meaning of life"
f _ = "Nope :("

main :: IO ()
main = do
  putStrLn $ f (PrivateB 2 42)
  putStrLn $ f (PrivateB 2 43)

Try it online!