4

This question (from 5 years ago) asks 'Why are all recursive pattern synonyms rejected?' and its example is still rejected. The User Guide says "Pattern synonyms cannot be defined recursively."

I have a recursive pattern synonym that's accepted (GHC 8.10.2). I can call it, and it loops -- no surprise. So why does it compile?/Is there a plausible use-case for something like this?

Code based on an example in Section 2.3 'Polymorphic pattern synonyms' of the 2016 paper.

data JoinList a = JNil | Unit a | JoinList a `JoinTree` JoinList a    deriving (Eq, Read, Show)

I'm trying to define a pattern synonym Nil. Obviously JNil is a match. But also JNil ``JoinTree`` JNil, and any arbitrary nestings of JoinTree providing all the leafs are JNil.

pattern Nil :: JoinList a
pattern Nil <- JNil
  where Nil = Nil `JoinTree` Nil      -- huh?

wot = Nil `JoinTree` Nil              -- these all compile
wotwot Nil = ()
wotwotwot = wotwot Nil
wotwotwotwot = wotwot wot

Trying to call wot loops, no surprise. Trying to call wotwotwot complains Non-exhaustive patterns in function wotwot.

Full disclosure: what I was playing with/I know this doesn't work [see also below] is:

pattern Nil = JNil                     -- base case matcher, and builder
  where Nil <- Nil `JoinTree` Nil      -- recursion on the matcher, not the builder
                                       -- but can't put `<-` equations after where

The following rejected Recursive pattern synonym definition -- which is what I expect

pattern NilRec <- NilRec `JoinTree` NilRec 
  where NilRec = JNil    

And anyway that wouldn't work: it needs to match NilRec = JNil first as the base case.


To answer @Noughtmare's q "how would you suggest ..." comment to his answer, I'd like to write the decl per above 'what I was playing with' (yes I know that's currently illegal syntax); and get that desugarred to a matcher with two cases, tried sequentially:

case arg of
  { JNil -> ...
  ; Nil `JoinTree` Nil -> ...
  }

Note each of those cases has outermost a data constructor from type JoinList. So the recursive use of Nil is guarded/intermediated, as would be any approach via a ViewPattern. (Which is what the q 5 years ago was asking.) Put it another way: I think the compiler could generate a ViewPattern from the pattern ... = ... where ... <- ....

3
  • Note that the pattern ... <- ... where ... = ... notation defines the matcher (with the arrow) and the builder (with the equality sign) separately, it is not a base case recursive case split as you seem to suggest.
    – Noughtmare
    Commented Jul 24, 2021 at 10:35
  • 1
    "get that desugared to a matcher with two cases" -- but your proposed desugaring still contains sugar! Commented Jul 24, 2021 at 15:01
  • @Daniel "your [my] proposed desugaring" I've made a stab at here gitlab.haskell.org/ghc/ghc/-/issues/12203#note_366851
    – AntC
    Commented Jul 26, 2021 at 7:00

1 Answer 1

4

Only the matcher cannot be recursive, because that could lead to infinite pattern matching (infinitely large programs). The desugaring of case x of Nil -> y would be:

  case x of
    JNil -> y
    JoinTree x2 x3 -> case x2 of
      JNil -> case x3 of
        JNil -> y
      JoinTree x4 x5 -> case x4 of
        ...

Going on infinitely.

Having a recursive builder is fine, because they can contain arbitrary functions. Here is perhaps a simpler example that shows this without the confusion of constructors:

pattern Foo <- ()
  where Foo = let loop = loop in loop

You can allow arbitrary functions in patterns by using ViewPatterns:

normJoinList (JoinTree (normJoinList -> JNil) (normJoinList -> JNil)) = JNil
normJoinList x = x

pattern Nil <- (normJoinList -> JNil)
  where Nil = JNil

I think that does what you want:

> case Nil `JoinTree` Nil of Nil -> True; _ -> False
True
4
  • Thanks, the paper already has a solution using ViewPatterns. I hate them. I'm trying to avoid them.
    – AntC
    Commented Jul 24, 2021 at 10:24
  • @AntC I have added an example of how the desugaring would be infinitely long. How would you suggest to avoid that without using a recursive function (which requires ViewPatterns)?
    – Noughtmare
    Commented Jul 24, 2021 at 10:49
  • I guess one could imagine something like let f ts x0 = case x0 of { JNil -> g 0 ts; JoinTree x1 x2 -> f (x2:ts) x1 }; g n [] = z; g n (t:ts) = h n t (g (n+1) ts); h 0 JNil k = k; h n (JoinTree x0 x1) = h (n-1) x0 (h (n-1) x1 k); h _ _ k = z in f [] x (where z is the term that does the rest of the matches in the original case). Intuition: f checks if its argument is a perfectly balanced tree of JNils of any depth; g checks if its argument is a list of perfectly balanced trees of increasing depth; h checks if its argument is a perfectly balanced tree of a given depth. Commented Jul 24, 2021 at 15:29
  • Do note that the actual implementation of pattern synonyms would handle recursive matching just fine (presumably with a little fiddling). It's just that no one bothered to specify it properly and relied on a desugaring which can't quite handle it. pattern Nil <- JNil; pattern Nil <- Nil `JoinTree` Nil, if it worked, should produce matchNil :: forall rep (r :: TYPE rep) a. JoinList a -> (Void# -> r) -> (Void# -> r) -> r; matchNil JNil y n = y void#; matchNil (l `JoinTree` r) y n = matchNil l (\_ -> matchNil r y n) n; matchNil _ y n = n void#.
    – HTNW
    Commented Jul 24, 2021 at 17:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.