Skip to content

Conversation

@kl0tl
Copy link
Member

@kl0tl kl0tl commented Sep 9, 2020

Given newtype N a = N a, the newtype constructor should be exported for Coercible N a and Coercible a N constraints to be solvables. This pull request adds a specific error for when we fail to find a newtype constructor in the environment rather than the unhelpful NoInstanceFound error currently thrown.

The CannotFindCoercedConstructor error is thrown for both data and newtype constructors because I didn’t find how to differentiate them when they aren’t exported, did I miss something?

@hdgarrood
Copy link
Contributor

This isn't related to this PR, but what happens if a constructor is imported and only used to permit a coercion? Do we still emit an unused import warning?

@kl0tl
Copy link
Member Author

kl0tl commented Sep 16, 2020

Newtype unwrapping is possible as long as the newtype constructor is exported, unlike Haskell it doesn‘t have to be in scope, so there’s no need to import a constructor only to allow some coercions.

@hdgarrood
Copy link
Contributor

I'm not sure that approach is safe: it's quite common to define a newtype in a MyModule.Internal module where everything is exported, and then re-export the subset of that API which is considered public from a separate module MyModule.

@kl0tl kl0tl force-pushed the cannot-find-coerced-constructor-error branch from f654cb6 to 37030af Compare September 19, 2020 14:38
@kl0tl kl0tl force-pushed the cannot-find-coerced-constructor-error branch from 37030af to 0e76a88 Compare October 10, 2020 20:27
@kl0tl
Copy link
Member Author

kl0tl commented Oct 10, 2020

I’ve rebased this pull request on top of #3937.

There’s now a CannotFindCoercedCtor error for when the constructor isn’t exported at all (so we don’t even know if it’s a newtype) and a CannotFindCoercedNewtypeCtor error for when the newtype constructor is exported but not imported.

@hdgarrood
Copy link
Contributor

@kl0tl do you think we should try to get this into 0.14.0? I think it would be good if we can. Would you mind resolving the conflicts when you get a chance?

@kl0tl kl0tl force-pushed the cannot-find-coerced-constructor-error branch from 0e76a88 to 158c7ba Compare December 19, 2020 10:56
@kl0tl
Copy link
Member Author

kl0tl commented Dec 19, 2020

I’ve rebased the pull request! The diff should be much nicer. I also added a DataDeclType field to the DataType constructor of TypeKind, so that a newtype constructor doesn’t have to be exported for us to know that it is a newtype. This allows us to get rid of the CannotFindCoercedCtor error and only throw CannotUnwrapHiddenNewtypeConstructor errors when we know that the failure is really due to an out of scope newtype constructor.

@kl0tl
Copy link
Member Author

kl0tl commented Dec 19, 2020

What do you think of CannotUnwrapHiddenNewtypeConstructor for the error? The constructor isn’t necessarily hidden after all, it might just not be imported. Perhaps CannotUnwrapOutOfScopeNewtypeConstructor would be more appropriate?

UnsupportedRoleDeclaration {} -> "UnsupportedRoleDeclaration"
RoleDeclarationArityMismatch {} -> "RoleDeclarationArityMismatch"
DuplicateRoleDeclaration {} -> "DuplicateRoleDeclaration"
CannotUnwrapHiddenNewtypeConstructor {} -> "CannotUnwrapHiddenNewtypeConstructor"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you explicitly invited bikeshedding, how about MissingConstructorImportForCoercible? It's an issue for both wrapping and unwrapping, and the error names seem to prefer describing a thing to be corrected over a thing the compiler failed to do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that’s better, thank you for the suggestion!

line $ "Duplicate role declaration for " <> markCode (runProperName name) <> "."

renderSimpleErrorMessage (CannotUnwrapHiddenNewtypeConstructor name) =
line $ "The newtype constructor " <> markCode (showQualified runProperName name) <> " is not in scope, it should be imported to allow coercions to and from its representation."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the other error messages, I think there should be a sentence break and line break between ‘... is not in scope’ and ‘it should be imported...’.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I turned “It should be imported to allow coercions to and from its representation” into its own sentence, on a new line.

@kl0tl kl0tl force-pushed the cannot-find-coerced-constructor-error branch from 8b5d427 to 4c6f9d4 Compare December 20, 2020 11:30
Copy link
Member

@thomashoneyman thomashoneyman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 from me, though this deserves a look from someone more familiar with the code as well. Thanks!

@hdgarrood
Copy link
Contributor

I actually feel like this should be a NoInstanceFound error rather than a separate code; the error is that the compiler can’t prove that it’s safe to coerce between the two types in question, which in PureScript is represented by the lack of a Coercible instance. I think it could be confusing that this error looks quite different from other NoInstanceFound errors which arise as a result of trying to coerce two types which aren’t Coercible.

I also have a slight worry with the wording of the error message, because to me the message sounds quite confident that the correct course of action is to import the constructor. In practice, if you receive this error, the answer may instead be “don’t do that”. For example, if you were trying to coerce an Array to a NonEmptyArray. I think a reminder that having the newtype constructor in scope will silence this error is warranted, but it should be secondary to the main piece of information, which is “I can’t prove that a coercion from type X to type Y is safe.”

@kl0tl
Copy link
Member Author

kl0tl commented Dec 27, 2020

Would you prefer that we add the MissingConstructorImportForCoercible constructor to ErrorMessageHint instead and accumulate hints in solveWanteds, so that the following:

module Example.N where

newtype N a = N a
module Example where

import Example.N (N)
import Safe.Coerce (coerce)

example :: forall a. a -> N a
example = coerce

yields

Error found:
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)
                                

  The newtype constructor N is not in scope.

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.

?

@hdgarrood
Copy link
Contributor

That sounds more appealing to me, yes. I also think it would be good to make the connection between the constructor being in scope and the coercion being possible slightly more clear in the hint; perhaps something along the lines of "Performing this coercion requires the data constructor N to be in scope"?

@hdgarrood hdgarrood merged commit 53b323b into purescript:master Dec 27, 2020
@kl0tl kl0tl deleted the cannot-find-coerced-constructor-error branch January 22, 2021 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants