{-# LANGUAGE DoAndIfThenElse, BangPatterns, NamedFieldPuns #-} -- | Mutable List Builder. -- -- A @ListBuilder s a@ is like a wrapper over @ST s [a]@, but uses unsafe -- mutation to achieve constant time append as well as prepend. -- -- As the builder is backed with a standard 'Data.List.List', it -- is light-weight and cheap to return to a list. -- -- Code from this module is derived from Scala's -- [ListBuffer](https://www.scala-lang.org/api/current/scala/collection/mutable/ListBuffer.html) -- module, using the unsafe set field technique described by -- [Twan van Laarhoven](https://www.twanvl.nl/blog/haskell/unsafe-sequence). module Data.ListBuilder ( -- * Mutable list builder ListBuilder -- * Construction , newBuilder -- * Mutations , append , prepend , insert , filterInPlace , clear -- * Accessors , readLength , readFirst , readLast , readAt -- * Conversions , freeze , unsafeFreeze ) where import Data.ListBuilder.Unsafe import qualified Data.List import Control.Monad (when) import Control.Monad.ST import Control.Monad.ST.Unsafe import Data.Foldable (foldr') import Data.Maybe (listToMaybe) import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef, modifySTRef') -- | A List Builder. -- -- This builder is backed by a standard haskell 'Data.List.List'. -- It offers predictable (and fast) operations, and doesn't -- pause for grow operations as an array based builder might. data ListBuilder s a = ListBuilder { start :: STRef s [a] , end :: STRef s [a] , len :: STRef s Int } -- | Create a new, empty 'ListBuilder' newBuilder :: ST s (ListBuilder s a) newBuilder = do start <- newSTRef [] end <- newSTRef [] len <- newSTRef 0 pure $ ListBuilder start end len -- | Append an item to the back of the 'ListBuilder' -- -- /O(1)/ append :: a -> ListBuilder s a -> ST s () append a ListBuilder { start, end, len } = do let !last' = [a] len' <- readSTRef len if len' == 0 then do writeSTRef start last' writeSTRef end last' else do end' <- readSTRef end unsafeIOToST $ unsafeSetField 1 end' last' writeSTRef end last' modifySTRef' len (+1) -- | Prepend an item to the front of the 'ListBuilder' -- -- /O(1)/ prepend :: a -> ListBuilder s a -> ST s () prepend a ListBuilder { start, end, len } = do front <- readSTRef start len' <- readSTRef len let !front' = a : front when (len' == 0) $ writeSTRef end front' writeSTRef start front' modifySTRef' len (+1) -- | Internal function. Locates the previous cons cell. -- -- /O(N)/ locate :: Int -> ListBuilder s a -> ST s [a] locate 0 _ = error "Internal error: locate called on 0" locate i ListBuilder { start, end, len } = do l <- readSTRef len if l == i then readSTRef end else do start' <- readSTRef start cur <- newSTRef start' let go 0 = readSTRef cur go j = do modifySTRef' cur (drop 1) go (j - 1) go (i - 1) -- | Insert into a location in a 'ListBuilder'. -- -- This function doesn't create a new spine -- across the list builder, and only allocates -- the new cons cell itself. -- -- /O(N)/ insert :: Int -> a -> ListBuilder s a -> ST s () insert ix _ _ | ix < 0 = error "Index out of bounds" insert 0 a bldr = prepend a bldr insert ix a bldr= do len' <- readSTRef (len bldr) if ix == len' then append a bldr else if ix > len' then error "Index out of bounds" else do prev <- locate ix bldr let !pn = drop 1 prev let !nx = a:pn unsafeIOToST $ unsafeSetField 1 prev nx -- | The current length of the 'ListBuilder'. -- -- /O(1)/ readLength :: ListBuilder s a -> ST s Int readLength bldr = readSTRef (len bldr) -- | Empty the 'ListBuilder' of all values. -- -- /O(1)/ clear :: ListBuilder s a -> ST s () clear ListBuilder { start, end, len } = do writeSTRef start [] writeSTRef end [] writeSTRef len 0 -- | Filter the 'ListBuilder' with the supplied predicate -- -- /O(N)/ filterInPlace :: (a -> Bool) -> ListBuilder s a -> ST s () filterInPlace func ListBuilder { start, end, len } = do prev <- newSTRef Nothing start' <- readSTRef start cur <- newSTRef start' let go = do cur' <- readSTRef cur case cur' of [] -> return () (h:follow) -> do prev' <- readSTRef prev if not (func h) then do case prev' of Nothing -> writeSTRef start follow Just y -> unsafeIOToST $ unsafeSetField 1 y follow modifySTRef' len (\x -> x - 1) else writeSTRef prev (Just cur') writeSTRef cur follow go go prev' <- readSTRef prev case prev' of Nothing -> do writeSTRef end [] Just y -> writeSTRef end y -- | Return the current last element in the 'ListBuilder' -- -- /O(1)/ readLast :: ListBuilder s a -> ST s (Maybe a) readLast ListBuilder { end } = do listToMaybe <$> readSTRef end -- | Return the current first element in the 'ListBuilder' -- -- /O(1)/ readFirst :: ListBuilder s a -> ST s (Maybe a) readFirst ListBuilder { start } = do listToMaybe <$> readSTRef start -- | Return the current element at a particular index for -- the 'ListBuilder' -- -- /O(N)/ readAt :: ListBuilder s a -> Int -> ST s (Maybe a) readAt ListBuilder { start } ix = do (Data.List.!? ix) <$> readSTRef start -- | Return the 'Data.List.List' backing the 'ListBuilder'. -- -- This does /not/ stop mutations made to -- the builder from affecting the resultant -- list. So one must not continue to call the -- mutating functions. -- -- This function is safe in tail position within a -- call to @runST@. -- -- /O(1)/ unsafeFreeze :: ListBuilder s a -> ST s [a] unsafeFreeze bldr = readSTRef (start bldr) -- | Freeze the result and return the list. -- -- This function strictly copies the spine -- of the list. -- -- /O(n)/ freeze :: ListBuilder s a -> ST s [a] freeze bldr = do aliased <- readSTRef (start bldr) return $! foldr' (:) [] aliased