primus-0.3.0.0: src/Primus/List.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{- |
Module : Primus.List
Description : list functions
Copyright : (c) Grant Weyburne, 2022
License : BSD-3
-}
module Primus.List (
-- * partition methods
partitionEithersL,
partitionEithersL',
partitionTheseL,
partitionTheseL',
partitionM,
-- * span methods
spanMaybe,
spanMaybe',
lengthExact,
zipWithLongest,
zipLongest,
-- * chunking
pairsOf1,
pairsOf2,
pairsOf',
chunksOf,
-- * split methods
splitAtLGE,
splits,
SplitL (..),
splitAtL,
atL,
atNoteL,
updateAtL,
setAtL,
-- * miscellaneous
allEqual,
allEqualBy,
snocL,
unsnocL,
unsnocL',
list,
list',
listSnoc,
) where
import Control.Arrow
import Data.Bool
import Data.Either
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as N
import Data.Pos
import Data.These
import GHC.Stack
import Primus.Bool
import Primus.Error
-- | split a list into overlapping pairs plus overflow
pairsOf1 :: [a] -> ([(a, a)], Maybe a)
pairsOf1 = pairsOf' _1P
-- | split a list into non-overlapping pairs plus overflow
pairsOf2 :: [a] -> ([(a, a)], Maybe a)
pairsOf2 = pairsOf' _2P
-- | split into pairs skipping given number of values
pairsOf' :: forall a. Pos -> [a] -> ([(a, a)], Maybe a)
pairsOf' (Pos i) = go
where
go :: [a] -> ([(a, a)], Maybe a)
go =
\case
[] -> ([], Nothing)
[a] -> ([], Just a)
[a, a'] -> ([(a, a')], Nothing)
(a : a' : a'' : as) ->
let (x, y) = go (drop (i - 1) (a' : a'' : as))
in ((a, a') : x, y)
-- | simple utility for chunking data but guarantees we make progress
chunksOf :: forall a. Pos -> [a] -> [[a]]
chunksOf (Pos n) = L.unfoldr f
where
f :: [a] -> Maybe ([a], [a])
f = \case
[] -> Nothing
xs@(_ : _) -> Just (splitAt n xs)
-- | checks that the list has all the same values
allEqual :: Eq a => [a] -> Either (a, a) ()
allEqual = allEqualBy (==)
-- | checks that the list has all the same values with a predicate
allEqualBy :: (a -> a -> Bool) -> [a] -> Either (a, a) ()
allEqualBy f =
\case
[] -> pure ()
[_] -> pure ()
x : x' : xs
| f x x' -> allEqualBy f (x' : xs)
| otherwise -> Left (x, x')
-- | represents the status of a split on a list
data SplitL a
= SplitLNeg !Pos
| SplitLLT !Int
| SplitLEQ
| SplitLGT !(NonEmpty a)
deriving stock (Ord, Show, Eq)
-- | split a list preserving information about the split
splitAtL :: forall a. Int -> [a] -> ([a], SplitL a)
splitAtL n xs
| n < 0 = (xs, SplitLNeg (unsafePos "splitAtL" (-n)))
| otherwise = go 0 xs
where
go :: Int -> [a] -> ([a], SplitL a)
go i []
| i == n = ([], SplitLEQ)
| otherwise = ([], SplitLLT i)
go i (a : as)
| i == n = ([], SplitLGT (a :| as))
| otherwise = first (a :) (go (i + 1) as)
-- | split a list but has to have enough elements else fails
splitAtLGE :: Int -> [a] -> Either String ([a], [a])
splitAtLGE n as =
let (ns, z) = splitAtL n as
in (ns,) <$> case z of
SplitLNeg (Pos j) -> Left $ "negative index " ++ show j
SplitLLT len -> Left $ "not enough elements: expected " ++ show n ++ " found " ++ show len
SplitLEQ -> pure mempty
SplitLGT ex -> pure (N.toList ex)
-- | set a value at a given index in a list
setAtL :: Int -> a -> [a] -> Maybe [a]
setAtL i0 = updateAtL i0 . const
-- | update a value at a given index in a list
updateAtL :: Int -> (a -> a) -> [a] -> Maybe [a]
updateAtL i f as0 =
case atLImpl i as0 of
Right (a, (xs, ys)) -> Just (xs <> (f a : ys))
Left _ -> Nothing
-- | update a value at a given index in a list
atLImpl :: Int -> [a] -> Either String (a, ([a], [a]))
atLImpl i as0 =
let (xs, ys) = splitAtL i as0
in case ys of
SplitLNeg (Pos j) -> Left $ "negative index " ++ show j
SplitLLT _ -> Left $ "LT: i=" <> show i <> " out of bounds"
SplitLEQ -> Left $ "EQ: i=" <> show i <> " out of bounds"
SplitLGT (a :| as) -> Right (a, (xs, as))
-- | index into a list
atL :: Int -> [a] -> Maybe a
atL = either (const Nothing) (Just . fst) .@ atLImpl
-- | unsafe index into a list
atNoteL :: HasCallStack => String -> [a] -> Int -> a
atNoteL msg = fst . forceRight msg .@ flip atLImpl
-- | compares the length of a potentially infinite list with "n" and succeeds if they are the same
lengthExact :: Int -> [a] -> Either String [a]
lengthExact n xs =
let (as, z) = splitAtL n xs
in case z of
SplitLNeg (Pos j) -> Left $ "negative index " ++ show j
SplitLLT len -> Left $ "LT: expected " ++ show n ++ " found " ++ show len
SplitLEQ -> Right as
SplitLGT _ -> Left $ "GT: too many elements: expected " ++ show n
-- | creates the longest of the two lists: fills with 'This' or 'That'
zipWithLongest :: forall a b c. (These a b -> c) -> [a] -> [b] -> [c]
zipWithLongest f = go .@ (,)
where
go = \case
([], []) -> []
(xs@(_ : _), []) -> map (f . This) xs
([], ys@(_ : _)) -> map (f . That) ys
(x : xs, y : ys) -> f (These x y) : go (xs, ys)
-- | 'zipWithLongest' for 'id'
zipLongest :: [a] -> [b] -> [These a b]
zipLongest = zipWithLongest id
-- | break up a list into all possible pairs of nonempty lists: see 'Primus.NonEmpty.splits1'
splits :: forall a. [a] -> [([a], [a])]
splits = \case
[] -> []
x : xs -> go ([x], xs)
where
go :: ([a], [a]) -> [([a], [a])]
go = \case
([], _) -> []
(_, []) -> []
(a : as, b : bs) -> (a : as, b : bs) : go (a : as ++ [b], bs)
-- | like 'Data.List.partition' but allow the user to change the types of "e" and "b" using 'Either'
partitionEithersL' :: Foldable t => (a -> Either e b) -> t a -> ([e], [b])
partitionEithersL' f = partitionEithers . foldr ((:) . f) []
-- | like 'partitionEithersL'' using 'Primus.Bool.boolEither'
partitionEithersL :: Foldable t => (a -> Bool) -> (a -> e) -> (a -> b) -> t a -> ([e], [b])
partitionEithersL p l r = partitionEithers . foldr ((:) . boolEither p l r) []
-- | like 'Data.List.partition' but allow the user to change the types of "e" and "b" using 'These'
partitionTheseL' :: Foldable t => (a -> These e b) -> t a -> ([e], [b], [(e, b)])
partitionTheseL' f = partitionThese . foldr ((:) . f) []
-- | like 'partitionTheseL' using 'Primus.Bool.boolThese'
partitionTheseL :: Foldable t => (a -> Bool) -> (a -> Bool) -> (a -> e) -> (a -> b) -> t a -> ([e], [b], [(e, b)])
partitionTheseL p q l r = partitionThese . foldr ((:) . boolThese p q l r) []
-- | like 'Data.List.span' but allow the user to change the success type using 'Maybe'
spanMaybe' :: (a -> Maybe b) -> [a] -> ([b], [a])
spanMaybe' f = go
where
go = \case
[] -> ([], [])
a : as -> case f a of
Nothing -> ([], a : as)
Just b -> first (b :) (go as)
-- | like 'spanMaybe'' using 'Primus.Bool.boolMaybe'
spanMaybe :: (a -> Bool) -> (a -> b) -> [a] -> ([b], [a])
spanMaybe p r = spanMaybe' (boolMaybe p r)
-- | partition for an applicative
partitionM :: Applicative m => (a -> m Bool) -> [a] -> m ([a], [a])
partitionM f = go
where
go = \case
[] -> pure mempty
a : as -> (\b -> bool first second b (a :)) <$> f a <*> go as
-- | break up a list into cases using cons
list :: b -> (a -> [a] -> b) -> [a] -> b
list z s = \case
[] -> z
a:as -> s a as
-- | break up a list into cases using cons
list' :: [a] -> b -> (a -> [a] -> b) -> b
list' as z s = list z s as
-- | break up a list into cases using snoc
listSnoc :: b -> ([a] -> a -> b) -> [a] -> b
listSnoc z s = maybe z (uncurry s) . unsnocL
-- | snoc for a list
snocL :: [a] -> a -> [a]
snocL as a = as ++ [a]
-- | unsnoc for a list
unsnocL :: [a] -> Maybe ([a], a)
unsnocL = list Nothing (Just .@ unsnocL')
-- | unsnoc for a value and a list
unsnocL' :: a -> [a] -> ([a], a)
unsnocL' a =
\case
[] -> ([], a)
x : xs -> first (a :) (unsnocL' x xs)