To start off this whole thing I'm working with a pattern synonym defined as follows:
{-# Language PatternSynonyms #-}
pattern x := y <- x @ y
This allows me to run multiple pattern matches across a parameter at once. A regular as binding (@
) does not allow the left hand side to be a pattern but this does.
With this I make the following toy function
{-# Language ViewPatterns #-}
f ((_:_) := (head -> y)) =
[ y ]
f [] =
[]
It's not the best way to implement this I am sure, but it's a minimum working example for the behavior in question.
This has a function that takes a single parameter.
It matches the parameter against two patterns using the defined synonym.
The first pattern matches any non-empty list and makes no bindings.
The second runs the head function on the list and binds y
to the result.
So the question is can head
cause an error or will the other pattern prevent it?
>>> f []
[]
The other pattern prevents it! Alright so if I do them in the other order then it should break right?
f' ((head -> y) := (_:_)) =
[ y ]
f' [] =
[]
>>> f' []
[]
Nope! It still works. So now my question is: Is the second pattern doing anything at all? Maybe view patterns has some sort of smart behavior where it calls the function and fails the pattern if an error occurs ...
f'' (head -> y) =
[ y ]
f'' [] =
[]
>>> f'' []
[*** Exception: Prelude.head: empty list
No ... it doesn't. This fails. Somehow (_:_)
blocks the error no matter what side it's on. Maybe ghc prefers to match destructuring patterns before view patterns? To test this I can replace the pattern (_:_)
with (reverse -> _:_)
. This way it has to run a function before it can get to the destructuring.
But having tested it, the new pattern doesn't change the behavior. This hypothesis can be ruled out.
So maybe it's laziness? x
can't be evaluated if the list is empty so it sits in thunk and the error never occurs. It seems to somewhat be the case. If I replace (head -> x)
with (undefined -> x)
we have no change in behavior.
However if I replace it with (undefined -> "yo")
:
f ((undefined -> "yo") := (x:_)) = [x]
f [] = []
>>> f []
*** Exception: Prelude.undefined
The undefined
does get evaluated. Which seems to indicate that the pattern is forcing evaluation to compare with "yo"
. And if I now switch the order:
f ((x:_) := (undefined -> "yo")) = [x]
f [] = []
>>> f []
[]
It isn't evaluated. It seems that now we are short circuiting the pattern match.
So the laziness hypothesis seems to make sense? It's still very opaque to me and I would love to have someone with more experience as to the internals of ghc confirm this hypothesis.
So my question is now what is going on? Is it laziness? How does it work?
A big thanks to discord user lexi. They helped a lot in the diagnosis thus far.