primus-0.1.0.0: src/Primus/NonEmpty.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
{- |
Module : Primus.NonEmpty
Description : utilities for nonempty lists
Copyright : (c) Grant Weyburne, 2022
License : BSD-3
-}
module Primus.NonEmpty (
MLR (..),
-- * zip
zipWithExtras1,
zipWithExtras,
mlrOrdering,
fromList1LR,
-- * chunking
chunksOf1,
chunksRange1,
chunkNLen,
chunkNLen1,
-- * split
Split1 (..),
split1Ordering,
splitAt1,
splitAt1',
splitAt1GE,
splitAts1,
splits1,
splits3,
-- * partition
partition1,
-- toThese1,
-- * span
spanAdjacent1,
breakAdjacent1,
span1,
break1,
-- * ascending order methods
Seq1 (..),
isSequence1,
isEnumAscending,
seq1Ordering,
-- * isomorphisms
uncons1,
unsnoc1,
consNonEmpty,
snocNonEmpty,
-- * positive specific functions
sumP,
lengthP,
-- * fold unfold
foldMapM1,
unfoldr1NE,
unfoldrM1,
-- * iterators
iterateMaybe1,
iterateMaybe1',
iterateN1,
replicateP,
-- * miscellaneous
appendL1,
appendR1,
snoc1,
updateAt1,
at1,
setAt1,
units1,
unitsF,
lengthExact1,
take1,
sum1,
groupByAdjacent1,
findDupsBy,
replicate1,
replicate1M,
) where
import Control.Arrow
import Control.Monad
import Data.Either
import Data.Foldable
import Data.Function
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as N
import Data.Pos
import Data.Semigroup
import Data.Semigroup.Foldable
import Data.These
import Data.Tuple
import qualified GHC.Exts as GE (IsList (..))
import Primus.Bool
import Primus.Error
import Primus.Extra
import Primus.Fold
import Primus.Lens
-- | zips two nonempty lists together and puts any leftovers into 'MLR'
zipWithExtras1 :: (a -> b -> c) -> NonEmpty a -> NonEmpty b -> (NonEmpty c, MLR a b)
zipWithExtras1 f (a :| as) (b :| bs) = first (f a b :|) $ zipWithExtras f as bs
-- | represents an optional 'Either' ie Maybe (Either (NonEmpty a) (NonEmpty b))
data MLR a b
= -- | extra values on the left hand side
MLRLeft !(NonEmpty a)
| -- | both values have the same length
MLREqual
| -- | extra values on the right hand side
MLRRight !(NonEmpty b)
deriving stock (Show, Eq, Ord)
-- | 'MLRLeft' predicate
mlrOrdering :: MLR a b -> Ordering
mlrOrdering = \case
MLRLeft{} -> LT
MLREqual -> EQ
MLRRight{} -> GT
-- | zips two lists together and puts any leftovers into 'MLR'
zipWithExtras :: forall a b c. (a -> b -> c) -> [a] -> [b] -> ([c], MLR a b)
zipWithExtras f = go
where
go [] [] = ([], MLREqual)
go (a : as) (b : bs) = let (x, y) = go as bs in (f a b : x, y)
go (a : as) [] = ([], MLRLeft (a :| as))
go [] (b : bs) = ([], MLRRight (b :| bs))
-- | conversion from list to a nonempty list
fromList1LR :: [a] -> Either String (NonEmpty a)
fromList1LR =
\case
[] -> Left "fromList1LR: empty list"
n : ns -> Right $ n :| ns
-- | split a nonempty list into a nonempty list of nonempty chunks
chunksOf1 :: Pos -> NonEmpty a -> NonEmpty (NonEmpty a)
chunksOf1 = join chunksRange1
{- | split a nonempty list into a nonempty list of nonempty chunks given a chunk size and how many to skip each iteration
can decide the size of the chunks and how many elements to skip
-}
chunksRange1 :: Pos -> Pos -> NonEmpty a -> NonEmpty (NonEmpty a)
chunksRange1 n (Pos skip) = unfoldr1NE (take1 n &&& N.drop skip)
{- | creates a nonempty container of length "sz" with chunks of a given size: @see 'chunkNLen'
must fill the container exactly
-}
chunkNLen1 ::
forall a u.
Foldable u =>
Pos ->
Pos ->
u a ->
Either String (NonEmpty (NonEmpty a))
chunkNLen1 sz = chunkNLen (units1 sz)
{- | fills a container "tz" with chunks of size "len"
must fill the container exactly
-}
chunkNLen ::
forall t a u z.
(Traversable t, Foldable u) =>
t z ->
Pos ->
u a ->
Either String (t (NonEmpty a))
chunkNLen tz len ua = do
chunkN' f tz (toList ua)
where
f :: [a] -> Either String ([a], NonEmpty a)
f = \case
[] -> Left "chunkNLen: not enough data"
x : xs -> swap <$> splitAt1GE len (x :| xs)
{- | unfoldr for a nonempty list
will not terminate if the user keeps returning a larger [s] than received
-}
unfoldr1NE ::
forall s a.
(NonEmpty s -> (a, [s])) ->
NonEmpty s ->
NonEmpty a
unfoldr1NE f = go
where
go :: NonEmpty s -> NonEmpty a
go ns =
let (a, ys) = f ns
in (a :|) $ case ys of
[] -> []
x : xs -> N.toList (go (x :| xs))
-- | 'replicate' for a nonempty list
replicate1 :: Pos -> a -> NonEmpty a
replicate1 (Pos n) a = a :| replicate (n - 1) a
-- | 'replicateM' for a nonempty list
replicate1M :: Applicative m => Pos -> m a -> m (NonEmpty a)
replicate1M (Pos n) ma = (:|) <$> ma <*> replicateM (n - 1) ma
-- | 'partitionThese' for a nonempty list
partition1 ::
Foldable1 t =>
(a -> Bool) ->
t a ->
These (NonEmpty a) (NonEmpty a)
partition1 p =
sconcat
. N.map (boolM p (This . pure) (That . pure))
. toNonEmpty
-- | internal function used by 'span1'
toThese1 ::
These (NonEmpty a) (NonEmpty b) ->
([a], [b]) ->
These (NonEmpty a) (NonEmpty b)
toThese1 th ns =
th & case ns of
([], []) -> id
(a : as, []) -> (<> This (a :| as))
([], b : bs) -> (<> That (b :| bs))
(a : as, b : bs) -> (<> These (a :| as) (b :| bs))
-- | 'span' for a nonempty list
span1 ::
Foldable1 t =>
(a -> Bool) ->
t a ->
These (NonEmpty a) (NonEmpty a)
span1 p (toNonEmpty -> (a :| as)) =
toThese1
( boolM
p
(That . pure)
(This . pure)
a
)
(L.span p as)
-- | 'break' for a nonempty list
break1 ::
Foldable1 t =>
(a -> Bool) ->
t a ->
These (NonEmpty a) (NonEmpty a)
break1 p = span1 (not . p)
-- | 'sum' for a nonempty list
sum1 :: (Foldable1 t, Num a) => t a -> a
sum1 = L.foldl' (+) 0
-- | predicate for an ascending nonempty list
isSequence1 :: (Foldable1 t, Eq a, Enum a) => t a -> Bool
isSequence1 = (EQ ==) . seq1Ordering . isEnumAscending
-- | possible results for determining if a nonempty list is in ascending order
data Seq1 a
= -- | generated enumerable sequence is shorter than the original list
S1Short !(NonEmpty a)
| -- | first mismatch
S1Fail !(a, a)
| -- | both sequences match
S1Ok
deriving stock (Show, Eq, Ord, Functor)
-- | predicate for 'S1Short'
seq1Ordering :: Seq1 a -> Ordering
seq1Ordering = \case
S1Short{} -> LT
S1Ok{} -> EQ
S1Fail{} -> GT
-- | shows the first failure or if the length of the enum is too short
isEnumAscending :: forall t a. (Foldable1 t, Eq a, Enum a) => t a -> Seq1 a
isEnumAscending (toNonEmpty -> as@(a :| _)) =
let (cs, me) = zipWithExtras1 f (a :| drop 1 [a ..]) as
in case me of
MLRLeft _ -> either S1Fail (const S1Ok) $ sequenceA cs
MLREqual -> either S1Fail (const S1Ok) $ sequenceA cs
MLRRight zs -> S1Short zs
where
f :: a -> a -> Either (a, a) ()
f x y = if x == y then Right () else Left (x, y)
-- | snoc for a nonempty list
snoc1 :: Foldable t => t a -> a -> NonEmpty a
snoc1 as a = foldr (N.<|) (pure a) as
-- | unsnoc for a nonempty list
unsnoc1 :: forall a. NonEmpty a -> ([a], a)
unsnoc1 = uncurry go . uncons1
where
go :: a -> [a] -> ([a], a)
go n [] = ([], n)
go n (x : xs) = first (n :) (go x xs)
-- | uncons for a nonempty list
uncons1 :: forall a. NonEmpty a -> (a, [a])
uncons1 (z :| zs) = (z, zs)
-- | cons iso from 'NonEmpty'
consNonEmpty :: Iso (NonEmpty a) (NonEmpty b) (a, [a]) (b, [b])
consNonEmpty = iso uncons1 (uncurry (:|))
-- | snoc iso from 'NonEmpty'
snocNonEmpty :: Iso (NonEmpty a) (NonEmpty b) ([a], a) ([b], b)
snocNonEmpty = iso unsnoc1 (uncurry snoc1)
-- | 'N.groupBy1' but applies the predicate to adjacent elements
groupByAdjacent1 :: forall a. (a -> a -> Bool) -> NonEmpty a -> NonEmpty (NonEmpty a)
groupByAdjacent1 p (a0 :| as0) =
let (as, ass) = go a0 as0
in (a0 :| as) :| ass
where
go :: a -> [a] -> ([a], [NonEmpty a])
go a' = \case
[] -> ([], [])
a : as ->
let (ys, zs) = go a as
in if p a' a
then (a : ys, zs)
else ([], (a :| ys) : zs)
-- | partition duplicates elements together with their positiion
findDupsBy :: forall a c. Ord c => (a -> c) -> [a] -> ([NonEmpty (Int, a)], [(Int, a)])
findDupsBy f =
partitionEithers
. map g
. N.groupAllWith (f . snd)
. zip [0 ..]
where
g :: NonEmpty (Int, a) -> Either (NonEmpty (Int, a)) (Int, a)
g = \case
x :| [] -> Right x
x :| y : ys -> Left (x :| y : ys)
-- | "foldMapM" for nonempty containers: uses Semigroup instead of Monoid
foldMapM1 ::
forall b m f a.
(Semigroup b, Monad m, Foldable1 f) =>
(a -> m b) ->
f a ->
m b
foldMapM1 f (toNonEmpty -> n :| ns) = foldr step return ns =<< f n
where
step :: a -> (b -> m b) -> b -> m b
step x r z = f x >>= \y -> r $! z <> y
-- | 'Primus.Fold.unfoldM' for nonempty results
unfoldrM1 :: Monad m => (s -> m (a, Maybe s)) -> s -> m (NonEmpty a)
unfoldrM1 f s = do
(a, ms) <- f s
case ms of
Nothing -> return (a :| [])
Just s' -> (a N.<|) <$> unfoldrM1 f s'
-- | 'take' for a nonempty list
take1 :: Pos -> NonEmpty a -> NonEmpty a
take1 (Pos i) (a :| as) = a :| take (i - 1) as
-- | 'splitAt' for a nonempty list but doesnt guarantee the number of elements
splitAt1 :: Pos -> NonEmpty a -> (NonEmpty a, [a])
splitAt1 (Pos i) (a :| as) = first (a :|) (splitAt (i - 1) as)
-- | comparator for 'Split1'
split1Ordering :: Split1 a -> Ordering
split1Ordering = \case
SplitLT{} -> LT
SplitEQ{} -> EQ
SplitGT{} -> GT
-- | represents the status of a split a nonempty list
data Split1 a
= SplitLT !Pos
| SplitEQ
| SplitGT !(NonEmpty a)
deriving stock (Ord, Show, Eq)
-- | split a nonempty list preserving information about the split
splitAt1' :: forall a. Pos -> NonEmpty a -> (NonEmpty a, Split1 a)
splitAt1' = go _1P
where
go :: Pos -> Pos -> NonEmpty a -> (NonEmpty a, Split1 a)
go !i !n (a :| [])
| i == n = (a :| [], SplitEQ)
| otherwise = (a :| [], SplitLT i)
go !i !n (a :| a1 : as)
| i == n = (a :| [], SplitGT (a1 :| as))
| otherwise =
let (ys, y) = go (succP i) n (a1 :| as)
in (a N.<| ys, y)
-- | split a nonempty list but has to have enough elements else fails
splitAt1GE :: Pos -> NonEmpty a -> Either String (NonEmpty a, [a])
splitAt1GE n as =
let (ns, z) = splitAt1' n as
in (ns,) <$> case z of
SplitLT (Pos len) -> Left $ "not enough elements: expected " ++ show (unP n) ++ " found " ++ show len
SplitEQ -> pure mempty
SplitGT ex -> pure (N.toList ex)
-- | repeatedly split a nonempty list
splitAts1 :: Pos -> NonEmpty a -> NonEmpty (NonEmpty a)
splitAts1 i = unfoldr1NE (splitAt1 i) . toNonEmpty
-- | compares the length of a potentially infinite nonempty list with "n" and succeeds if they are the same
lengthExact1 :: Pos -> NonEmpty a -> Either String (NonEmpty a)
lengthExact1 n xs =
let (as, z) = splitAt1' n xs
in case z of
SplitLT (Pos len) -> Left $ "LT: not enough elements: expected " ++ show (unP n) ++ " found " ++ show len
SplitEQ -> Right as
SplitGT _ -> Left $ "GT: too many elements: expected " ++ show (unP n)
-- | break up a nonempty list into all possible pairs of nonempty lists
splits1 :: forall a. NonEmpty a -> [(NonEmpty a, NonEmpty a)]
splits1 (n :| ns) = go ([n], ns)
where
go :: ([a], [a]) -> [(NonEmpty a, NonEmpty a)]
go = \case
([], _) -> []
(_, []) -> []
(a : as, b : bs) -> (a :| as, b :| bs) : go (a : as ++ [b], bs)
-- | like 'Data.List.iterate' but allows termination using Maybe
iterateMaybe1' :: (a -> Maybe a) -> a -> NonEmpty a
iterateMaybe1' f a0 = a0 :| go a0
where
go a = case f a of
Nothing -> []
Just x -> x : go x
-- | like 'iterateMaybe1'' with 'boolMaybe'
iterateMaybe1 :: (a -> Bool) -> (a -> a) -> a -> NonEmpty a
iterateMaybe1 f g = iterateMaybe1' (boolMaybe f g)
-- | iterate "n" times
iterateN1 :: Pos -> (a -> a) -> a -> NonEmpty a
iterateN1 n = take1 n .@ N.iterate
-- | break up a nonempty list into a nonempty list of three parts
splits3 :: forall a. NonEmpty a -> NonEmpty ([a], a, [a])
splits3 (n :| ns) = N.scanl f ([], n, ns) ns
where
f :: forall z. ([a], a, [a]) -> z -> ([a], a, [a])
f (xs, y, zs') _ = case zs' of
[] -> programmError "splits3"
z : zs -> (xs ++ [y], z, zs)
-- | like 'Data.List.span' but applies the predicate to adjacent elements
spanAdjacent1 :: (a -> a -> Bool) -> NonEmpty a -> (NonEmpty a, [a])
spanAdjacent1 p (a0 :| as0) = first (a0 :|) (go a0 as0)
where
go a' = \case
[] -> ([], [])
a : as
| p a' a -> first (a :) (go a as)
| otherwise -> ([], a : as)
-- | like 'Data.List.break' but applies the predicate to adjacent elements
breakAdjacent1 :: (a -> a -> Bool) -> NonEmpty a -> (NonEmpty a, [a])
breakAdjacent1 p = spanAdjacent1 (not .@ p)
-- | append a list with a nonempty list
appendL1 :: [a] -> NonEmpty a -> NonEmpty a
appendL1 as bs = foldr N.cons bs as
-- | append a nonempty list with a list
appendR1 :: NonEmpty a -> [a] -> NonEmpty a
appendR1 (a :| as) bs = a :| (as <> bs)
-- | set a value at an index starting at one
setAt1 :: Pos -> a -> NonEmpty a -> Maybe (NonEmpty a)
setAt1 i = updateAt1 i . const
-- | update a value at an index starting at one
updateAt1 :: Pos -> (a -> a) -> NonEmpty a -> Maybe (NonEmpty a)
updateAt1 (Pos i) f ns =
case N.splitAt (i - 1) ns of
([], b : bs) -> Just (f b :| bs)
(a : as, b : bs) -> Just (a :| as ++ (f b : bs))
(_, []) -> Nothing
-- | get a value at an index starting at one
at1 :: Pos -> NonEmpty a -> Maybe a
at1 (Pos i) ns =
case N.splitAt (i - 1) ns of
(_, b : _) -> Just b
(_, []) -> Nothing
-- | generate a repeated nonempty list of values for a fixed size
replicateP :: Pos -> a -> NonEmpty a
replicateP (Pos i) a = a :| replicate (i - 1) a
-- | length of nonempty list
lengthP :: Foldable1 t => t a -> Pos
lengthP = unsafePos "lengthP" . N.length . toNonEmpty
{-# INLINE lengthP #-}
-- | generate a nonempty list of units for a fixed size
units1 :: Pos -> NonEmpty ()
units1 = unitsF
-- | generate a nonempty list of units for a given container of the given size
unitsF :: forall l a. (GE.IsList (l a), GE.Item (l a) ~ ()) => Pos -> l a
unitsF = GE.fromList . flip replicate () . unP
-- | sum of nonempty list of 'Pos' values
sumP :: Foldable1 t => t Pos -> Pos
sumP = L.foldr1 (+!)
{-# INLINE sumP #-}