问题
There are several generics libraries with numerous overlapping modules in just the Haskell Platform alone (syb
, Data.Typeable
, Data.Data
, GHC.Generics
), but I'm having trouble with a very basic generic programming task.
I want to be able to convert between types of the same shape, i.e. I want a polymorphic, typed conversion function between isomorphic types, essentially what is offered at the end of this paper(PDF) where indexed type families are mentioned.
I'm not concerned with scrapping my boilerplate, but rather with being able to build new libraries around sum and product abstractions.
The question below is in terms of GHC.Generic
which I thought was closest to what I needed, but other solutions are welcome.
The following two types have the same shape
data Pair = Pair Char Int deriving (Generic, Show)
data Pair2 = Pair2 Char Int deriving (Generic, Show)
I want to convert values between them using GHC.Generics. The following fails to typecheck because of all the phantom parameters and other nonsense:
f :: Pair -> Pair2
f = to . from
Ultimately I want a function akin to fromInteger
that has a polymorphic return value for any Generic
(or whatever other class could support this) instance. I guess I'm looking for something like GHC.Generics
:
--class:
type family NormalForm a
class ToGeneric a where
to :: a -> NormalForm a
class FromGeneric b where
from :: NormalForm b -> b
--examples:
data A = A Char Int deriving Show
data B = B Char Int deriving Show
type instance NormalForm A = (Char,Int)
instance ToGeneric A where
to (A a b) = (a,b)
instance FromGeneric A where
from (a,b) = A a b
type instance NormalForm B = (Char,Int)
instance ToGeneric B where
to (B a b) = (a,b)
instance FromGeneric B where
from (a,b) = B a b
-- the function I'm looking for
coerce :: (ToGeneric a, FromGeneric b, NormalForm a ~ NormalForm b)=> a -> b
coerce = from . to
With the above we can do everything I want:
*Main> (coerce $A 'a' 1) :: B
B 'a' 1
*Main> (coerce $A 'a' 1) :: A
A 'a' 1
EDIT: This is how Nathan Howell's f
function seems to work below, actually.
Questions
Is this possible to do with libraries currently in the haskell platform?
If not, could a library be defined that leveraged the existing
deriving
mechanism forGeneric
,Data
, etc. without resorting to TH?
回答1:
If "of the same shape" means that datatypes are equal up to constructor names, record selectors and type synonyms then datatype conversion is as simple as traversing representation.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts, FlexibleInstances #-}
import GHC.Generics
conv
:: (Generic a, Generic b, Conv (Rep a) (Rep b))
=> a -> b
conv = to . cv . from
class Conv a b where
cv :: a x -> b x
-- skip irrelevant parts: datatype name, constructor name, selector
instance Conv f1 f2 => Conv (M1 i1 c1 f1) (M1 i2 c2 f2) where
cv = M1 . cv . unM1
instance (Conv a1 a2, Conv b1 b2) => Conv (a1 :*: b1) (a2 :*: b2) where
cv ~(a :*: b) = cv a :*: cv b
instance (Conv a1 a2, Conv b1 b2) => Conv (a1 :+: b1) (a2 :+: b2) where
cv (L1 a) = L1 $ cv a
cv (R1 b) = R1 $ cv b
-- copy values
instance Conv U1 U1 where cv = id
instance Conv (K1 R c) (K1 R c) where cv = id
Test case:
data A = A1 String Int | A2 (Int,Int) deriving (Generic, Show)
data B = B1 [Char] Int | B2 { xy :: (Int,Int) } deriving (Generic, Show)
data X = X Int Int deriving (Generic, Show)
*Main> conv $ X 3 14 :: (Int,Int)
(3,14)
*Main> conv $ A1 "hello" 42 :: B
B1 "hello" 42
*Main> conv $ A2 (13,42) :: B
B2 {xy = (13,42)}
Update
A few more instances allow more interesting conversions:
instance Conv U1 (M1 S s (K1 R ())) where
cv _ = M1 $ K1 ()
-- *> conv (Nothing :: Maybe Int) :: Either () Int
-- Left ()
instance Conv (M1 S s (K1 R ())) U1 where
cv _ = U1
-- *> conv (Left () :: Either () Int) :: Maybe Int
-- Nothing
-- this one requires OverlappingInstances
instance (Generic c1, Generic c2, Conv (Rep c1) (Rep c2))
=> Conv (K1 R c1) (K1 R c2)
where
cv (K1 x) = K1 $ conv x
-- *> conv (Right Nothing :: Either () (Maybe Int)) :: Maybe (Either () Int)
-- Just (Left ())
-- data List a = Empty | Cons a (List a) deriving (Generic, Show)
-- *> conv [1,2,3::Int] :: List Int
-- Cons 1 (Cons 2 (Cons 3 Empty))
回答2:
It is possible, and relatively painless. Unlike using unsafeCoerce
directly, you'll get a build break if the types don't line up. You can probably rely on the equality constraints on f
to provide enough compile time type safety to use unsafeCoerce
and avoid working with the Rep
family.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeFamilies #-}
import GHC.Generics
data Pair1 = Pair1 Char Int deriving (Generic, Show)
data Pair2 = Pair2 Char Int deriving (Generic, Show)
data Triple1 = Triple1 Char Int Double deriving (Generic, Show)
data Triple2 = Triple2 Char Int Double deriving (Generic, Show)
f :: (Generic a, Generic c, Rep a ~ D1 da (C1 ca f), Rep c ~ D1 db (C1 cb f))
=> a -> c
f = to . M1 . M1 . unM1 . unM1 . from
-- this might also be acceptable:
-- f = unsafeCoerce
p1 :: Pair1 -> Pair2
p1 = f
p2 :: Pair2 -> Pair1
p2 = f
t1 :: Triple1 -> Triple2
t1 = f
t2 :: Triple2 -> Triple1
t2 = f
Running it yields the expected result:
*Main> p1 $ Pair1 'x' 1
Pair2 'x' 1
*Main> p2 $ Pair2 'x' 1
Pair1 'x' 1
*Main> t1 $ Triple1 'y' 2 3.0
Triple2 'y' 2 3.0
*Main> t2 $ Triple2 'y' 2 3.0
Triple1 'y' 2 3.0
来源:https://stackoverflow.com/questions/11887666/using-standard-haskell-generics-libraries-for-typed-type-isomorphisms