Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning a reified type using Data.Reflection in Haskell

This is a type question regarding Data.Reflection in Haskell. Reflection lets me take an Int and convert it to a type.

The functions f and g below are our best attempts at something reasonable, if you have a better way, let's have it!

For example, I could add numbers mod 41 by doing something like:

import Data.Reflection
import Data.Proxy

newtype Zq q i = Zq i deriving (Eq)
instance (Reifies q i, Integral i) => Num (Zq q i) where
   (...)
zqToIntegral :: (Reifies q i, Integral i) => Zq q i -> i
   (...)

f :: forall i . (Integral i) => i -> (forall q . Reifies q i => Zq q i) -> i
f modulus k = 
    reify modulus (\ (_::Proxy t) -> zqToIntegral (k :: Zq t i)

Then

>>:t (f 41 (31+15))
(f 41 (31+15)) :: Integral i => i

However, we would like to write a function like:

g :: forall i . (Integral i) => i -> (forall q . Reifies q i => Zq q i) -> Zq q i
g modulus k = 
    reifyIntegral modulus (\ (_::Proxy t) -> (k :: Zq t i)

and would like to get:

>>:t (g 41 (31+15))
(g 41 (31+15)) :: <some type info> => Zq q i

The difference is that we would like to be able to return a type that uses a reified int. At least one problem with the definition above is that the rank-2 type q is not visible to the return type.

The signature for reify in Data.Reflection is

reify :: a -> (forall s. Reifies s a => Proxy s -> r) -> r

which as far we can tell requires the rank-2 type, and we don't know (if it is indeed possible) how to expose this type to the return type of the function.

like image 231
crockeea Avatar asked Nov 13 '22 04:11

crockeea


1 Answers

You can lift a value to a type. But the type can't escape the function that does the lifting. That's just how rank 2 types work. Imagine if we could write g as you describe. Then, given some user-input value, we could get a Zq of varying qs at runtime.

But then what could we do with them?

If we have two Zq with the same q, we can add them. But we wouldn't know that they were the same q or not until runtime! And that's too late to check types, since we need to decide if we can add them or not at compile time. If you ignore the fact that q is in a phantom position, this is the same reason you can't have a function that returns an Int or a Bool at compile time, based on input (you can use an Either, of course).

So, as noted in the comment, you can do a few different things.

One thing you could do is just return the modulus (i.e. the value-level version of the q) at runtime, along with your result. Then you can always use it later.

That looks like this:

g :: forall i . (Integral i) => i -> (forall q . Reifies q i => Zq q i) -> (i,i)
g modulus k = reify modulus (\ m@(_::Proxy t) -> (zqToIntegral (k :: Zq t i), reflect m))

Another thing you can do is use existentials like so:

data ZqE i = forall q. ZqE (Zq q i)

h :: forall i . (Integral i) => i -> (forall q . Reifies q i => Zq q i) -> ZqE i
h modulus k = reify modulus (\ (_::Proxy t) -> ZqE (k :: Zq t i))

Now the only thing we can do with our ZqE is unpack it and return something else which also doesn't expose q directly in the type.

Note that we have no way to know that the q in any two ZqE are equal, so we can't do operations combining them directly, but rather should be creating them all under the same reify call. And this is not a bug, but a feature!

like image 166
sclv Avatar answered Dec 31 '22 05:12

sclv