使用`at`和`ix`组合镜头

6

假设我有一个相当简单的数据类型Person,其中包含一些字段,以及一种保存Person集合的类型。

data Person = Person { _name :: String, _age  :: Int }

data ProgramState = PS { _dict :: IntMap Person }

makeLenses ''Person
makeLenses ''ProgramState

我想创建一个镜头,通过查找他们的密钥来访问个人。
person :: Int -> Lens' ProgramState Person

看起来,我进行这个操作的两个选项是使用atix索引到字典中。

-- Option 1, using 'at'
person :: Int -> Lens' ProgramState (Maybe Person)
person key = dict . at key

-- Option 2, using 'ix'
person :: Int -> Traversal' ProgramState Person
person key = dict . ix key

但是这两种选项都不能让我达到想要的效果,即使用 Lens' 访问 Person 而不是 Maybe Person。选项1与其他镜头无法很好地组合,而选项2意味着我必须放弃我的获取器。
我了解为什么要像这样编写 ixat。在字典中可能不存在该键,因此如果您想要一个既能够获取又能够设置的 Lens',它必须访问 Maybe a。另一种选择是接受一个 Traversal',它可以访问0个或1个值,但这意味着您需要放弃获取器。但在我的情况下,我知道我想要的元素总是存在的,所以我不需要担心缺少键的情况。
是否有一种方法可以编写我想要的内容,还是我应该重新思考我的程序结构?

你没有解决的一个关键问题是,你是否确定要操作的键在地图中。 - dfeuer
3个回答

5

您可能希望将atnon同构一起使用,以获取默认映射条目并消除查找中的Maybe

non :: Eq a => a -> Iso' (Maybe a) a

person key = dict . at key . non defaultEntry

-- can get and set just like plain lenses
someProgramState & dict . at someKey . non defaultEntry .~ somePerson

您可以在文档中查看更多示例。


3

根据András Kovács的回答,我最终定义了一个unsafeFromJust镜头来证明我需要组合这些镜头的'等同性'

import Data.Maybe (fromJust)

unsafeFromJust :: Lens' (Maybe a) a
unsafeFromJust = lens fromJust setJust
 where
  setJust (Just _) b = Just a
  setJust Nothing  _ = error "setJust: Nothing"

另一种定义是:
unsafeFromJust :: Lens' (Maybe a) a
unsafeFromJust = anon (error "unsafeFromJust: Nothing") (\_ -> False)

但我感觉这并不如第一种形式那么清晰。我没有使用non,因为它需要一个在这种情况下是不必要的Eq实例。

现在我可以写

person :: Lens' ProgramState Person
person key = dict . at key . unsafeFromJust

不错!这应该是你的 where 子句中的 setJust (Just _) b = Just b - Jamie Cook

0

实际上,@Chris Taylor的答案是不正确的。您可以通过以下命令(在GHCi中)查看:

>view (at 0 . unsafeFromJust) (fromList [(0,'b')])
*** Exception: Maybe.fromJust: Nothing
[expected: *** Exception: setJust]

>set (at 1 . unsafeFromJust) 'c' (fromList [(0,'b')])
*** Exception: setJust]
[expected: fromList [(0,'b'),(1,'c')]

第一个命令在查找不存在的值时仍然会抛出错误,但它并没有抛出正确的错误。对于第二个测试,我无法插入新的键,这似乎没有意义。

相反,我正在使用以下两个组合器:

at' :: (Ord a) => a -> Lens' (Map a b) b
at' a = lens r s
  where r m = case lookup a m of
                (Just b) -> b
                Nothing -> error "Could not find key in map!"
        s m b' = insert a b' m

at'' :: (Ord a) => a -> Lens (Map a b) (Map a b) b (Maybe b)
at'' a = lens r s
  where r m = case lookup a m of
                (Just b) -> b
                Nothing -> error "Could not find key in map!"
        s m Nothing = delete a m
        s m (Just b') = insert a b' m

at' 是如何工作的 (at k . unsafeFromJust): 尝试检索不存在的值会抛出错误,插入新值成功。 at'' 类似: 它允许您读取纯值,但您可以设置Maybe值。这使您可以从映射中删除键。

示例:

> view (at' 0) (fromList [(0,'b')])
'b'

> view (at'' 0) (fromList [(0,'b')])
'b'

> view (at' 1) (fromList [(0,'b')])
*** Exception: Could not find key in map!

> view (at'' 1) (fromList [(0,'b')])
*** Exception: Could not find key in map!

> set (at' 0) 'c' (fromList [(0,'b')])
fromList [(0,'c')]

> set (at'' 0) (Just 'c') (fromList [(0,'b')])
fromList [(0,'c')]

> set (at' 1) 'c' (fromList [(0,'b')])
fromList [(0,'b'),(1,'c')]

> set (at'' 1) (Just 'c') (fromList [(0,'b')])
fromList [(0,'b'),(1,'c')]

> set (at'' 0) Nothing (fromList [(0,'b')])
fromList []

整个业务似乎不太靠谱,除非密钥始终存在于映射表中。在这种情况下,将其添加到映射表中似乎没有太多意义。使用“profunctor lens”方法探索可能很有趣,其中将镜头与棱镜组合会产生一个仿射遍历。 - dfeuer
@dfeuer 也许您可以在回答中更详细地解释一下? - crockeea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接