编写更复杂的遍历(镜头)

Writing a more complex Traversal (Lenses)

我正在不断深入研究 Kmett 的镜头;今天我正在尝试编写一些自定义遍历,直到现在我已经设法通过组合现有遍历来创建新遍历来相处,但是我正在做一些更复杂的事情并且卡住了。

我正在写一个文本编辑器,我只是添加了多个光标,最初每个缓冲区都有一个光标并有一个镜头来聚焦它,现在我要概括为一个光标列表并想要遍历名单。诀窍在于,在之前的案例中,我的镜头在 setter 内部进行了一些验证,以确保光标被限制在缓冲区文本的有效范围内。它看起来像这样:

clampCursor :: Text -> Cursor -> Cursor

cursor :: Lens' Buffer Cursor
cursor = lens getter setter
  where getter buf = buf^.curs
        setter buf new = let txt = buf^.text
                          in buf & curs .~ clampCursor txt new

注意它如何使用缓冲区上下文中的文本信息在光标上创建一个透镜; (此外,如果有人有建议,我很想听听任何更简洁的方法来做这件事而不是制作定制镜头,我发现自己做了很多)。

所以现在我有多个游标,我需要将其转换为遍历',但是我当然不能使用 lens getter setter 方法定义遍历;四处寻找如何定义遍历我读到了这个tutorial;其中规定如下:

Question: How do I create traversals?

Answer: There are three main ways to create primitive traversals:

  • traverse is a Traversal' that you get for any type that implements Traversable
  • Every Lens' will also type-check as a Traversal'
  • You can use Template Haskell to generate Traversal's using makePrisms since every Prism' is also a Traversal' (not covered in this tutorial)

None 这些方法在这里真的很有帮助;我也看到过使用应用风格创建遍历的风格,但它总是让我有点困惑,我真的不知道在这种情况下我将如何使用它来获得我想要的东西。

我想我可以写一个 Lens' Buffer [Cursor] 映射到 setter 中的游标以执行验证然后遍历该列表,但我认为必须有一种方法来烘焙它以某种方式在遍历之后(当每个元素都被聚焦时)进入遍历。也许有更好的方法来完全做到这一点;

仍在寻找尽可能多的关于遍历的知识,所以任何解释都不胜感激!谢谢!

编辑: @dfeuer 指出,当您进行此类验证时,您最终会得到无效的镜头,我真的很喜欢它提供的干净界面,可以在镜头内进行这些验证;据我所知,由于验证是幂等的,因此不会引起任何实际问题,但我愿意接受有关如何更好地做到这一点的建议。

随着 curs :: Lens' Buffer Cursor 变为 curs :: Lens' Buffer [Cursor],您构建一个无视法律 cursor :: Traversal' Buffer Cursor 的任务可以拆分为构建一个进行边界检查的无视法律 Lens' Buffer [Cursor],并将任何 Lens' s [a] 变成 Traversal' s a.

第一个可能已经被你解决了:你做你已经在做的事情,但是检查 [Cursor].

的每个元素的边界

第二个是遵纪守法的请求,因此您本可以期待在您的 post 下没有大量评论的情况下得到答复:

turnIntoTraversal :: Lens' s [a] -> Traversal' s a
turnIntoTraversal l = l . traverse

同样为了好玩,这里尝试直接实现。

cursor :: Traversal' Buffer Cursor
cursor atofa s = (curs . traverse) (fmap (clampCursor $ s ^. text) . atofa) s

我的建议是直接使用镜头的 Functor / Applicative 表示来做这种事情。

要编写非类型更改(简单)LensTraversal,您需要编写一个函数,该函数将函数 k :: a -> f a 和结构 k :: a -> f a 作为参数=19=] 然后生成 f s.

k是一种广义的修正函数,表示镜头使用者想要对镜头聚焦的数据进行的改变。但是因为 k 不是简单的 a -> a 类型,而是 a -> f a 类型,它还允许在更新中携带一个 "result",例如,值更新前的字段(如果你对 f 使用状态 monad,那么你可以在更新函数中将状态设置为字段的旧值,然后当你 运行 状态 monad 时读出它)。

我们在下面的代码中的做法是改变这个修改函数,在返回新值之前执行一些钳位:

-- Takes a cursor update function and returns a modified update function 
-- that clamps the return value of the original function
clampCursorUpdate :: Functor f => Text -> (Cursor -> f Cursor) -> (Cursor -> f Cursor)
clampCursorUpdate k = \cur -> fmap (clampCursor txt) (k cur)

然后我们可以把一个非验证镜头变成一个验证镜头(注意,正如评论中所说,这个验证镜头不是一个守法镜头):

-- assuming that _cursor is a lens that accesses
-- the _cursor field without doing any validation
_cursor :: Lens' Buffer Cursor

cursor :: Functor f => (Cursor -> f Cursor) -> Buffer -> f Buffer
cursor k buffer = _cursor (clampCursorUpdate txt k) buffer
 where txt = buffer^.text

这种方法很容易推广到遍历。首先,我们通过组合 Lens' Buffer [Cursor]traverse 来编写非验证遍历,这会将其变成 Traversal' Buffer Cursor:

-- assuming that _cursors is a lens that returns the list of cursors
_cursors :: Lens' Buffer [Cursor]

-- non-validating cursors traversal
_cursorsTraversal :: Traversal' Buffer Cursor
_cursorsTraversal = _cursors . traverse

现在我们可以使用与之前相同的方法:因为遍历已经为我们做了 "mapping",代码是相同的,除了我们现在有一个 Applicative f 约束我们的 f,因为我们想要 Traversal':

cursors :: Applicative f => (Cursor -> f Cursor) -> Buffer -> f Buffer
cursors k buffer = _cursorsTraversal (clampCursorUpdate txt k) buffer
 whee txt = buffer^.text