-
Notifications
You must be signed in to change notification settings - Fork 571
Instance chain groups #2929
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
Instance chain groups #2929
Conversation
|
I like the simplicity of this approach a lot, but there is a potential issue, as we discussed on Slack: The solver works in stages, since there are two types of constraints:
Equality constraints cause types to become more solved by unification, which lets us try to solve more type class constraints, which generates more equalities, and so on. The problem is that if we have a chain containing more than one instance, we cannot decide to use the second instance in the chain without knowing with certainty that we can never match the first. The problem is that we don't necessarily know this until we have enough information about the types involved to rule out the first instance, and that might require other functional dependencies to be solved first. For example, suppose we have class C a
instance x :: C String where
...
else instance y :: C a where
...and suppose we need to solve So before we can consider I think we can do this by modifying the |
|
@paf31 There are still some TODOs and refactoring that could be done. But is this the kind of thing you were thinking? |
| go ([], TUnknown u1) ([], TUnknown u2) | u1 == u2 = (Match (), M.empty) | ||
| go ([], TypeVar v1) ([], TypeVar v2) | v1 == v2 = (Match (), M.empty) | ||
| go ([], Skolem _ sk1 _ _) ([], Skolem _ sk2 _ _) | sk1 == sk2 = (Match (), M.empty) | ||
| go (sd, r) ([], TypeVar v) = (Match (), M.singleton v [rowFromList (sd, r)]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need an Unknown case here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been trying for a little while, but I'm not sure how to get this situation to happen. Any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I don't think either Unknown case is required. But either both are, or neither is.
| go ([], Skolem _ sk1 _ _) ([], Skolem _ sk2 _ _) | sk1 == sk2 = (Match (), M.empty) | ||
| go (sd, r) ([], TypeVar v) = (Match (), M.singleton v [rowFromList (sd, r)]) | ||
| go _ _ = (Apart, M.empty) | ||
| typeHeadsAreEqual (TypeVar _) _ = (Unknown, M.empty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this case get hit?
| , "infix" | ||
| , "class" | ||
| , "instance" | ||
| , "instances" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I can revert this change. I updated the syntax to use interleaved else - which are already in this list.
|
Looks great! Is this breaking? I guess, technically yes, since it reserves new keywords. In that case, maybe we should also disallow overlapping instances, and release this in 0.12.0. |
|
The only breaking part here would be that the order of overlapping instances might be different (if we don't disallow them). But we don't advise people use overlapping instances anyway? There are no new keywords. |
|
Ok, great, then we can merge this into 0.11.x and disallow overlaps in 0.12.0 as a breaking change! Very excited to merge this 😄 |
| return $ uncurry Solved (head tcds) | ||
| | otherwise = return $ uncurry Solved (minimumBy (compare `on` length . tcdPath . snd) tcds) | ||
| | otherwise = | ||
| return $ uncurry Solved (minimumBy (compare `on` (\d -> (tcdChain d, tcdIndex d, length (tcdPath d))) . snd) tcds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we are considering instances in order, I shouldn't need to do this any more.
| overlapping TypeClassDictionaryInScope{ tcdDependencies = Nothing } _ = False | ||
| overlapping _ TypeClassDictionaryInScope{ tcdDependencies = Nothing } = False | ||
| overlapping tcd1 tcd2 = tcdValue tcd1 /= tcdValue tcd2 | ||
| overlapping tcd1 tcd2 = tcdValue tcd1 /= tcdValue tcd2 && (null (tcdChain tcd1) || tcdChain tcd1 /= tcdChain tcd2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't need this either.
|
@LiamGoodacre Is this ready for a final review then? |
LiamGoodacre
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@paf31 should be good for a review. I've left some comments on things to consider. If you (or anyone) can think of any more examples to test with, that would be great. I should perhaps add some failing tests too.
| -- TODO: efficiency | ||
| chain <- groupBy ((==) `on` tcdChain) $ forClassName (combineContexts context inferred) className' tys'' | ||
| -- process instances in a chain in index order | ||
| let found = for (sortBy (compare `on` tcdIndex) chain) $ \tcd -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I might need to special-case when tcdChain = []. The processing of any non-chain instances shouldn't interfere with each other. In this case we wouldn't get the overlapping instances warning.
| ] | ||
| let instances = do | ||
| -- TODO: efficiency | ||
| chain <- groupBy ((==) `on` tcdChain) $ forClassName (combineContexts context inferred) className' tys'' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The efficiency note here was to consider grouping using a Map instead.
|
This only supports a single chain of instances per class then? It seems if you try to define multiple there's no error aside from the fact overlaps start occurring again. Maybe it should have an explicit error instead? (Or ideally support multiple chains 😜) |
|
@garyb This is supposed to support multiple chains. But, in the same way that you shouldn't define overlapping instances, you shouldn't define overlapping chains. Although, I did just try to use overlapping chains and it failed to solve. class Learn a b | a -> b
instance learnInst :: Learn a String
class IsString t o | t -> o
instance isStringString :: IsString String True
else instance isStringElse :: IsString a False
instance isStringString' :: IsString String True
else instance isStringElse' :: IsString a False
learnIsString :: forall a t o.
IsString t o =>
Learn a t =>
Proxy o
learnIsString = Proxy
isStringEg0 :: Proxy True
isStringEg0 = learnIsStringWith the output: [1/1 OverlappingInstances]
Overlapping type class instances found for
Foo.IsString t0 True
The following instances were found:
Foo.isStringElse (chosen)
Foo.isStringElse'
Overlapping type class instances can lead to different behavior based on the order of module imports, and for that reason are not recommended.
They may be disallowed completely in a future version of the compiler.
where t0 is an unknown type
[1/1 TypesDoNotUnify] src/Foo.purs:71:15
71 isStringEg0 = learnIsString
Could not match type
False
with type
True
while solving type class constraint
Foo.IsString t0 TrueIf I don't have the duplicate chain, then it passes. --instance isStringString' :: IsString String True
--else instance isStringElse' :: IsString a FalseSo something is wrong 😄 |
|
Hmm I think I see what's happening with the case I tried then, I'd need to be cleverer with the definitions (or maybe it's impossible)... I tried writing an |
|
Turns out that |
|
Ummmmm, travis is complaining about code that doesn't exist? @paf31 |
|
Or am I completely crazy? |
|
Oh, did it rebase my commits onto master and then try to build? 😕 |
|
Okay, I've rebased and fixed the issue that Travis was complaining about. |
I think CI will usually merge your branch into master (locally, ie without modifying actual master in the repo), and attempt to build that. I think that is probably what it should be doing, too. |
|
@LiamGoodacre Could you please rebase this? |
|
Ah rebased at the wrong time, need to do it again 😄 |
|
This should be good to go now. |
paf31
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯 Thank you!
For #2315