circular-0.1.0: src/Data/Stack/Circular.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module : Data.Stack.Circular
-- Description : Circular stacks of fixed size
-- Copyright : (c) Dominik Schrempf, 2020
-- License : GPL-3.0-or-later
--
-- Maintainer : dominik.schrempf@gmail.com
-- Stability : unstable
-- Portability : portable
--
-- Creation date: Thu Jun 18 10:00:28 2020.
module Data.Stack.Circular
( -- * Boxed circular stacks
CStack (..),
-- * Construction
empty,
-- * Conversion
toVector,
toVectorN,
fromVector,
-- * Accessors
get,
pop,
push,
unsafePush,
-- * Queries
isFull,
-- * Folding
--
-- Here all fold functions should be provided, but I am too lazy. Instead,
-- let's just provide some optimized functions to compute summary statistics
-- across all values on the stack.
--
-- For reasons of efficiency, __commutativity__ of the combining function is
-- __assumed__ for fold-like functions provided in this section! That is,
-- the order of elements of the stack must not matter.
foldl1',
sum,
mean,
product,
)
where
import Control.Monad.ST
import Data.Aeson
import Data.Aeson.Types
import qualified Data.Vector.Generic as V
import Data.Vector.Generic (Vector)
import qualified Data.Vector.Generic.Mutable as M
import Prelude hiding (product, sum)
-- | Circular stacks with fxed maximum size are just normal vectors with a
-- pointer to the last element.
--
-- Construction of 'CStack's is done with 'empty' and subsequent 'push'es, or
-- the provided type conversion functions so that the index and bounds are
-- updated and checked consistently. The data constructor 'CStack' is exported
-- only to enable creation of orphan instances such as Arbitrary (QuickCheck).
--
-- When denoting the efficiency of the functions @m@ refers to the current size
-- of the stack, and @n@ to the maximum size.
data CStack v a = CStack
{ stack :: v a,
index :: !Int,
curSize :: !Int
}
instance (Eq (v a), Vector v a) => Eq (CStack v a) where
(CStack v1 i1 m1) == (CStack v2 i2 m2) = (v1 == v2) && (i1 == i2) && (m1 == m2)
instance (Show (v a), Vector v a) => Show (CStack v a) where
show c@(CStack _ i m) = "CStack {" ++ show (toVector c) ++ ", " ++ show i ++ ", " ++ show m ++ "}"
-- | We have @c /= FromJSON $ ToJSON c@, but the elements on the stack and their
-- order are correctly saved and restored.
instance (ToJSON a, ToJSON (v a), Vector v a) => ToJSON (CStack v a) where
toJSON c = object ["stack" .= toVector c, "maxSize" .= n]
where
n = V.length $ stack c
toEncoding c = pairs ("stack" .= toVector c <> "maxSize" .= n)
where
n = V.length $ stack c
instance (FromJSON a, FromJSON (v a), Vector v a) => FromJSON (CStack v a) where
parseJSON = withObject "CStack" fromObject
fromObject :: forall v a. (FromJSON (v a), Vector v a) => Object -> Parser (CStack v a)
fromObject o = do
v <- o .: "stack" :: Parser (v a)
n <- o .: "maxSize" :: Parser Int
let c = empty n
pure $ V.foldr' unsafePush c v
-- Calculate the start index of the stack.
--
-- (startIndex + m - 1) `mod` n = i
startIndex :: Int -> Int -> Int -> Int
startIndex i m n
| m == 0 = error "startIndex: empty stack"
| m <= i + 1 = i + 1 - m
| otherwise = i + 1 - m + n
-- | A circular stack without an element but of a given maximum size. At this
-- state, it is not very useful :). O(n).
empty :: Vector v a => Int -> CStack v a
empty n
| n <= 0 = error "empty: maximum size must be 1 or larger"
| otherwise = CStack (V.create $ M.unsafeNew n) 0 0
-- | Convert a circular stack to a vector. The first element of the returned
-- vector is the deepest (oldest) element of the stack, the last element of the
-- returned vector is the current (newest) element of the stack.
--
-- This is a relatively expensive operation. O(m).
toVector :: Vector v a => CStack v a -> v a
toVector (CStack v i m)
| m == 0 = V.empty
| i' + m <= n = V.unsafeSlice i' m v
| otherwise = V.unsafeDrop i' v V.++ V.unsafeTake (i + 1) v
where
n = V.length v
i' = startIndex i m n
-- | Convert the last N elements of a circular stack to a vector. The first
-- element of the returned vector is the deepest (oldest) element of the stack,
-- the last element of the returned vector is the current (newest) element of
-- the stack.
--
-- The size of the stack must be larger than N.
--
-- This is a relatively expensive operation. O(N).
toVectorN :: Vector v a => Int -> CStack v a -> v a
toVectorN k (CStack v i m)
| k < 0 = error "toVectorN: negative n"
| k > m = error "toVectorN: stack too small"
| k == 0 = V.empty
| i' + k <= n = V.unsafeSlice i' k v
| otherwise = V.unsafeDrop i' v V.++ V.unsafeTake (i + 1) v
where
n = V.length v
i' = startIndex i k n
-- | Convert a vector to a circular stack. The first element of the vector is
-- the deepest (oldest) element of the stack, the last element of the vector is
-- the current (newest) element of the stack. O(n).
--
-- The vector must be non-empty.
fromVector :: Vector v a => v a -> CStack v a
fromVector v
| V.null v = error "fromVector: empty vector"
| otherwise = CStack v (n - 1) n
where
n = V.length v
-- | Get the last element without changing the stack. O(1).
get :: Vector v a => CStack v a -> a
get (CStack v i _) = V.unsafeIndex v i
-- Select the previous element without changing the stack.
previous :: Vector v a => CStack v a -> CStack v a
previous (CStack v i m)
| m == 0 = error "previous: empty stack"
| i == 0 = CStack v (n - 1) (m - 1)
| otherwise = CStack v (i - 1) (m - 1)
where
n = V.length v
-- | Get the last element and remove it from the stack. O(1).
--
-- The stack must be non-empty.
pop :: Vector v a => CStack v a -> (a, CStack v a)
pop c = (get c, previous c)
-- Replace an element in a vector.
set :: Vector v a => Int -> a -> v a -> v a
set i x = V.modify (\v -> M.write v i x)
{-# INLINE set #-}
-- Replace the last element.
put :: Vector v a => a -> CStack v a -> CStack v a
put x (CStack v i m) = CStack (set i x v) i m
-- Select the next element without changing the stack.
next :: Vector v a => CStack v a -> CStack v a
next (CStack v i m)
| i == (n - 1) = CStack v 0 (min (m + 1) n)
| otherwise = CStack v (i + 1) (min (m + 1) n)
where
n = V.length v
-- | Push an element on the stack. O(n).
push :: Vector v a => a -> CStack v a -> CStack v a
push x c = put x $ next c
unsafeSet :: Vector v a => Int -> a -> v a -> v a
unsafeSet i x v = runST $ do
mv <- V.unsafeThaw v
M.unsafeWrite mv i x
V.unsafeFreeze mv
-- Replace the last element. O(1).
unsafePut :: Vector v a => a -> CStack v a -> CStack v a
unsafePut x (CStack v i m) = CStack (unsafeSet i x v) i m
-- | Push an element on the stack. O(1).
--
-- Be careful; the internal vector is mutated! The immutable circular stack may
-- not be used after this operation.
unsafePush :: Vector v a => a -> CStack v a -> CStack v a
unsafePush x c = unsafePut x $ next c
-- | Check if the stack is full.
isFull :: Vector v a => CStack v a -> Bool
isFull (CStack v _ m) = V.length v == m
-- | Compute summary statistics of the elements on the stack using a custom
-- commutative `mappend` function.
foldl1' :: Vector v a => (a -> a -> a) -> CStack v a -> a
foldl1' f (CStack v i m)
| m == n = V.foldl1' f v
| i' + m <= n = V.foldl1' f $ V.unsafeSlice i' m v
| otherwise = f (V.foldl1' f (V.unsafeDrop i' v)) (V.foldl1' f (V.unsafeTake (i + 1) v))
where
n = V.length v
i' = startIndex i m n
-- | Compute the sum of the elements on the stack.
sum :: (Num a, Vector v a) => CStack v a -> a
sum (CStack v i m)
| m == n = V.sum v
| i' + m <= n = V.sum $ V.unsafeSlice i' m v
| otherwise = V.sum (V.unsafeDrop i' v) + V.sum (V.unsafeTake (i + 1) v)
where
n = V.length v
i' = startIndex i m n
-- | Compute the mean of the elements on the stack.
mean :: (Real a, Vector v a, Fractional b) => CStack v a -> b
mean c = realToFrac (sum c) / fromIntegral (curSize c)
-- | Compute the product of the elements on the stack.
--
-- For reasons of efficiency, commutativity of the combining function is
-- assumed. That is, the order of elements of the stack must not matter.
product :: (Num a, Vector v a) => CStack v a -> a
product (CStack v i m)
| m == n = V.product v
| i' + m <= n = V.product $ V.unsafeSlice i' m v
| otherwise = V.product (V.unsafeDrop i' v) * V.product (V.unsafeTake (i + 1) v)
where
n = V.length v
i' = startIndex i m n