Don't implement Newtype methods when deriving#3975
Don't implement Newtype methods when deriving#3975hdgarrood merged 4 commits intopurescript:masterfrom
Conversation
| class OldStyleNewtype t a where | ||
| wrap :: a -> t | ||
| unwrap :: t -> a | ||
|
|
||
| instance newtypeRecordNewtype :: | ||
| TypeEquals inner { x :: String } | ||
| => Newtype RecordNewtype inner where | ||
| => OldStyleNewtype RecordNewtype inner where |
There was a problem hiding this comment.
I'm not sure what this test was testing exactly, but it seems that the Newtype class was used only incidentally, as an example of whatever it is that's being tested. So instead of migrating this to the new Newtype class, I created another one,
There was a problem hiding this comment.
Yeah, I think it is unrelated - it's testing that rows in an instance context are allowed / work correctly.
|
Thanks for this! The code looks good to me, and we do need to either do this or partially revert the change in newtype and move the methods back into the class. I think since we are breaking basically every library anyway it’s probably a good opportunity to move the methods out of Newtype class and update the deriving here; generally the main risk with this sort of change is awful errors if you don’t have the right version of the compiler for the library version you’re using, but in this case I think that risk is fairly low. |
kl0tl
left a comment
There was a problem hiding this comment.
Thank you! This looks quite similar to what I had prepared myself in kl0tl@3a3c859.
In that commit I was also considering to add a Coercible constraint to the derived instance context. Without it wrap and unwrap don’t require the newtype constructor to be in scope, meaning that the following compiles:
module Example.N where
import Data.Newtype (class Newtype)
newtype N a = N a
instance newtypeN :: Newtype (N a) amodule Example where
import Example.N (N)
import Data.Newtype (wrap)
example :: forall a. a -> N a
example = wrapbut replacing wrap with coerce yields an error:
module Example where
import Example.N (N)
-import Data.Newtype (wrap)
+import Safe.Coerce (coerce)
example :: forall a. a -> N a
-example = wrap
+example = coerceError found:
in module Example
at Example.purs:7:11 - 7:17 (line 7, column 11 - line 7, column 17)
No type class instance was found for
Prim.Coerce.Coercible a0
(N a0)
while checking that type forall (a :: Type) (b :: Type). Coercible @Type a b => a -> b
is at least as general as type a0 -> N a0
while checking that expression coerce
has type a0 -> N a0
in value declaration example
where a0 is a rigid type variable
bound at (line 7, column 11 - line 7, column 17)
See https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md for more information,
or to contribute content related to this error.
because the newtype constructor is out of scope.
This also benefited from turning HiddenConstructors warnings for types with Newtype instances into errors, so that #3927 would not suggest to import unexported constructors.
Not requiring the newtype constructor to be in scope seems more ergonomic, but then newtypes declared with a Newtype instance in some internal modules and whose constructor is hidden from their library public interface can be freely wrapped and unwrapped.
I don’t understand this, can you rephrase? I’m ambivalent about adding Coercible to the instance context; I feel like you probably shouldn’t be declaring Newtype instances at all if the constructor isn’t part of your public API. |
|
I tried adding a Yielding an error: While the following works just fine: There must be a different bug somewhere. |
|
But regardless, I think perhaps disallowing hidden constructors in the presence of |
|
But this, on the other hand, does work: |
|
@hdgarrood If we add Coercible constraints to the context of derived Newtype instances, and a module declares a newtype with a Newtype instance but hide its constructor: module Example.N (N) where
import Data.Newtype (class Newtype)
import Prim.Coerce (class Coercible)
newtype N a = N a
instance newtypeN :: Coercible (N a) a => Newtype (N a) aThe compiler will raise a warning the first time it compiles the module, then trying to wrap or unwrap the newtype elsewhere will fail with an error suggesting to import the newtype constructor but this won’t solve anything since the newtype constructor isn’t exported in the first place. Turning the HiddenConstructors warning into an error for types with a Newtype instance allow us to fail earlier and not suggest things that cannot actually fix the issue. |
|
@fsoikin Could you provide a self contained example exhibiting the failure? The following compiles fine on your branch for me: module Example where
import Prim.Coerce (class Coercible)
import Data.Newtype (class Newtype)
newtype T = T Int
instance newtypeT :: Coercible T Int => Newtype T Int |
@fsoikin It doesn’t: newtypes can be exported with their constructor from internal modules, then re-exported without their constructor from public modules. Adding a Coercible instance to the context of derived Newtype instances ensures that such internal newtypes cannot be freely wrapped and unwrapped outside libraries internals. Granted those internal newtypes shouldn’t have Newtype instances in the first place, as Harry said, so perhaps we shouldn’t bother. |
|
Oooooh, I'm a moron: I misinterpreted the error message. It was failing on the consumer side, not at instance declaration. Anyway, I made
|
|
But here's another thought: even if we add But as I think more about it, it kinda seems more like an ergonomic problem. There won't be any sort of bad code generation or anything bad like that as a result. What do you think? |
I’m still a bit confused by this discussion. This feels like a bug in the compiler to me, even? If we have then shouldn’t it be possible to coerce between two types with a Newtype instance anywhere the Newtype instance is in scope? I don’t want to make HiddenConstructors an error (yet). |
|
That is: given then I think it should be possible to coerce |
|
@hdgarrood the difference is in where the Come to think of it, this kinda looks like the old behavior to me: if you declare a I'm personally more or less indifferent on this point, but I lean more towards @hdgarrood's side. I already made |
|
Also, I think the potential inconsistency between |
|
We should only bother with Coercible constraints in the context of Newtype instances if we want to disallow newtypes whose constructor is only exported for internal use to be freely wrapped and unwrapped by consumers: module Example.Internal where
import Data.Newtype (class Newtype)
newtype N a = N a
instance newtypeN :: Newtype (N a) amodule Example.Public (module Example.Internal) where
import Example.Internal (N)module Example where
import Example.Public (N)
import Data.Newtype (wrap)
example :: forall a. a -> N a
example = wrapIf we want to disallow this example then we have to add a |
|
This scenario was already allowed before the changes, right? |
|
Yes, disallowing this would require newtypes constructors to be imported for wrap, unwrap and other newtype combinators to be usable 😬 I’m fine with deriving Newtype instances without Coercible constraints in their context, sorry if was ambiguous about this, I just thought it was a subtle point to consider. Anyway, since Harry doesn’t want the HiddenConstructors warnings raised for Newtype instances to be turned into errors yet let’s drop 3541d18 and merge this. |
3541d18 to
afd1465
Compare
|
Done, dropped. |
Yes, that’s right. I think for me the main reason not to add the contexts is to avoid breaking existing code (or disallowing future code) which makes use of Newtype instances for types whose constructors are public but just happen not to be in scope there. |
This PR removes implementation of
wrapandunwrapmethods when deriving theNewtypeclass in the old-style syntax, likederive instance foo :: Newtype Bar _.These methods have been removed from the class in purescript/purescript-newtype#22, so now the old way of deriving them causes an error "wrap is not a method of Newtype".
To make tests pass after this change,
tests/support/bower.jsonwas updated with latest versions ofnewtype,safe-coerce,type-equality,typelevel-relude, andpreludepackages.And updating
preluderequired updates to theProxy-related tests.Sorry if I'm missing the mark again, just trying to help. Feel free to close if so.