Вы действительно можете сделать это с линзами! Или более конкретно; Обходы:)
Сначала некоторые настройки:
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}
module TreeTraversal where
import qualified Data.Vector as V
import Control.Lens hiding (children)
data Tree a = Tree { _label :: a
, _children :: V.Vector (Tree a) }
deriving (Show, Eq, Ord, Functor)
makeLenses ''Tree
type Path = [Int]
С этого момента есть два способа продолжить; Если вам нужно только знать, был ли успешен весь обход (например, любая ссылка в пути была недоступна), тогда вы можете использовать failover
; которая принимает обход и функцию и пытается выполнить функцию при обходе, но которая возвращает результат в контексте Alternative
; мы можем выбрать этот контекст как «возможно», чтобы мы могли обнаружить сбой при сопоставлении с образцом и вернуть соответствующий Left
или Right
. Я не знаю простого способа пройтись по списку индексов, поэтому я написал быстрый помощник, чтобы пересмотреть список ссылок и превратить их в обход с помощью композиции.
modSubtreeWithGenericError
:: forall a. Show a
=> Path -> (Tree a -> Tree a) -> Tree a -> Either String (Tree a)
modSubtreeWithGenericError links f =
maybe (Left "out of bounds") Right . failover (pathOf links) f
where
pathOf :: [Int] -> Traversal' (Tree a) (Tree a)
pathOf [] = id
pathOf (p : ps) = children . ix p . pathOf ps
Это должно сработать, если вы заботитесь только о неудаче в целом, но было бы неплохо знать, ГДЕ она провалилась, верно? Мы можем сделать это, написав собственный обход, который ЗНАЕТ, что он работает внутри Either String
; Большинство обходов должны работать над ЛЮБОЙ аппликацией, но в нашем случае мы ЗНАЕМ, что хотим, чтобы наш результат был в любом из них; так что мы можем воспользоваться этим:
modSubtreeWithExpressiveError
:: forall a. Show a
=> [Int] -> (Tree a -> Tree a) -> Tree a -> Either String (Tree a)
modSubtreeWithExpressiveError links f = pathOf links %%~ (pure . f)
where
pathOf :: [Int] -> LensLike' (Either String) (Tree a) (Tree a)
pathOf [] = id
pathOf (x : xs) = childOrFail x . pathOf xs
childOrFail :: Show a => Int -> LensLike' (Either String) (Tree a) (Tree a)
childOrFail link f t =
if t & has (children . ix link)
then t & children . ix link %%~ f
else buildError link t
childOrFail
интересный бит; Бит LensLike
- это просто псевдоним для (Tree a -> Either String (Tree a)) -> Tree a -> Either String (Tree a)
, который просто traverse
специализирован для Either String
; мы не можем просто использовать traverse
напрямую, хотя, потому что мы хотим пройти только по одному поддереву, и наша функция работает на Tree a
, а не просто a
. Я записал обход вручную, сначала проверив, существует ли цель, используя has
, затем либо с ошибкой Left
с приятной ошибкой, либо запустив f
(который представляет остальную часть обхода) над соответствующим потомком, используя %%~
. Комбинатор %%~
тоже немного страшен; по иронии судьбы его определение буквально (%%~) = id
; Обычно мы использовали бы здесь %~
; но он ожидает конкретного Applicative, который не соответствует указанному нами Either String
. %%~
успешно запускает наш пользовательский обход, хотя нам все еще нужно добавить дополнительный pure
в нашу функцию, чтобы включить его в контекст Either.
Это довольно продвинутый объектив, но, в конце концов, это все обычные обходы (большинство объективов).
У меня есть руководство по написанию ваших собственных обходов, которое может помочь https://lens -by-example.chrispenner.ca / Articles / traversals / writing-traversals
Удачи! Надеюсь, это поможет:)