Is it possible to establish Coercible instances between custom types and standard library ones?
Asked Answered
F

3

10

For a simple example, say I want a type to represent tic-tac-toe marks:

data Mark = Nought | Cross

Which is the same as Bool

Prelude> :info Bool
data Bool = False | True    -- Defined in ‘GHC.Types’

But there's no Coercible Bool Mark between them, not even if I import GHC.Types (I first thought maybe GHC needs Bool's defining place to be visible), the only way to have this instance seems to be through newtype.

Probably I could have defined newtype Mark = Mark Bool and define Nought and Cross with bidirectional patterns, I wish there's something simpler than that.

Feint answered 22/2, 2021 at 2:8 Comment(1)
There's unsafeCoerce, which will probably work but which would not be recommended. The only way to get a Coercible instance would be local, using unsafeCoerce. case unsafeCoerce (Coercion @() @()) :: Coercible Mark Bool of Coercion -> ..... I wouldn't really suggest it unless there's a desperate performance need.Genevagenevan
P
12

Unfortunately, you're out of luck. As the documentation for Data.Coerce explains, "one can pretend that the following three kinds of instances exist:"

  • Self-instances, as in instance Coercible a a,

  • Instances for coercing between two versions of a data type that differ by representational or phantom type parameters, as in instance Coercible a a' => Coercible (Maybe a) (Maybe a'), and

  • Instances between new types.

Furthermore, "Trying to manually declare an instance of Coercible is an error", so that's all you get. There are no instances between arbitrarily different data types, even if they look similar.


This may seem frustratingly limiting, but consider this: if there were a Coercible instance between Bool and Mark, what's stopping it from coercing Nought to True and Cross to False? It may be that Bool and Mark are represented in memory the same way, but there is no guarantee that they are semantically similar enough to warrant a Coercible instance.


Your solution of using a newtype and pattern synonyms is a great, safe way to get around the problem, even if it is a little annoying.

Another option is to consider using Generic. For instance, check out the idea of genericCoerce from this other question

Palish answered 22/2, 2021 at 2:37 Comment(0)
S
8

Coercible Bool Mark is not required. Mark-instances can be derived via Bool without it.

Generic types whose generic representations (Rep) are Coercible can be converted to each other:

   from           coerce              to
A -----> Rep A () -----> Rep Via () -----> Via 

For the datatype Mark this means instances (Eq, ..) can be derived via instances of Bool.

type Mark :: Type
data Mark = Nought | Cross
 deriving
 stock Generic

 deriving Eq
 via Bool <-> Mark

How does Bool <-> Mark work?

type    (<->) :: Type -> Type -> Type
newtype via <-> a = Via a

First we capture the constraint that we can coerce between the generic representation of two types:

type CoercibleRep :: Type -> Type -> Constraint
type CoercibleRep via a = (Generic via, Generic a, Rep a () `Coercible` Rep via ())

Given this constraint we can move from a to it via type, creating intermediate Reps:

translateTo :: forall b a. CoercibleRep a b => a -> b
translateTo = from @a @() >>> coerce >>> to @b @()

Now we can easily write an Eq instance for this type, we assume an Eq via instance for the via type (Bool in our case)

instance (CoercibleRep via a, Eq via) => Eq (via <-> a) where
 (==) :: (via <-> a) -> (via <-> a) -> Bool
 Via a1 == Via a2 = translateTo @via a1 == translateTo @via a2

The instance for Semigroup requires translating via back to a

instance (CoercibleRep via a, Semigroup via) => Semigroup (via <-> a) where
 (<>) :: (via <-> a) -> (via <-> a) -> (via <-> a)
 Via a1 <> Via a2 = Via do
  translateTo @a do
     translateTo @via a1 <> translateTo @via a2

Now we can derive Eq and Semigroup!

-- >> V3 "a" "b" "c" <> V3 "!" "!" "!"
-- V3 "a!" "b!" "c!"
type V4 :: Type -> Type
data V4 a = V4 a a a a
 deriving
 stock Generic

 deriving (Eq, Semigroup)
 via (a, a, a, a) <-> V4 a

Using a newtype from the beginning avoids this boilerplate but once it's up it can be reused. It is simple to write a newtype and use pattern synonyms to cover it up.

Spill answered 26/2, 2021 at 11:31 Comment(3)
I don't see a definition of <-> anywhere. Could you include that?Genevagenevan
GenericIso is a mystery too. Is this answer missing some imports?Genevagenevan
I fixed the missing definitions, I had renamed it to CoercibleRep halfway throughSpill
N
7

This isn’t possible yet, and pattern synonyms are a good solution for now. I often use code like this to derive useful instances for a type that happens to be isomorphic to an existing primitive type.

module Mark
  ( Mark(Nought, Cross)
  ) where

newtype Mark = Mark Bool
  deriving stock (…)
  deriving newtype (…)
  deriving (…) via Any
  …

pattern Nought = Mark False
pattern Cross = Mark True

Coercion between unrelated ADTs is also not on the list of permitted unsafe coercions. Last I knew, in practice in GHC, coercions between Mark and Bool will work only if the values in question are fully evaluated, because they have a small number of constructors, so the constructor index is stored in the tag bits of the pointer at runtime. But an arbitrary thunk of type Mark or Bool can’t be coerced reliably, and the method doesn’t generalise to types with more than {4, 8} constructors (on resp. {32, 64}-bit systems).

Moreover, the code generator and runtime representation of objects both change periodically, so even if this works now (I don’t know), it will probably break in the future anyway.

My hope is that we get a generalised Coercible in the future that can accommodate more coercions than just newtype-of-TT, or even better, that allows us to specify a stable ABI for a data type. To my knowledge, no one is actively working on that in Haskell, although there is some similar work going on in Rust for safe transmute, so maybe someone will smuggle it back over to functional-land.

(Speaking of ABI, you could use the FFI for this, and I’ve done so in circumstances where I was already writing foreign code and knew the Storable instances matched. alloca a suitably sized buffer, poke a value of type Bool into it, castPtr the Ptr Bool into a Ptr Mark, peek the Mark out of it, and unsafePerformIO the whole shebang.)

Nebulous answered 22/2, 2021 at 3:11 Comment(4)
I'm pretty sure unsafeCoerce will be fine, though never officially supported. Whether a value is evaluated/tagged or not shouldn't be relevant. In older (basically, non-bleeding-edge) GHC versions, the bigger danger was that the simplifier could float coercions around in bad ways, basically taking a use of unsafeCoerce @Mark @Bool and letting the coercion axiom Mark ~ Bool float somewhere invalid, like maybe tripping specialization rules. I think that's fixed now.Genevagenevan
One situation where you have to be extra careful with unsafeCoerce is when different types are compiled with different options affecting unpacking. data Foo = Foo {-# UNPACK #-} !Bar will only unpack Bar with optimizations enabled, and of course -XStrict, -funbox-strict-fields and -fno-unbox-small-strict-fields will have their effects.Genevagenevan
This Storable plan is bizarre. In what circumstances is that better than writing a pure Haskell translation function with no unsafe features at all? (Side benefit of staying pure: the behavior is transparent enough that the compiler can remind you that the translation needs to be updated when one or the other type's definition changes.)Dania
@DanielWagner: lol it’s meant to read as bizarre, it’s not better. In my case I had an array of data marshalled to C++, mutated, returned, and decoded to a different type to record that the transformation had happened in the foreign land. In terms of both correctness and performance, it’s not worthwhile for a single value.Nebulous

© 2022 - 2024 — McMap. All rights reserved.