Использование стандартных библиотек генериков haskell для типизированных изоморфизмов типов
Существует несколько универсальных библиотек с многочисленными перекрывающимися модулями только на платформе Haskell (syb
, Data.Typeable
, Data.Data
, GHC.Generics
), но у меня возникают проблемы с очень простой общей задачей программирования.
Я хочу иметь возможность конвертировать между типами одной и той же формы, т.е. я хочу использовать полиморфную, типизированную функцию преобразования между изоморфными типами, по существу, что предлагается в конце этот документ (PDF), где упоминаются семейства с индексированным типом.
Я не заинтересован в том, чтобы сломать мой шаблон, а скорее, чтобы создавать новые библиотеки вокруг сумм и абстракций продукта.
Вопрос ниже представлен в терминах GHC.Generic
, который, как я думал, наиболее близок к тому, что мне нужно, но приветствуем другие решения.
Следующие два типа имеют одинаковую форму
data Pair = Pair Char Int deriving (Generic, Show)
data Pair2 = Pair2 Char Int deriving (Generic, Show)
Я хочу преобразовать значения между ними с помощью GHC.Generics. Ниже приведено описание typecheck из-за всех параметров phantom и других нонсенсов:
f :: Pair -> Pair2
f = to . from
В конечном итоге я хочу использовать функцию fromInteger
, которая имеет полиморфное возвращаемое значение для любого экземпляра Generic
(или любого другого класса, который мог бы поддерживать этот). Наверное, я ищу что-то вроде 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
С помощью вышеизложенного мы можем делать все, что я хочу:
*Main> (coerce $A 'a' 1) :: B
B 'a' 1
*Main> (coerce $A 'a' 1) :: A
A 'a' 1
EDIT: Вот как работает функция Натана Хауэлла f
.
Вопросы
-
Возможно ли это сделать с библиотеками, находящимися в настоящее время на платформе haskell?
-
Если нет, можно ли определить библиотеку, которая использовала существующий механизм deriving
для Generic
, Data
и т.д., не прибегая к TH?
Ответы
Ответ 1
Если "одинаковой формы" означает, что типы данных равны именам конструкторов, селекторам записей и синонимам типов, тогда преобразование данных типа так же просто, как и представление перемещения.
{-# 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
Тестовый пример:
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
Несколько примеров позволяют более интересные преобразования:
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
Это возможно и относительно безболезненно. В отличие от использования unsafeCoerce
напрямую, вы получите разрыв сборки, если типы не совпадают. Вероятно, вы можете полагаться на ограничения равенства на f
, чтобы обеспечить достаточную безопасность типа времени компиляции для использования unsafeCoerce
и избежать работы с семейством Rep
.
{-# 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
Выполнение этого результата дает ожидаемый результат:
*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