haskell / deepseq

Deep evaluation of data structures
http://hackage.haskell.org/package/deepseq
Other
40 stars 29 forks source link

Documentation improvements #104

Open OsePedro opened 1 month ago

OsePedro commented 1 month ago

The "Generic NFData deriving" documentation section shows how to derive instances of both NFData and NFData1 for Foo, but it doesn't say anything about when generic instances need NFData1 (the NFData1 documentation doesn't help either). As this is in the NFData class documentation, for people like me who first encountered NFData through a package that depends on it (Criterion in my case), this gives the impression that there must be a mysterious technicality that requires you to derive NFData1 for types with a single type parameter whenever you need an NFData instance. This is what I thought until I experimented and found that deepseq works fine on your Foo example without the NFData1 instance. E.g. in GHCi 8.10.7:

λ: data Foo a = Foo a String deriving(Generic,NFData)
λ: (Foo undefined "Hello" :: Foo Int) `deepseq` 1
*** Exception: Prelude.undefined

I see from the source code that rnf = rnf1 for functors like list, Maybe and Either, so I now wonder if NFData1 and NFData2 are just helper classes that are only useful when you're manually deriving NFData instances. Are there cases where NFData cannot be generically derived for unary/binary polymorphic types without NFData1/2? If not, can the derivation of NFData1 be removed from the Foo examples, and can a note be added stating that generic instances do not require NFData1/2? Or if NFData1/2 does need to be generically derived sometimes, can someone add an explanation to the documentation?

mixphix commented 1 month ago

The unary/binary polymorphism is not the same kind as you are thinking. NFData1 works on objects of kind k -> Type, not on terms of the kind Type. In your example, even though for any NFData a you have NFData (Foo a), you do not have NFData1 Foo. This is captured by the quantified constraints in the class declarations for NFData1 and NFData2, which instances then can assume without knowing beforehand on which type the application will be made.

I believe QuantifiedConstraints is only available on newer versions of GHC. Please update if you can, using ghcup or manually.

OsePedro commented 1 month ago

Yes, I'm aware that NFData1 expresses a constraint on types of kind k -> Type. But it's not my example -- it's the example in this part of the module documentation. I just removed NFData1 from the original example to demonstrate that you can derive an NFData instance without it.

From my point of view, the most important questions about NFData1 that the documentation doesn't make clear are:

  1. Am I right in thinking that the only people who need to call liftRnf or rnf1 are people who are manually deriving NFData instances?
  2. If not, what else is NFData1 useful for?
mixphix commented 1 month ago

NFData1 is to NFData what Eq1 is to Eq: a lawful typeclass. Where Eq1 "enforces" liftEq (==) == (==), we should always have liftRnf rnf == rnf for a NFData1 type constructor. In fact, the default implementation proves that law for any Generic1 data structure from its canonical representation Rep1.

The only part of the interface that need concern the day-to-day developer are the "Helper functions". These are normally what you want for making sure your own data is in RNF at certain evaluation steps. For example, f $!! x makes sure x is in RNF before applying f, but force (f $!! x) makes sure that the result is also in RNF before continuing to be used by the program.

OsePedro commented 1 month ago

Right, and isn't the "Generic NFData deriving" section also aimed at the day-to-day developer, whose goal is to apply the "Helper functions" to their own data types? If so, wouldn't it be less confusing to remove the derived Generic1 and NFData1 instances from the example Foo a type? After all, none of the "Helper functions" require NFData1.