Haskell types with coercible representations identical to their C counterparts?
Asked Answered
T

1

8

How can I determine if a Haskell type has an equivalent Coercible instance on a given platform?

I've just been told about Coercible in GHC 7.8, which seems great. In that context, I guess an equally good question to solve my specific problem is: Is there a way to interrogate GHC about which pairs of types a, b there is a Coercible a b instance for (on the current platform, say)?

It seems to me that for coerce :: Coercible a b => a -> b to be useful in a compiler- and platform-agnostic program, one would need to know – preferably only at compile-time, but possibly also explicitly when writing the code – whether a given Coercible a b instance exists on the given platform and use a slower non-noop fallback otherwise (by means of CPP, I guess).

Follow-up question: Would it make sense for GHC to provide a function

coerceOrConvert :: (a -> b) -> a -> b

with the property that coerceOrConvert f is

  • coerce if there is a Coercible a b instance for the present GHC version and platform

  • f if not

I realize this makes little sense for ordinary typeclasses, but Coercible seems far from ordinary, so it's hard for me to tell…

Taliataliaferro answered 17/3, 2014 at 16:3 Comment(0)
R
5

Typically the kind of coercion handled in Haskell comes in two flavors: representational equality (via newtype and Coercible) and new information about type variables (via Typeable). The second type has little to do with runtime representation, so I'll just describe the Coercible/newtype mechanism.

It's a guarantee that that newtype changes only the type information and not the underlying representation, thus if we have (the standard example)

newtype Age = Age { unAge :: Int }

then we should be able to feel confident that something like

instance Num Age where
  Age a + Age b = Age (a + b)
  ...

is exactly as quick as (+) on Int is---i.e. there is no pointer indirection going on behind the scenes. In fact, GHC eliminates the Age constructor here with no difficulty. The challenge comes about when we want to do something like

map Age :: [Int] -> [Age]

since Int and Age are structurally identical this should be a no-op as well---all we must do is satisfy the type system at compile time and then throw away map Age operationally at runtime. Sadly, this isn't the case since the map will still traverse our list even if it does nothing at each stage.

In situations where a lot of newtypes get thrown around but we also want GHC to produce the tightest compiled code you might see (dangerous, careful) uses of unsafeCoerce

unsafeCoerce :: [Int] -> [Age]

In this case unsafeCoerce is "safe" as we know that these two types are runtime identical. Also, since unsafeCoerce operates purely at the type level and is a true no-op at runtime we know that unlike map Age, unsafeCoerce is truly an O(0) coercion.

But it's pretty dangerous.

Coercible hopes to fix this by allowing instantiations like

instance Coercible a b => Coercible [a] [b] where coerce = unsafeCoerce

so that the Haskell typeclass machinery allows for coerce to be used only when safe, unlike unsafeCoerce. To ensure this is the case it must not be possible for malicious instances of Coercible to be built. To this end all Coercible instances are built by the compiler based off uses of newtype.

As a final note, when you truly dive into how Coercible works you'll have to understand the new Haskell Role system which allows developers to annotate whether or not a newtype should allow for a coercion. This is clearly outlined in [the documentation for the Coercible class] (http://www.haskell.org/ghc/docs/7.8.1-rc2/html/libraries/base-4.7.0.0/Data-Coerce.html).

Reductive answered 17/3, 2014 at 16:24 Comment(6)
Thank you. This is very informative, but a question still remains: Is there a (probably platform-dependent) list, or (better yet) a way to generate such a list from GHC, of which standard types a and b there is a Coercible a b instance for? (By "standard types" I probably mean something like "the types specified in the Haskell report", or some subset of that).Taliataliaferro
I'll add a more concrete scenario: Fix concrete types a and b. Suppose I want to write a conversion function f :: a -> b. I suspect that with GHC on many platforms, there is a Coercible a b instance, but not on all platforms (this could happen, right?). Suppose also there is a general, slow, conversion function g :: a -> b that always does the right thing without assuming anything about underlying representations (let's say g is in the Haskell report). Can I somehow (using CPP if I must) say "let f = coerce whenever there is a Coercible a b instance, otherwise let f = g"?Taliataliaferro
I don't think that's possible. The Haskell Report guarantees that newtypes have the same representation, but doesn't say much else about how things ought to be implemented. You can do things specific to GHC's implementation of Haskell—I think if you look into the internals of the reflection package you see this a lot—but those things tend to be highly unsafe. Coercible as designed is independent of any actual implementation details—it works entirely off the newtype promises.Reductive
I am indeed asking precisely about GHC's specific implementation, and I understand that Coercible is independent of implementation details... I guess what I'm asking is: Is there an easily accessible list, with some guarantees that it won't change in a minor version, of which (standard) types are implemented with newtype in GHC? Or, alternatively, could it be possibly to ask a specific version of GHC on a specific platform, "hey, do you simply newtype this standard Haskell type from this standard Haskell type?", perhaps by asking for Coercible instances? I feel it would be helpful.Taliataliaferro
For example, I'd like to go: "Are we running GHC?" If no, do slow conversions. If yes, ask GHC "does this particular version on this particular platform define newtype CDouble = CDouble Double (or, is there a Coercible CDouble Double instance)?" If no, do slow conversions. If yes, convert with coerce. This sounds really useful for a lot of numerical stuff involving FFI.Taliataliaferro
Well, you can be guided by the restriction that coerce only works when the newtype constructor is in scope. That means that any types which are publicly declared to be newtype-equivalent will show up in the docs. Many of the FFI types declared in Foreign.C.Types are exactly represented as newtypes. If you want to get to the explicit underlying memory then you'll want to explore the unboxed types in GHC.Exts but notice that these are explicitly not newtypes, although GHC may decide during compilation to unbox a type.Reductive

© 2022 - 2024 — McMap. All rights reserved.