Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upImplement `Coercible` for safe zero-cost coercions #3351
Conversation
kcsongor
reviewed
May 4, 2018
| -- (e.g. in the case `data F a b = F (a (F a b))`, the use of `a` as | ||
| -- a function/constructor would trigger this case) | ||
| _ -> | ||
| walk t1 |
This comment has been minimized.
This comment has been minimized.
kcsongor
May 4, 2018
Contributor
It looks like this case should mark all t2s as representational:
data F a b = F (a b)
As a is polymorphic, we don't know what its role annotation is going to be, therefore we should be careful and assume that its argument is representational (because it very well could be!).
That means that for F, b would be inferred representational as well.
I think the current implementation will infer phantom for b, allowing to coerce
F Maybe Int and F Maybe String
This comment has been minimized.
This comment has been minimized.
kcsongor
May 4, 2018
Contributor
It looks like this case should mark all
t2sasrepresentational:
Or rather, just walk all t2s, as that will mark all type variables representational anyway, and ensure that we don't accidentally mark phantom things representational (which would be safe, but undesirable).
So:
data G a b = G (a (Phantom b))
data Phantom a = Phantom
will not get tricked into marking b as representational.
Note: this case is actually interesting, because it differs from what GHC does. Not having to consider nominal roles results in a non-trivial difference:
For G above, GHC would infer
type role G representational nominal
This is because with nominal as a possibility, we must assume that a's argument can be nominal, therefore Phantom b is in a nominal role. However, nominal is "infectious": if Phantom b is nominal, then all type variable therein must be nominal too (b here). This being the case, GHC rightly marks b as nominal, and stops. In the paper, this is accounted for by the rules that mark all free variables nominal.
However, with only phantom and representational roles, we can do better than simply downgrading nominal to representational! Phantom b being in a representational role doesn't "infect" b: walking is sufficient to determine the above result.
As a result, GHC would never allow to coerce between G Maybe Int and G Maybe String - but we can!
kcsongor
reviewed
May 4, 2018
| -- argument is representational (since its use of our parameters is | ||
| -- important) and terminate if the argument is phantom. | ||
| TypeConstructor t1Name -> | ||
| let t1Roles = inferRoles env t1Name |
This comment has been minimized.
This comment has been minimized.
kcsongor
May 4, 2018
Contributor
What happens with mutually recursive types?
data F a = F (G a)
data G a = G (F a)
kcsongor
reviewed
May 4, 2018
| -- appears (with a default role of phantom) and that they appear in the | ||
| -- right order. | ||
| let ctorRoles = foldMap (foldMap walk . snd) ctors | ||
| in map (\(tv, _) -> (tv, fromMaybe Phantom (lookup tv ctorRoles))) tvs |
This comment has been minimized.
This comment has been minimized.
kcsongor
May 4, 2018
Contributor
This use of lookup seems like we're taking the role of the first occurrence of a in the constructors.
Does it mean that for
data Phantom a = Phantom
data F a = MkF (Phantom a) a
we get
type role F phantom
?
This comment has been minimized.
This comment has been minimized.
lunaris
May 7, 2018
I thought you were right but after some testing I think I actually get away with this due to the fact that Phantom parameters are not realised at all. E.g. for the types:
data Phantom a = Phantom
data F a = MkF (Phantom a) awalking the MkF constructor for F yields:
[ [], [("a", Representational)] ]That is, the Phantom use results in [], not ("a", Phantom). So this works. I think this is OK but am open to alternative implementations.
This comment has been minimized.
This comment has been minimized.
kcsongor
May 7, 2018
Contributor
Ah, you're right - I missed this. Perhaps then walk should just return a list of representational types? (as it already does, but its type is somewhat misleading)
This comment has been minimized.
This comment has been minimized.
|
Without explicit role annotations, how can the programmer ensure safety when using phantom types that should prevent coercibility? There are many thinkable instances of this with the FFI. |
This comment has been minimized.
This comment has been minimized.
|
@rightfold you certainly raise a good point, I would imagine that by default all arguments to foreign types have to be representational |
This comment has been minimized.
This comment has been minimized.
lunaris
commented
May 7, 2018
•
|
I've implemented @kcsongor's suggestion to infer |
This comment has been minimized.
This comment has been minimized.
|
I like the idea of using |
This comment has been minimized.
This comment has been minimized.
lunaris
commented
May 8, 2018
|
As it currently stands that's what this branch does now. I've prototype role declarations in another branch but that's a separate discussion I think. |
lunaris
added some commits
Apr 26, 2018
lunaris
force-pushed the
lunaris:feature/coercible
branch
from
6001065
to
9e243ab
Jun 19, 2018
This comment has been minimized.
This comment has been minimized.
lunaris
commented
Jun 19, 2018
|
I've rebased this on
module Safe.Coerce (coerce) where
import Prim.Coerce (class Coercible)
import Unsafe.Coerce (unsafeCoerce)
coerce :: forall a b. Coercible a b => a -> b
coerce = unsafeCoerce
|
This comment has been minimized.
This comment has been minimized.
|
Just a minor note, I think most of Other than that, this looks good to me and I hope it gets merged soon. |
LiamGoodacre
reviewed
Jun 19, 2018
|
I'll do a proper review tonight. |
| , "First, as a trivial base-case, reflexivity - any type has the same" | ||
| , "representation as itself:" | ||
| , "" | ||
| , " instance Coercible a a" |
This comment has been minimized.
This comment has been minimized.
LiamGoodacre
Jun 19, 2018
Member
The example code here and later on aren't in PureScript syntax (no instance naming). What do you think about:
instance reflexivity :: Coercible a a| -- recursive call's result. This is true, but fine, since this tuple will | ||
| -- never be looked up when building the final result, which only looks at | ||
| -- the variables defined as parameters to the type. | ||
| walk t |
This comment has been minimized.
This comment has been minimized.
LiamGoodacre
Jun 19, 2018
Member
In data T a = T (forall a. a -> a) will this produce Representational a, when it should be Phantom?
LiamGoodacre
reviewed
Jun 19, 2018
| -- reduce the first or second argument -- if the constraint is | ||
| -- solvable, either path will yield the same outcome. Consequently we | ||
| -- just try the first argument first and the second argument second. | ||
| ws <- coercibleWanteds env a b <|> coercibleWanteds env b a |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Just tried this situation and got no instance found via -- existing tests
data G a b = G (a (Phantom2 b))
gToG :: G Maybe Int -> G Maybe String
gToG = coerce
-- new stuff
newtype OptionIn a = OptionIn (Maybe a)
newtype OptionOut a = OptionOut (Maybe a)
gToGN :: G OptionIn Int -> G OptionOut String
gToGN = coerce |
This comment has been minimized.
This comment has been minimized.
|
Following from @kcsongor's earlier comment: data MutI f a = MutI a (Maybe (f a))
newtype MutA a = MutA (MutI MutB a)
newtype MutB t = MutB (MutI MutA t)
mutAToMutB :: MutA Int -> MutB Int
mutAToMutB = coerceGives the error:
|
This comment has been minimized.
This comment has been minimized.
|
The following causes the compiler to loop and eat all your memory newtype X a = X (X (X a))
xToY :: X Int -> X (X Int)
xToY = coerce |
This comment has been minimized.
This comment has been minimized.
|
Not sure if that previous one is a bug with this or not. As the following also loops: newtype X a = X (X (X a))
xToY :: X Int -> X (X Int)
xToY = ?ohNoWill make a new issue for this. |
lunaris commentedMay 4, 2018
•
edited
This PR adds
Coerciblefor safe zero-cost coercions, as per the similar feature in GHC Haskell. It's intended to be the first step towards implementingderiving via(#3302). Compiler support only boils down to the constraintCoercible-- the actual functioncoerce :: forall a b. Coercible a b => a -> bcan be implemented in library code (asunsafeCoerce, thus receiving the same inlining/elimination treatment and so being zero-cost). The tests will fail because of this point because I need to find a place to putSafe.Coerce(the module which exposescoerce). Happy to take input on how to best accomplish this.Changes
Define a new built-in module,
Prim.Coercewhich exports a singletype class,
Coercible a b, which relates types with identical runtimerepresentations.
Extend the type checker's class solver to solve constraints of the
form
Coercible a bautomatically, as per the rules in the paper "SafeZero-Cost Coercions for Haskell" (though simpler due to the absence of
some of Haskell's features like type families).
As part of the above point, add support for role inference to the type
checker.