{- | Module : Control.Comonad.Zipper.Extra License : MIT Stability : experimental Extra utilities for the `Zipper` comonad. -} module Control.Comonad.Zipper.Extra ( Control.Comonad.Store.Zipper.Zipper , Control.Comonad.Store.Zipper.zipper , Control.Comonad.Store.Zipper.zipper1 , Control.Comonad.Store.Zipper.unzipper , Control.Comonad.Store.Zipper.size , paginate , paginate' , PaginationException(..) , zipperNextMaybe , zipperPreviousMaybe , zipperWithin , zipper' , ZipperException(..) , elemIndexThrow , ElemNotFoundException(..) , seekOn , seekOnThrow ) where import Control.Monad.Catch import Control.Comonad.Store import Control.Comonad.Store.Zipper import Data.List import Data.List.Split import Data.Typeable -- | Turn a list into a zipper of chunks of length n paginate :: Int -> [a] -> Maybe (Zipper [] [a]) paginate n = zipper . chunksOf n -- | An exception type for when pagination fails to break up a list because either the -- length of the list or the page size is zero. data PaginationException = EmptyContentsError | ZeroPageSize | UnknownPaginationException deriving (Show, Eq, Typeable) instance Exception PaginationException where displayException EmptyContentsError = "Can not create a Zipper of length zero." displayException ZeroPageSize = "Can not divide into pages of size zero." displayException UnknownPaginationException = "Unknown pagination exception." -- | Like `paginate`, but throw an exception if it can't create the zipper. paginate' :: MonadThrow m => Int -> [a] -> m (Zipper [] [a]) paginate' n xs = case paginate n xs of Just x -> return x Nothing -> if n == 0 then throwM ZeroPageSize else if null xs then throwM EmptyContentsError else throwM UnknownPaginationException -- | Return the peek of the next element if it exists. zipperNextMaybe :: Zipper t a -> Maybe a zipperNextMaybe xs = if pos xs < size xs-1 then Just (peeks (+1) xs) else Nothing -- | Return the peek of the previous element if it exists. zipperPreviousMaybe :: Zipper t a -> Maybe a zipperPreviousMaybe xs = if pos xs > 0 then Just (peeks (+ (-1)) xs) else Nothing -- | Return a list of elements within 'r' hops either side of the zipper target. zipperWithin :: Int -> Zipper t a -> [a] zipperWithin r xs = (`peek` xs) <$> [(max 0 (pos xs - r)) .. (min (size xs -1) (pos xs + r))] -- | Exception thrown when trying to make an empty zipper. data ZipperException = EmptyZipper deriving (Show, Eq, Typeable) instance Exception ZipperException where displayException EmptyZipper = "Can not create an empty zipper." -- | Like `zipper` but lifted to `MonadThrow`. zipper' :: (MonadThrow m, Traversable t) => t a -> m (Zipper t a) zipper' xs = maybe (throwM EmptyZipper) return $ zipper xs -- | Exception thrown when an element can not be found in a list or a zipper. data ElemNotFoundException a = ElemNotFoundException a [a] deriving (Show, Eq, Typeable) instance (Typeable a, Show a) => Exception (ElemNotFoundException a) where displayException (ElemNotFoundException x xs) = "Elem " <> show x <> " not found in " <> show xs -- | Lifted version of `Data.List.elemIndex` that throws an `ElemNotFoundException` if the target does not exist. elemIndexThrow :: (MonadThrow m, Eq a, Typeable a, Show a) => a -> [a] -> m Int elemIndexThrow x xs = case elemIndex x xs of Nothing -> throwM $ ElemNotFoundException x xs Just a -> return a -- | Seek on a property of the elements of the zipper. Finds the index of the element to search for -- and moves the tape to that position. seekOn :: Eq b => (a -> b) -> b -> Zipper [] a -> Maybe (Zipper [] a) seekOn f x ys = do k <- elemIndex x (f <$> unzipper ys) return $ seek k ys -- | Lifted version of `seekOn` which throws an `ElemNotFoundException` if the target does not exist. seekOnThrow :: (MonadThrow m, Eq b, Typeable b, Show b) => (a -> b) -> b -> Zipper [] a -> m (Zipper [] a) seekOnThrow f x ys = do k <- elemIndexThrow x (f <$> unzipper ys) return $ seek k ys