-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Product Instances Explicitly Lazy #268
Comments
Does this type have other instances that are lazy the way you want? If not, I think it would be much better to make a new type to do what you want. If so, there's a (very subtle) discussion to be had around consistency vs. backwards compatibility. |
Can increasing the laziness of the pattern match make any programs "less correct"? I find it hard to believe that making this change would limit, rather than broaden, the scope of correctly-evaluated programs. |
@treeowl Could you elaborate on how adding explicit laziness would break backwards compatibility? |
It can lead to space leaks |
I'll file that under "feature". Strictness can easily be reintroduced in user-land, I think we should provide the most accurate definition we can. |
@tomjaguarpaw Thanks for pointing out space leaks. I have tried: pure () `mplus` Pair undefined undefined :: Product Maybe Maybe () Which works as expected. It seems to me that the only difference in space is the unpacking of the Is there a case where this could cause a major space leak? |
It was pointed out by @Lysxia in Lysxia/generic-data#68 (comment) that |
Actually, I shouldn't have said it can lead to a space leak, just that it can lead to different memory usage patterns. I suspect the memory usage of the lazy version is likely to be better. I don't see, however, how it's possible to recover the strict behaviour from the lazy one "in user-land". |
Why one would want to recover the strictness of unpacking |
The kind of user-land fix could be a newtype wrapper that would reimplement the stricter instance, or possibly an unpacked data declaration that addresses other concerns like working with unlifted types, should performance be of such concern. These kinds of applications are not universal and should not govern the domain of correctness for the (categorically) universal |
If it helps the committee, note that this is in part a lawfulness issue: the documentation for (This law would be upheld by making only the RHS of |
Oh, are you thinking of a newtype wrapper around |
I have determined that the current instance (Monad f, Monad g) => Monad (Product f g) where
Pair m n >>= f = Pair (m >>= fstP . f) (n >>= sndP . f)
where
fstP (Pair a _) = a
sndP (Pair _ b) = b We can reason thus: liftA2 f (Pair m n) q
= -- Monad/Applicative and Monad/Functor consistency laws
Pair m n >>= \x -> f x <$> q
= -- Definition of >>=
Pair
(m >>= \x -> fstP (f x <$> q))
(n >>= \x -> sndP (f x <$> q))
= -- Definitions of fmap and the helper functions, and case reduction
Pair
(m >>= \x -> case q of Pair q1 _ -> f x <$> q1)
(n >>= \x -> case q of Pair _ q2 -> f x <$> q2)
= -- Presumed strictness of underlying fmaps. EDIT: this is a bad assumption! See my comment below.
Pair
(m >>= \x -> f x <$> fstP q)
(n >>= \x -> f x <$> sndP q)
= -- Monad/Applicative/Functor consistency for underlying monads
Pair
(liftA2 f m (fstP q))
(liftA2 f n (sndP q)) Furthermore, it seems to be impossible to write a
My personal bias for basic types is always strict lawfulness, which narrows the options to 1 and 3. Of those, 1 is less intrusive (doesn't stop working code from compiling). Those who like to make loops will prefer 2, of course. The real solution: offer all three reasonable options (1, 2, and 3) in different types, from modules with names like |
I just remembered that the strictness of underlying instance (Monad m, Monad n) => Applicative (Product m n) where
liftA2 f (Prod m n) q =
Pair
(m >>= \x -> case q of Pair q1 _ -> f x <$> q1)
(n >>= \x -> case q of Pair _ q2 -> f x <$> q2) Ouch! |
I don't like requiring |
@mixphix There are too many different things running around now for me to know what exactly you're asking me to compare to what 😕. That's mostly my fault. |
If we care that the laws hold even with strictness in mind, the identity law Is there actually a consensus, if not a policy, that type class laws in base should hold even with |
This has the feel of swallowing a spider to catch a fly, but now I wonder if the fields on Edit: Hm, on second thought that may have appreciable performance implications for the |
It's no good (legally) when one of the underlying functors is |
What I am gathering from the above is that the current implementation of Product is not lawful, but there actually no way to make it a lawful Applicative and Monad. Is this correct? |
I find it always confusing to figure out how strictly (sorry for the pun) laws like So given that, I consider that laws re-strictness can be interpreted as authors seems best. Whether there should be lazy or strict pattern match is arbitrary. Until there is some policy&guideline, there will be endless debates. (IIRC the |
Stuff like ~(Pair f g) <*> ~(Pair x y) = Pair (f <*> x) (g <*> y) is often prone to be a source of space leaks, because forcing result to WHNF evaluates exactly nothing: it does not trigger evaluation of any of arguments. E. g., putting such |
Summary
Product
fromData.Functor.Product
has problematic strictness in its instances due to pattern matching. This causes:Problematic Instances:
For example using Applicative
Steps to reproduce
Expected behavior
x
should producePair Nothing Nothing
y
should terminate and producePair Nothing Nothing
.The text was updated successfully, but these errors were encountered: