-
Notifications
You must be signed in to change notification settings - Fork 564
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
Multiparameter instance incorrectly considered partially overlapping #4338
Comments
This comment was marked as off-topic.
This comment was marked as off-topic.
That behavior is correct, as documented here: https://github.com/purescript/documentation/blob/master/language/Type-Classes.md#instance-chains (in response to @CarstenKoenig's comment, not the original post). |
Let's move this convo to the Discourse thread; pretty sure there's no issue here, and if there were it wouldn't be this issue (there are no multiparameter classes involved). |
Back on topic: @fsoikin, do you have a slightly more realistic repro of this issue? The minimal case you've provided would never work because |
Yes, of course. But first, I'd like to note that the minimal example would, in fact, work, despite the fact that But the original use case is, of course, rows. Isn't it always? :-) The target API looks like this: type ExpectedArgs =
{ x :: Int -- This field is required
, y :: Opt String -- This field is optional
, z :: Opt Boolean -- And so is this
}
f :: ∀ args. CanBeCoerced args ExpectedArgs => args -> Int
f args' = args.x + ... (some implementation) ...
where
args = coerce args' :: ExpectedArgs
-- Use sites:
main = do
traceM $ f { x: 42 } -- y and z omitted
traceM $ f { x: 42, y: "foo" } -- only z omitted
traceM $ f { x: 42, y: "foo", z: false } -- both present
traceM $ f { x: 42, z: false } -- only y omitted
traceM $ f { x: 42, y: opt "foo", z: false } -- y has type `Opt String` instead of `String`
traceM $ f { x: 42, z: opt false } -- z has type `Opt Boolean` instead of `Boolean` The newtype Opt a = Opt a
opt :: ∀ a. a -> Opt a
opt = Opt The interesting part is the class CanBeCoerced a b
instance CanBeCoerced a b => CanBeCoerced (Opt a) (Opt b)
else instance CanBeCoerced a b => CanBeCoerced a (Opt b)
else instance (RowToList a al, RowToList b bl, CanBeCoercedRL al bl) => CanBeCoerced (Record a) (Record b)
else instance TypeEquals a b => CanBeCoerced a b
class CanBeCoercedRL (a :: RowList Type) (b :: RowList Type)
instance CanBeCoercedRL Nil Nil
else instance (CanBeCoerced x y, CanBeCoercedRL a b) => CanBeCoercedRL (Cons label x a) (Cons label y b)
else instance CanBeCoercedRL a b => CanBeCoercedRL a (Cons label (Opt y) b)
coerce :: ∀ a b. CanBeCoerced a b => a -> b
coerce = unsafeCoerce With this, all of the above examples work. type ExpectedArgsG a = { x :: a, y :: Opt a }
g :: ∀ args a. CanBeCoerced args (ExpectedArgsG a) => Show a => args -> Int
g args' = ...
where
args = coerce args' :: ExpectedArgsG a With this, I would expect to be able to: main = do
traceM $ g { x: 42 }
traceM $ g { x: 42, y: 5 } Alas, this doesn't work (after the new apartness check in 0.15), because the choice of instance depends on the choice of To work around that issue (and here's a pull request for it), I introduce a way to explicitly mark required fields, which is done with a new type newtype Req a = Req a
type ExpectedArgsG a = { x :: Req a, y :: Opt a } This allows me to match on class CanBeCoerced a b
instance TypeEquals a b => CanBeCoerced a (Req b)
else instance CanBeCoerced a b => CanBeCoerced (Opt a) (Opt b)
else instance CanBeCoerced a b => CanBeCoerced a (Opt b)
else instance (RowToList a al, RowToList b bl, CanBeCoercedRL al bl) => CanBeCoerced (Record a) (Record b)
else instance TypeEquals a b => CanBeCoerced a b Now the above example with h :: ∀ a. Show a => a -> Int
h a = g { x: a, y: a } Now this doesn't compile, finally producing the error at the root of this issue:
For convenience, here's the full program, from beginning to end. It fails to compile with the above error, but commenting module Main where
import Prelude
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype (class Newtype, unwrap)
import Data.String (length)
import Debug (traceM)
import Effect (Effect)
import Prim.RowList (class RowToList, Cons, Nil, RowList)
import Type.Equality (class TypeEquals)
import Unsafe.Coerce (unsafeCoerce)
newtype Opt a = Opt a
newtype Req a = Req a
derive instance Newtype (Req a) _
toMaybe :: ∀ a. Opt a -> Maybe a
toMaybe (Opt a) = Just a -- bogus implementation, just a placeholder!
opt :: ∀ a. a -> Opt a
opt = Opt
class CanBeCoerced a b
instance TypeEquals a b => CanBeCoerced a (Req b)
else instance CanBeCoerced a b => CanBeCoerced (Opt a) (Opt b)
else instance CanBeCoerced a b => CanBeCoerced a (Opt b)
else instance (RowToList a al, RowToList b bl, CanBeCoercedRL al bl) => CanBeCoerced (Record a) (Record b)
else instance TypeEquals a b => CanBeCoerced a b
class CanBeCoercedRL (a :: RowList Type) (b :: RowList Type)
instance CanBeCoercedRL Nil Nil
else instance (CanBeCoerced x y, CanBeCoercedRL a b) => CanBeCoercedRL (Cons label x a) (Cons label y b)
else instance CanBeCoercedRL a b => CanBeCoercedRL a (Cons label (Opt y) b)
coerce :: ∀ a b. CanBeCoerced a b => a -> b
coerce = unsafeCoerce
type ExpectedArgsG a = { x :: Req a, y :: Opt a }
g :: ∀ args a. CanBeCoerced args (ExpectedArgsG a) => Show a => args -> Int
g args' =
(args.x # unwrap # show # length) + (args.y # toMaybe <#> show <#> length # fromMaybe 0)
-- ^ Just some implementation to make sure the type variable isn't left ambiguous
where
args = coerce args' :: ExpectedArgsG a
h :: ∀ a. Show a => a -> Int
h a = g { x: a, y: a }
main :: Effect Unit
main = do
traceM $ g { x: 42 }
traceM $ g { x: 42, y: 5 }
traceM $ g { x: 42, y: opt 5 } |
Thanks, yes indeed, I stand corrected. That example is helpful; thank you! I'm not sure that code ought to compile but I could be wrong about this again. In this case, you (eventually) want to solve the constraint |
Oh yes, you're absolutely right: it's not the same error. In my initial post This is why a distilled example is better: less stuff to get confused about :-) But yes, I remember now: this was exactly my train of thought, and to work around that, I added another class CanBeCoerced a b
instance CanBeCoerced a (Opt a)
else instance TypeEquals a b => CanBeCoerced a (Req b)
else instance CanBeCoerced a b => CanBeCoerced (Opt a) (Opt b)
else instance CanBeCoerced a b => CanBeCoerced a (Opt b)
else instance (RowToList a al, RowToList b bl, CanBeCoercedRL al bl) => CanBeCoerced (Record a) (Record b)
else instance TypeEquals a b => CanBeCoerced a b With that, h :: ∀ a. Show a => a -> Int
h a = g { x: a, y: opt a }
-- ^^^ this part is new Now that finally yields the original error:
Claiming that However, while trying to recreate it again, I accidentally stumbled on further solution: if I add another instance for Nevertheless, I think this could be considered a compiler bug: it's still claiming an impossible partial overlap. |
Yeah, the original example (and this latest one) is certainly buggy behavior. I was only asking for clarification because I had a somewhat narrow fix in mind and wanted to know if the problem was broader than I was thinking. It still might be, but it looks like the narrow thing would do the right thing for this library at least. |
Oh, that's great to hear! To be honest, I didn't really expect a fix :-) |
The full code that triggers the error is: module Main where
import Prelude
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype (class Newtype, unwrap)
import Data.String (length)
import Debug (traceM)
import Effect (Effect)
import Prim.RowList (class RowToList, Cons, Nil, RowList)
import Type.Equality (class TypeEquals)
import Unsafe.Coerce (unsafeCoerce)
newtype Opt a = Opt a
newtype Req a = Req a
derive instance Newtype (Req a) _
toMaybe :: ∀ a. Opt a -> Maybe a
toMaybe (Opt a) = Just a -- bogus implementation, just a placeholder!
opt :: ∀ a. a -> Opt a
opt = Opt
class CanBeCoerced a b
instance CanBeCoerced a (Opt a)
else instance TypeEquals a b => CanBeCoerced a (Req b)
else instance CanBeCoerced a b => CanBeCoerced (Opt a) (Opt b)
else instance CanBeCoerced a b => CanBeCoerced a (Opt b)
else instance (RowToList a al, RowToList b bl, CanBeCoercedRL al bl) => CanBeCoerced (Record a) (Record b)
else instance TypeEquals a b => CanBeCoerced a b
class CanBeCoercedRL (a :: RowList Type) (b :: RowList Type)
instance CanBeCoercedRL Nil Nil
else instance (CanBeCoerced x y, CanBeCoercedRL a b) => CanBeCoercedRL (Cons label x a) (Cons label y b)
else instance CanBeCoercedRL a b => CanBeCoercedRL a (Cons label (Opt y) b)
coerce :: ∀ a b. CanBeCoerced a b => a -> b
coerce = unsafeCoerce
type ExpectedArgsG a = { x :: Req a, y :: Opt a }
g :: ∀ args a. CanBeCoerced args (ExpectedArgsG a) => Show a => args -> Int
g args' =
(args.x # unwrap # show # length) + (args.y # toMaybe <#> show <#> length # fromMaybe 0)
-- ^ Just some implementation to make sure the type variable isn't left ambiguous
where
args = coerce args' :: ExpectedArgsG a
h :: ∀ a. Show a => a -> Int
h a = g { x: a, y: opt a }
-- ^^^ this part is new
main :: Effect Unit
main = do
traceM $ g { x: 42 }
traceM $ g { x: 42, y: 5 }
traceM $ g { x: 42, y: opt 5 } |
No I'm agreeing that I don't think #4135 was about fundeps, so it's probably about this issue instead. (Parameters only considered locally in terms of the overlaps check, instead of globally in relation to the other parameters.) |
Maybe we should say that the other issue is a duplicate of this one, if we invent time-travel :) |
Ohhh I see, yeah, could be! |
I tried to upgrade a project to 0.15.4 from 0.14.5 and I think I hit this case too:
We're using this to convert types to similar types which are FFI-stable and deterministic, like flattening Data.Map into a sorted Array of Tuples. For that, we have a
Here
|
Turns out we didn't need the problematic type class instance. It's still worth reporting and fixing though, since it compiled fine in 0.14 but not in 0.15. Let me know if you want me to minimize it further, it became pretty clear to me that the issue was at line 80-ish when I wrote the code below so I didn't reduce it further.
|
@drather, I believe the compiler is behaving correctly on your example. The error message produced by uncommenting the
This is new behavior as of 0.15.0; it's documented here, and #4149 is the PR that made the change. |
I think there's another case of this in Which can be observed here, resulting in
It considers this overlapping, but I wonder if we could allow things like this to solve with an occurs check somehow? |
Description
See repro section for precise description of the symptoms. I'm not completely sure what the problem is, but it looks like the apartness check happens separately for each parameter of the instance, so that the instance may be considered partially overlapping if each parameter cannot be shown to be "apart" when considered in isolation, even if there is no possible substitution that would match all parameters together.
To Reproduce
The last line yields an error:
How can
C (N a) (N a)
possibly overlapC t0 (N t0)
? That would requiret0 ~ N t0
.Expected behavior
The second instance should be chosen, allowing the program to compile.
PureScript version
0.15.2
The text was updated successfully, but these errors were encountered: