Reputation: 1118
I've been trying to do a little bit of abstraction over data types, and I've encountered a situation with GHC's generics that seems a little odd. Here is my basic set of declarations:
class GFields f where
gfields :: f a -> [String]
instance (GFields c) => GFields (D1 i c) where
gfields = gfields . unM1
instance (GFields fs) => GFields (C1 i fs) where
gfields = gfields . unM1
instance (GFields f, GFields fs) => GFields (f :*: fs) where
gfields (f :*: fs) = gfields f ++ gfields fs
instance (Selector s) => GFields (S1 s r) where
gfields = (:[]) . selName
data Thing = Thing { foo :: String, bar :: Int }
deriving (Generic)
Trying to use this in GHCi gives me Prelude.undefined
if I give it an undefined value:
> gfields $ from (undefined :: Thing)
*** Exception: Prelude.undefined
However, if I try running some of the expected instance by hand (just grabbing a single field), I get what I would expect:
> selName $ (\(l :*: r) -> l) $ unM1 $ unM1 $ from (undefined :: Thing)
"foo"
Why do I get Prelude.undefined
in one, but not the other?
Upvotes: 6
Views: 199
Reputation: 19657
The problem is that none of your instances force the argument in any way, except this one:
instance (GFields f, GFields fs) => GFields (f :*: fs) where
gfields (f :*: fs) = gfields f ++ gfields fs
You're aiming to pass undefined
to your function, so you have to be very careful in forcing the arguments. The argument is only there to guide the type-checker, it can't be looked at.
The fix is easy. Make the pattern lazy (or irrefutable, as the Haskell Report calls it):
instance (GFields f, GFields fs) => GFields (f :*: fs) where
gfields ~(f :*: fs) = gfields f ++ gfields fs
This way, the match doesn't actually force the value. It will instead always succeed, and the use of f
and fs
translates into applications of selector functions.
With this change, your program works:
ghci> gfields $ from (undefined :: Thing)
["foo","bar"]
Your other program works because you call selName
on the outside:
ghci> selName $ (\(l :*: r) -> l) $ unM1 $ unM1 $ from (undefined :: Thing)
"foo"
Now even though you have the pattern match on the pair in the expression, only the type
of the argument of selName
is relevant for the result. But this expression isn't exactly
the same as your first test program, as the different results demonstrate and as jozefg
further explains in his answer.
Upvotes: 4
Reputation: 53901
So this was interesting, what you have there isn't actually quite what's done, the actual code after the inlining is
main = print
. (\(l :*: r) -> selName l ++ selName r)
. unM1
. unM1
. from
$ (undefined :: Thing)
However, changing \(l :*: r) -> selName l ++ selName r
to what you had doesn't crash. So the difference is clearly in this line. The obvious thought, that there's something bad about the right field is quickly disproved since \(l :*: r) -> r
still runs.
We can see that the only nonbottom results are of the form (\l :*: r -> ???)
where ??? is either l
or r
. Nothing else.
So let's take a look at the derived instance with -ddump-deriv
.
from (Thing g1_atM g2_atN) = M1 (M1 (M1 (K1 g1_atM) :*: M1 (K1 g2_atN)))
Notice that this is strict in the constructor. So we must not be strict in the result of from undefined
because the code will crash. So now we're kinda walking on a house of cards here, since forcing any part of this will crash our program. The interesting bit is that
-- The derived selectors
instance Selector S1_0_0Thing where
selName _ = "foo"
instance Selector S1_0_1Thing where
selName _ = "bar"
isn't strict in it's argument. So here's the catch, your code all compiles down to the constant "foo"
because selName
is constant, we don't use any of the previous computations; it's a compile time computation. However, if we do any sort of computation with l
and r
in that lambda, than when we use selName
or do anything to see the result, we force the lambda to run, but since l :*: r
is really bottom, we crash.
As a quick demonstration, this will crash
main = (`seq` putStrLn "Crashes")
. (\(l :*: r) -> ())
. unM1
. unM1
. from
$ (undefined :: Thing)
But this will not
main = (const $ putStrLn "Crashes")
. (\(l :*: r) -> ())
. unM1
. unM1
. from
$ (undefined :: Thing)
TLDR, just make each of the fields undefined, but the toplevel constructor shouldn't be bottom.
Upvotes: 4