-
Notifications
You must be signed in to change notification settings - Fork 571
Walk under constrained types during role inference #3906
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
Conversation
I’ve managed to reproduce the example in the paper with a foreign data type, but this rely on a lax role declaration and isn’t something that could be avoided by assigning nominal roles to the constraint arguments of constrained types. module Example where
import Prelude
import Safe.Coerce (coerce)
import Unsafe.Coerce (unsafeCoerce)
foreign import data ShowDict :: Type -> Type
type role ShowDict representational
intro :: forall a. Show a => ShowDict a
intro = (unsafeCoerce :: (forall a. a -> a) -> (forall a. Show a => ShowDict a)) \dict -> dict
elim :: forall a b. (Show a => b) -> ShowDict a -> b
elim = unsafeCoerce ($)
withShowDict :: forall a. ShowDict a → a → String
withShowDict dict x = elim (show x) dict
newtype HTML = MkHTML String
instance showHTML :: Show HTML where
show (MkHTML s) = "HTML:" <> show s
showStringDict :: ShowDict String
showStringDict = intro
showHTMLDict :: ShowDict HTML
showHTMLDict = intro
badShowDict :: ShowDict HTML
badShowDict = coerce showStringDict
|
I’ve found a reason to assign at least a representational role to the constraint arguments of constrained types! Not considering the constraint of constrained types at all when inferring roles allows to provide arbitrary dictionaries: module Example where
import Prelude
import Safe.Coerce (coerce)
newtype Shown a = Shown (Show a => String)
shownUnit :: Shown Unit
shownUnit = Shown $ show unit
example :: String
example = case (coerce :: Shown Unit -> Shown Ordering) shownUnit of
Shown shown -> shown Which can lead to runtime exceptions:
The newtype is necessary here because we don’t solve constraints for constrained types in general, so
|
I feel like things appearing as parameters to constraints under data constructors should probably be nominal, but I’ll need to reread the relevant part of the paper to be sure. |
The paper doesn’t mention constraints under data constructors in section 4.5 Role inference but given how they desugar I think we ought to walk the members of the type class. This doesn’t break type classes coherence and it’s enough to reject unsafe coercions: module Example where
import Prelude
import Data.Ord.Down (Down)
import Data.Set (Set)
import Data.Set as Set
import Effect (Effect)
import Effect.Console (log)
import Safe.Coerce (coerce)
newtype Shown a = Shown (Show a => String)
class Insert a where
insert :: a -> Set a -> Set a
instance insertOrd :: Ord a => Insert a where
insert = Set.insert
newtype Inserted a b = Inserted (Insert a => b)
insertedInt :: Inserted Int (Shown (Set Int))
insertedInt = Inserted $ Shown $ show $ insert 0 $ Set.fromFoldable [1, 2]
main :: Effect Unit
main = case (coerce :: Inserted Int (Shown (Set Int)) -> Inserted (Down Int) (Shown (Set (Down Int)))) insertedInt of
Inserted inserted -> case inserted of
Shown shown -> log shown
Given a > node -e 'require("./output/Example").main()'
(fromFoldable ((Down 1) : (Down 2) : (Down 0) : Nil)) Which is rather unexpected for a |
I’ve managed to capture a dictionary under a newtype! module Example where
import Prelude
import Safe.Coerce (coerce)
newtype Shown a = Shown ((Show a => a -> String) -> String)
shown :: forall a. Shown a -> String
shown = case _ of
Shown f -> f show
newtype HTML = MkHTML String
instance showHTML :: Show HTML where
show (MkHTML s) = "HTML:" <> show s
shownString :: Shown String
shownString = Shown (\f -> f "Hello")
shownHTML :: Shown HTML
shownHTML = Shown (\f -> f (MkHTML "Hello"))
badShownHTML :: Shown HTML
badShownHTML = coerce shownString
Anything more permissive than nominal breaks coherence after all 🙈 |
12b5ad7
to
bdb83f3
Compare
The code all looks good, but I think it would be worth adding a test where a type variable mentioned in a constraint is shadowing one of the type parameters (as I don't think we have one already). For example, I think this should be accepted: class C a
data Test a = Test a (forall a. C a => a)
type role Test representational |
bdb83f3
to
7fb9271
Compare
You’re right, we ought to ensure we don’t assign nominal roles to parameters shadowed by locally bound variables. I added a the test you suggested. |
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.
Thanks!
940213d
to
3083e77
Compare
Would you mind merging master here to see if CI passes too? |
3083e77
to
ea835ff
Compare
I rebased on top of master, tests pass again 😌 |
🎉 |
The Safe Zero-cost Coercions for Haskell paper argues that type instances arguments should get nominal roles in section 3.2 Preserving class coherence with the following exemple:
I can‘t think of a way to store an instance dictionary under a constructor in PureScript so we should be able to disregard the constraint arguments of constrained types entirely.
Fix #3868.