关联列表的镜头



Control.Lens.At中有at镜头用于Map/HashMap/等。但是,是否有任何镜头类似于关联列表类型[(k, v)](可转换为地图)的at

我不知道为您提供了一个,但at属于类型类At,所以我们当然可以自己编写。为了避免使用灵活(可能重叠)的实例扩展,我们将在 newtype 中执行此操作。

newtype AList k v = AList [(k, v)]

首先,我们需要几个家庭实例。

{-# LANGUAGE TypeFamilies #-}
type instance IxValue (AList k v) = v
type instance Index (AList k v) = k

这只是定义了我们新类型中的"键"和"值"是什么,这很简单。现在,我们需要能够在特定键处读取和写入值。Haskell已经为我们提供了一种读取值的方法(Data.List.lookup),但我们必须自己制作书写功能。这里没有什么花哨或镜头:只是普通的老式Haskell滤镜和地图。

replaceAt :: Eq k => k -> Maybe v -> AList k v -> AList k v
replaceAt k Nothing (AList m) = AList $ filter ((k', _) -> k /= k') m
replaceAt k (Just v) (AList m) =
case lookup k m of
Nothing ->
-- Not present in the list; add it
AList ((k, v) : m)
Just _ ->
-- Present; replace it
AList $ map ((k', v') -> if k == k' then (k', v) else (k', v')) m

现在我们需要编写At实例,这取决于Ixed实例。幸运的是,只要我们实现At,镜头库就为Ixed提供了默认实现,因此第一个实例声明很简单。

instance Eq k => Ixed (AList k v)

编写at也相当简单。只需查看类型并稍微跟随您的鼻子,您得出的实现就是我们想要的。

instance Eq k => At (AList k v) where
at k f (AList m) = fmap (v' -> replaceAt k v' (AList m)) $ f (lookup k m)

我们完成了。现在at将为AList工作.如果 newtype 包装器困扰您,您可以轻松地创建一个新函数(如果您愿意的话,可以at')为您执行 newtype 包装/解包。

证明这个实例满足镜头定律留给读者作为练习。

完整代码

{-# LANGUAGE TypeFamilies #-}
import Control.Lens.At
import Data.List(lookup)
newtype AList k v = AList [(k, v)]
type instance IxValue (AList k v) = v
type instance Index (AList k v) = k
replaceAt :: Eq k => k -> Maybe v -> AList k v -> AList k v
replaceAt k Nothing (AList m) = AList $ filter ((k', _) -> k /= k') m
replaceAt k (Just v) (AList m) =
case lookup k m of
Nothing ->
-- Not present in the list; add it
AList ((k, v) : m)
Just _ ->
-- Present; replace it
AList $ map ((k', v') -> if k == k' then (k', v) else (k', v')) m
-- Just take the default implementation here.
instance Eq k => Ixed (AList k v)
instance Eq k => At (AList k v) where
at k f (AList m) = fmap (v' -> replaceAt k v' (AList m)) $ f (lookup k m)

最新更新