正如人们在 cmets 中指出的那样,您无法为 Map 实现有效的 Applicative 实例,因为您无法以守法的方式实现 pure。由于恒等律,pure id <*> v = v,pure 实现需要在将映射与函数应用程序相交时维护所有键。您不能对部分映射执行此操作,因为根据参数化,您可能在一个映射或另一个映射中没有键来召唤您需要在其中生成 b 的函数 a -> b 或参数 a生成的地图。 pure x 需要像 ZipList(使用 repeat)那样工作,生成一个映射,将 每个 键映射到相同的值 x,但这是不可能的Map 因为它是有限的。但是,可以使用允许无限映射的替代表示,例如基于函数的映射和Eq。
-- Represent a map by its lookup function.
newtype EqMap k v = EM (k -> Maybe v)
-- Empty: map every key to ‘Nothing’.
emEmpty :: EqMap k v
emEmpty = EM (const Nothing)
-- Singleton: map the given key to ‘Just’ the given value,
-- and all other keys to ‘Nothing’.
emSingleton :: (Eq k) => k -> v -> EqMap k v
emSingleton k v = EM (\ k' -> if k == k' then Just v else Nothing)
-- Insertion: add an entry that overrides any earlier entry
-- for the same key to return ‘Just’ a new value.
emInsert :: (Eq k) => k -> v -> EqMap k v -> EqMap k v
emInsert k v (EM e) = EM (\ k' -> if k == k' then Just v else e k')
-- Deletion: add an entry that overrides any earlier entry
-- for the same key to return ‘Nothing’.
emDelete :: (Eq k) => k -> EqMap k v -> EqMap k v
emDelete k (EM e) = EM (\ k' -> if k == k' then Nothing else e k')
emLookup :: EqMap k v -> k -> Maybe v
emLookup (EM e) = e
instance Functor (EqMap k) where
-- Map over the return value of the lookup function.
fmap :: (a -> b) -> EqMap k a -> EqMap k v
fmap f (EM e) = EM (fmap (fmap f) e)
instance Applicative (EqMap k) where
-- Map all keys to a constant value.
pure :: a -> EqMap k a
pure x = EM (const (Just x))
-- Intersect two maps with application.
(<*>) :: EqMap k (a -> b) -> EqMap k a -> EqMap k b
fs <*> xs = EM (\ k -> emLookup k fs <*> emLookup k xs)
不幸的是,这不仅仅是语义上的无限:当您添加或删除键值对时,它也在内存中无限增长!这是因为条目是闭包的链表,没有具体化为数据结构:您只能通过添加一个指示它们被删除的条目来从映射中删除值,就像版本控制系统中的还原一样.对于查找来说,它的效率也非常低,它在键的数量上是线性的,而不是Map 的对数。对于初学者和中级函数式程序员来说,这充其量只是一个不错的学术练习,只是为了了解如何用函数表示事物。
这里的一个简单替代方案是“默认映射”,它将不存在的键映射到一个常量值。
data DefaultMap k v = DM v (Map k v)
dmLookup :: (Ord k) => k -> DefaultMap k v -> v
dmLookup k (DM d m) = fromMaybe d (Map.lookup k m)
-- …
那么Applicative 的实现很简单:现有键的交集,加上默认应用的不存在键。
instance Functor (DefaultMap k) where
-- Map over the return value of the lookup function.
fmap :: (a -> b) -> DefaultMap k a -> DefaultMap k b
fmap f (DM d m) = DM (f d) (fmap f m)
instance Applicative (DefaultMap k) where
-- Map all keys to a constant value.
pure x = DM x mempty
-- Intersect two maps with application, accounting for defaults.
DM df fs <*> DM dx xs = DM (df dx) $ Map.unions
[ Map.intersectionWith ($) fs xs
, fmap ($ dx) fs
, fmap (df $) xs
]
DefaultMap 有点不寻常,因为您可以删除键值对,但只能通过有效地将它们“重置”为默认值,因为对给定键的查找总是会成功即使在删除相同的密钥之后。尽管您当然可以使用 DefaultMap k (Maybe v) 恢复类似于 Map 的部分行为的东西,默认值为 Nothing 和始终将定义的键映射到 Just 的不变量。
我认为还有一个instance Monad (DefaultMap k),与instance Monad ((->) k)或instance Monad (Stream k)同构,因为像Stream一样,DefaultMap是总是无限的——而可能是有限的ZipList 不能有Monad 实例,因为它必然违反结合律a >=> (b >=> c) = (a >=> b) >=> c。