brassica-0.0.3: src/Brassica/SoundChange/Apply/Internal/MultiZipper.hs
{-# LANGUAGE DeriveTraversable #-}
{-| __Warning:__ This module is __internal__, and does __not__ follow
the Package Versioning Policy. It may be useful for extending
Brassica, but be prepared to track development closely if you import
this module.
-}
module Brassica.SoundChange.Apply.Internal.MultiZipper
( MultiZipper
-- * Conversion
, fromListStart
, fromListPos
, toList
-- * Querying
, curPos
, atStart
, atEnd
, atBoundary
, value
, valueN
, locationOf
, yank
-- * Movement
, move
, fwd
, bwd
, consume
, seek
, toBeginning
, toEnd
-- * Modification
, insert
, insertMany
, zap
, tag
, tagAt
, query
, untag
, untagWhen
, modifyBetween
, extend
, extend'
) where
import Control.Applicative (Alternative((<|>)))
import Data.Foldable (Foldable(foldl'))
import qualified Data.Map.Strict as M
-- | A 'MultiZipper' is a list zipper (list+current index), with the
-- addition of ‘tags’ which can be assigned to indices in the
-- list. Any tag may be assigned to any index, with the restriction
-- that two different indices may not be tagged with the same
-- tag. This sort of data structure is useful for certain algorithms,
-- where it can be convenient to use tags to save positions in the
-- list and then return back to them later.
--
-- (One subtlety: unlike most list zipper implementations, a
-- 'MultiZipper' positioned at the ‘end’ of a list is actually at
-- positioned at the index one past the end of the list, rather than
-- at the last element of the list. Although this makes some functions
-- slightly more complex — most notably, 'value' becomes non-total —
-- it makes other algorithms simpler. For instance, this lets
-- functions processing a 'MultiZipper' to process a portion of the
-- 'MultiZipper' and then move to the next element immediately after
-- the processed portion, allowing another function to be run to
-- process the next part of the 'MultiZipper'.)
data MultiZipper t a = MultiZipper [a] Int (M.Map t Int)
deriving (Show, Functor, Foldable, Traversable)
-- | Convert a list to a 'MultiZipper' positioned at the start of that
-- list.
fromListStart :: [a] -> MultiZipper t a
fromListStart as = MultiZipper as 0 M.empty
-- | Convert a list to a 'MultiZipper' at a specific position in the
-- list. Returns 'Nothing' if the index is invalid.
fromListPos :: [a] -> Int -> Maybe (MultiZipper t a)
fromListPos as pos =
if invalid pos as
then Nothing
else Just $ MultiZipper as pos M.empty
-- | Get the list stored in a 'MultiZipper'.
toList :: MultiZipper t a -> [a]
toList (MultiZipper as _ _) = as
-- | The current position of the 'MultiZipper'.
curPos :: MultiZipper t a -> Int
curPos (MultiZipper _ pos _) = pos
-- | Determine whether the 'MultiZipper' is positioned at the start of
-- its list.
atStart :: MultiZipper t a -> Bool
atStart (MultiZipper _ pos _) = pos <= 0
-- | Determine whether the 'MultiZipper' is positioned at the end of
-- its list.
atEnd :: MultiZipper t a -> Bool
atEnd (MultiZipper as pos _) = pos >= length as
-- | Determine whether the 'MultiZipper' is positioned at the start or
-- end of its list.
atBoundary :: MultiZipper t a -> Bool
atBoundary = (||) <$> atStart <*> atEnd
-- | The element at the current position of the 'MultiZipper'. Returns
-- 'Nothing' if the 'MultiZipper' is positioned ‘at the end of the
-- list’ (recall this actually means that the 'MultiZipper' is
-- positioned /after/ the last element of its list).
value :: MultiZipper t a -> Maybe a
value (MultiZipper as pos _) =
if atNonvalue pos as
then Nothing
else Just $ as !! pos
-- | @valueN n mz@ returns the next @n@ elements of @mz@ starting from
-- the current position, as well as returning a new 'MultiZipper'
-- positioned past the end of those @n@ elements. (So running
-- @valueN m@ and then @valueN n@ would return the next @m+n@
-- elements.) Returns 'Nothing' if this would move the position of the
-- 'MultiZipper' past the end of the list.
valueN :: Int -> MultiZipper t a -> Maybe ([a], MultiZipper t a)
valueN i (MultiZipper as pos ts) =
let pos' = pos + i in
if invalid pos' as || i < 0
then Nothing
else Just (take i $ drop pos as, MultiZipper as pos' ts)
-- | Given a tag, return its position
locationOf :: Ord t => t -> MultiZipper t a -> Maybe Int
locationOf t (MultiZipper _ _ ts) = M.lookup t ts
-- | Get all tags at the current position
query :: Ord t => MultiZipper t a -> [t]
query (MultiZipper _ pos ts) = M.keys $ M.filter (==pos) ts
seekIx :: Int -> MultiZipper t a -> Maybe (MultiZipper t a)
seekIx i (MultiZipper as _ ts) =
if invalid i as
then Nothing
else Just (MultiZipper as i ts)
-- | @move n mz@ will move the position of @mz@ by @n@ forward (if
-- n>0) or by @-n@ backward (if n<0). Returns 'Nothing' if this would
-- cause the 'MultiZipper' to move after the end or before the
-- beginning of the list.
move :: Int -> MultiZipper t a -> Maybe (MultiZipper t a)
move s mz@(MultiZipper _ pos _) = seekIx (pos + s) mz
-- | Move one position forward if possible, otherwise return 'Nothing'.
fwd :: MultiZipper t a -> Maybe (MultiZipper t a)
fwd = move 1
-- | Move one position backwards if possible, otherwise return 'Nothing'.
bwd :: MultiZipper t a -> Maybe (MultiZipper t a)
bwd = move (-1)
-- | If possible, move one position forward, returning the value moved
-- over
consume :: MultiZipper t a -> Maybe (a, MultiZipper t a)
consume (MultiZipper as pos ts) =
if invalid (pos+1) as
then Nothing
else Just (as!!pos, MultiZipper as (pos+1) ts)
-- | Move the 'MultiZipper' to be at the specified tag. Returns
-- 'Nothing' if that tag is not present.
seek :: Ord t => t -> MultiZipper t a -> Maybe (MultiZipper t a)
seek t (MultiZipper as _ ts) = case M.lookup t ts of
Nothing -> Nothing
Just pos -> Just $ MultiZipper as pos ts
-- | Move to the beginning of the 'MultiZipper'.
toBeginning :: MultiZipper t a -> MultiZipper t a
toBeginning (MultiZipper as _ ts) = MultiZipper as 0 ts
-- | Move to the end of the 'MultiZipper'.
toEnd :: MultiZipper t a -> MultiZipper t a
toEnd (MultiZipper as _ ts) = MultiZipper as (length as) ts
-- | Find first element before point which returns 'Just' when
-- queried, if any, returning the result of the query function.
yank :: (a -> Maybe b) -> MultiZipper t a -> Maybe b
yank p mz = bwd mz >>= \mz' -> (value mz' >>= p) <|> yank p mz'
-- | Insert a new element at point and move forward by one position.
insert :: a -> MultiZipper t a -> MultiZipper t a
insert a (MultiZipper as pos ts) =
case splitAt pos as of
(as1, as2) -> MultiZipper (as1 ++ [a] ++ as2) (pos+1) $ correctIxsFrom pos (+1) ts
-- | Insert multiple elements at point and move after them. A simple
-- wrapper around 'insert'.
insertMany :: [a] -> MultiZipper t a -> MultiZipper t a
insertMany = flip $ foldl' $ flip insert
-- | Modify the first element before point to which the modification
-- function returns 'Just'.
zap :: (a -> Maybe a) -> MultiZipper t a -> MultiZipper t a
zap p = \mz@(MultiZipper as pos ts) -> case go as (pos-1) of
Nothing -> mz
Just as' -> MultiZipper as' pos ts
where
go _ (-1) = Nothing
go as pos
| pos == length as = go as (pos-1)
| otherwise = case p (as !! pos) of
Nothing -> go as (pos-1)
Just a' -> case splitAt pos as of
(as1, _:as2) -> Just $ as1 ++ (a':as2)
_ -> error "error in zap: impossible case reached"
-- | Set a tag at the current position.
tag :: Ord t => t -> MultiZipper t a -> MultiZipper t a
tag t (MultiZipper as pos ts) = MultiZipper as pos $ M.insert t pos ts
-- | Set a tag at a given position if possible, otherwise return 'Nothing'.
tagAt :: Ord t => t -> Int -> MultiZipper t a -> Maybe (MultiZipper t a)
tagAt t i (MultiZipper as pos ts) =
if invalid i as
then Nothing
else Just $ MultiZipper as pos $ M.insert t i ts
-- | Remove tags satisfying predicate
untagWhen :: (t -> Bool) -> MultiZipper t a -> MultiZipper t a
untagWhen p (MultiZipper as pos ts) = MultiZipper as pos $ snd $ M.partitionWithKey (flip $ const p) ts
-- | Remove all tags.
untag :: MultiZipper t a -> MultiZipper t a
untag (MultiZipper as pos _) = MultiZipper as pos M.empty
-- | Modify a 'MultiZipper' between the selected tags. Returns
-- 'Nothing' if a nonexistent tag is selected, else returns the
-- modified 'MultiZipper'.
modifyBetween :: Ord t
=> (t, t)
-- ^ Selected tags. Note that the resulting interval
-- will be [inclusive, exclusive).
-> ([a] -> [a])
-- ^ Function to modify designated interval.
-> MultiZipper t a
-> Maybe (MultiZipper t a)
modifyBetween (t1, t2) f mz@(MultiZipper as pos ts) = do
(i1, i2) <- fmap correctOrder $ (,) <$> locationOf t1 mz <*> locationOf t2 mz
let (before_t1, after_t1) = splitAt i1 as
(cut_part, after_t2) = splitAt (i2-i1) after_t1
replacement = f cut_part
dEnd = length replacement - length cut_part
pos' = pos + dEnd
return $ MultiZipper (before_t1 ++ replacement ++ after_t2) pos' (correctIxsFrom i2 (+dEnd) ts)
where
correctOrder (m, n) = if m <= n then (m, n) else (n, m)
-- | Given a function to compute a value from a 'MultiZipper' starting
-- at a particular point, apply that function to all possible starting
-- points and collect the results. Tags are left unchanged.
--
-- (Note: this is really just the same @extend@ method as in the
-- @Comonad@ typeclass, although 'MultiZipper' wouldn’t be a lawful
-- comonad.)
extend :: (MultiZipper t a -> b) -> MultiZipper t a -> MultiZipper t b
extend f (MultiZipper as pos ts) = MultiZipper as' pos ts
where
as' = fmap (\i -> f $ MultiZipper as i ts) [0 .. length as - 1]
-- | Like 'extend', but includes the end position of the zipper, thus
-- increasing the 'MultiZipper' length by one when called.
extend' :: (MultiZipper t a -> b) -> MultiZipper t a -> MultiZipper t b
extend' f (MultiZipper as pos ts) = MultiZipper as' pos ts
where
as' = fmap (\i -> f $ MultiZipper as i ts) [0 .. length as]
-- Utility functions for checking and modifying indices in lists:
invalid :: Int -> [a] -> Bool
invalid pos as = (pos < 0) || (pos > length as)
atNonvalue :: Int -> [a] -> Bool
atNonvalue pos as = (pos < 0) || (pos >= length as)
correctIxsFrom :: Int -> (Int -> Int) -> M.Map t Int -> M.Map t Int
correctIxsFrom i f = M.map $ \pos -> if pos >= i then f pos else pos