GMapEither类型的家庭例子是不是真的对应直觉?

时间:2017-04-09 18:01:21

标签: haskell type-families

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)

我认为这可能会提供更好的直觉。

1 个答案:

答案 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)和地图对之间的地图应该有一个很好的同构。

这非常有趣,并且值得玩弄与其他东西同构的东西。

进一步阅读: