GHC用户指南在类型系列的Data instance declarations部分中显示了此示例:
data instance GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)
我习惯使用Either
类型,只要我们想要左值或右值,所以我希望GMapEither
以某种方式提供左或右变体,但似乎它总是同时存在:
{-# LANGUAGE TypeFamilies #-}
module Main where
import qualified Data.HashMap.Strict as H
data family GMap k :: * -> *
data instance GMap Int Int = GMapIntInt (H.HashMap Int Int)
deriving (Show, Eq)
data instance GMap String Int = GMapStringInt (H.HashMap String
Int)
deriving (Show, Eq)
data instance GMap (Either a b) v = GMapEither (GMap a v)
(GMap b v)
main :: IO ()
main = do
let m = GMapIntInt H.empty
print m
let m2 = GMapStringInt H.empty
print m2
let m3 = GMapEither m m2
let (GMapEither m3l m3r) = m3
print m3l
print m3r
我是否正确理解这里使用元组更合适,例如:
data instance GMap (a, b) v = GMapTuple (GMap a v) (GMap b v)
我认为这可能会提供更好的直觉。
答案 0 :(得分:2)
当我们进行抽象时,它通常具有所谓的“语义域”,这是抽象应该表示的东西。抽象的属性应该与语义域的属性匹配。 (当抽象具有类型类时,这称为type class morphism)。
GMap
显然试图表示某种映射操作。映射的原型示例是一个函数。还有像Data.Map
这样的有限地图,但它也有点假装是一种特殊的功能。
所以无论如何,我们应该期望GMap a b
具有与函数a -> b
类似的属性。如果GMap (a,b) v
被定义为等于(GMap a v, GMap b v)
,那么我们应该期望在语义域中存在相应的同构。因此,只需将所有GMap
转换为函数箭头->
,我们就得到:
f' :: ((a,b) -> v) -> (a -> v, b -> v)
g' :: (a -> v, b -> v) -> ((a,b) -> v)
g'
很容易进入类型检查,但有两种不同的实现,无法选择一种:
g' :: (a -> v, b -> v) -> ((a,b) -> v)
g' = (tl, tr) (x,y) = tl x
-- and
g' = (tl, tr) (x,y) = tr y
和f'
完全不可能
f' :: ((a,b) -> v) -> (a -> v, b -> v)
f' t = (\a -> ??? , \b -> ???)
在左侧???
,我们有a
,我们需要v
,但如果我们同时v
,我们只能构建t
一个a
和一个b
,我们没有任何地方可以获得我们需要的b
。同样的事情发生在元组的正确组件中。
没有明确的方法可以在一对(a,b) -> v
的函数和一对函数之间来回切换。因此,要宣布这两个方面相等,GMap
似乎不正确。同样的事情发生在像Data.Map
这样的有限地图上(你可以得到一些东西来进行类型检查,但它不会最终成为一个真正的同构因为f' . g' /= id
(反之亦然,我不记得哪个) )。
而来自(Either a b -> v) -> (a -> v, b -> v)
的同构很容易写
f :: (Either a b -> v) -> (a -> v, b -> v)
f t = (t . Left, t . Right)
g :: (a -> v, b -> v) -> (Either a b -> v)
g (l, r) (Left x) = l x
g (l, r) (Right y) = r y
对于脚踏实地的程序员来说,这个语义域的东西可能有点抽象。为什么我们可以写这个同构是否重要?但是当你试图让GMap
做任何事情时,你会发现问题很快就会出现。
让我们开始捆绑数据系列,我们应该能够编写几个非常简单的操作:
class MapKey k where
data family GMap k :: * -> *
empty :: GMap k v
lookup :: GMap k v -> k -> Maybe v
insert :: k -> v -> GMap k v -> GMap k v
一个非常简单的基本案例,可以使用
instance MapKey Int where
data GMap Int v = GMapInt (Int -> Maybe v)
empty = GMapInt (const Nothing)
lookup (GMapInt f) x = f x
insert x v (GMapInt f) = GMapInt (\y -> if x == y then Just v else f y)
如果我们尝试
instance (MapKey a, MapKey b) => MapKey (a,b) where
data GMap (a,b) v = GMapTuple (GMap a v) (GMap b v)
empty = GMapTuple empty empty
lookup (GMapTuple l r) (x,y) =
-- several implementations here, but maybe we could do
lookup l x `mplus` lookup r y
insert (x,y) v (GMapTuple l r) = GMapTuple (insert x v l) (insert y v r)
似乎合理,但不起作用
>>> lookup (insert (1 :: Int, 2 :: Int) "value" empty) (1,3)
Just "value"
应该是Nothing
,因为我们插入了(1,2)
,而不是(1,3)
。你可以说这只是我实施中的一个错误,但我敢于你写一个工作的错误。
类型和代数之间有一个美丽的对应关系,它将指导你应该如何转换类型。这里~~
表示“类似于”:
Either a b ~~ a + b
(a,b) ~~ a * b
a -> b ~~ b ^ a
所以
Map ((a,b) -> v) ~~ v^(a*b)
= (v^a)^b
~~ Map b (Map a v)
也就是说,我们应该期望来自元组和嵌套映射的映射之间存在同构。类似地:
Map (Either a b -> v) ~~ v^(a+b)
= v^a * v^b
~~ (Map v a, Map v b)
副产品(Either
)和地图对之间的地图应该有一个很好的同构。
这非常有趣,并且值得玩弄与其他东西同构的东西。
进一步阅读: