{- | A /semigroup/ ('Semigroup') is a set with a binary associative operation (@<>@). This module defines a /partial semigroup/ ('PartialSemigroup'), a semigroup for which @<>@ is not required to be defined over all inputs. -} module Data.PartialSemigroup ( -- * Partial semigroup PartialSemigroup (..) -- * Either -- $either , AppendLeft (..) , AppendRight (..) -- * Tuples -- $tuple -- * Concatenation , groupAndConcat , partialConcat , partialConcat1 -- * Zipping , partialZip , partialZip1 -- * Total to partial -- $total , Total (..) -- * Partial to total -- $partial , Partial (..) -- * Refusing to combine -- $refusing , One (..) , AtMostOne (..) ) where import Control.Applicative (ZipList (..), (<$>), (<*>)) import Control.Monad ((>>=)) import Data.Either (Either (..)) import Data.Function ((.)) import Data.Functor.Identity (Identity (..)) import Data.List.NonEmpty (NonEmpty (..), nonEmpty) import Data.Maybe (Maybe (..)) import Data.Monoid (Product (..), Sum (..)) import Data.Semigroup (Semigroup (..)) import Prelude (Eq, Num (..), Ord, Read, Show) {- $setup >>> import Data.Function (($)) >>> import Data.Functor (fmap) -} -- The same fixity as <> infixr 6 <>? {- | A 'PartialSemigroup' is like a 'Semigroup', but with an operator returning @'Maybe' a@ rather than @a@. For comparison: @ ('<>') :: 'Semigroup' a => a -> a -> a ('<>?') :: 'PartialSemigroup' a => a -> a -> 'Maybe' a @ === The associativity axiom for partial semigroups For all @x@, @y@, @z@: * If @x '<>?' y = 'Just' xy@ and @y '<>?' z = 'Just' yz@, then * @x '<>?' yz = xy '<>?' z@. ==== Relationship to the semigroup associativity axiom The partial semigroup associativity axiom is a natural adaptation of the semigroup associativity axiom @x '<>' (y '<>' z) = (x '<>' y) '<>' z@ with a slight modification to accommodate situations where '<>' is undefined. We may gain some insight into the connection between 'Semigroup' and 'PartialSemigroup' by rephrasing the partial semigroup associativity in terms of a partial '<>' operator thusly: For all @x@, @y@, @z@: * If @x '<>' y@ and @y '<>' z@ are both defined, then * @x '<>' (y '<>' z)@ is defined if and only if @(x '<>' y) '<>' z@ is defined, and * if these things /are/ all defined, then the axiom for total semigroups @x '<>' (y '<>' z) = (x '<>' y) '<>' z@ must hold. -} class PartialSemigroup a where (<>?) :: a -> a -> Maybe a -------------------------------------------------------------------------------- instance PartialSemigroup () where () <>? () = Just () -------------------------------------------------------------------------------- instance PartialSemigroup [a] where x <>? y = Just (x <> y) -------------------------------------------------------------------------------- instance Num a => PartialSemigroup (Sum a) where x <>? y = Just (x <> y) instance Num a => PartialSemigroup (Product a) where x <>? y = Just (x <> y) -------------------------------------------------------------------------------- instance PartialSemigroup a => PartialSemigroup (Identity a) where Identity x <>? Identity y = Identity <$> (x <>? y) -------------------------------------------------------------------------------- instance (PartialSemigroup a, PartialSemigroup b) => PartialSemigroup (Either a b) where Left x <>? Left y = Left <$> (x <>? y) Right x <>? Right y = Right <$> (x <>? y) _ <>? _ = Nothing {- $either The exemplary nontrivial 'PartialSemigroup' is 'Either', for which the append operator produces a 'Just' result only if both arguments are 'Left' or both arguments are 'Right'. >>> Left "ab" <>? Left "cd" Just (Left "abcd") >>> Left "ab" <>? Right [1, 2] Nothing -} -------------------------------------------------------------------------------- {- $tuple A tuple forms a partial semigroups when all of its constituent parts have partial semigroups. The append operator returns a 'Just' value when /all/ of the fields' append operators must return 'Just' values. >>> x = (Left "ab", Right "hi") >>> y = (Left "cd", Right "jk") >>> x <>? y Just (Left "abcd",Right "hijk") >>> x = (Left "ab", Right "hi") >>> y = (Left "cd", Left "jk") >>> x <>? y Nothing -} instance (PartialSemigroup a, PartialSemigroup b) => PartialSemigroup (a, b) where (a, b) <>? (a', b') = (,) <$> (a <>? a') <*> (b <>? b') instance (PartialSemigroup a, PartialSemigroup b, PartialSemigroup c) => PartialSemigroup (a, b, c) where (a, b, c) <>? (a', b', c') = (,,) <$> (a <>? a') <*> (b <>? b') <*> (c <>? c') -------------------------------------------------------------------------------- {- | Apply a semigroup operation to any pairs of consecutive list elements where the semigroup operation is defined over them. ==== Examples For 'Either', 'groupAndConcat' combines contiguous sublists of 'Left' and contiguous sublists of 'Right'. >>> xs = [Left "a", Right "b", Right "c", Left "d", Left "e", Left "f"] >>> groupAndConcat xs [Left "a",Right "bc",Left "def"] -} groupAndConcat :: PartialSemigroup a => [a] -> [a] groupAndConcat [] = [] groupAndConcat [x] = [x] groupAndConcat (x : y : zs) = case x <>? y of Nothing -> x : groupAndConcat (y : zs) Just a -> groupAndConcat (a : zs) {- | If @xs@ is nonempty and the partial semigroup operator is defined for all pairs of values in @xs@, then @'partialConcat' xs@ produces a 'Just' result with the combination of all the values. Otherwise, returns 'Nothing'. ==== Examples When all values can combine, we get a 'Just' of their combination. >>> partialConcat [Left "a", Left "b", Left "c"] Just (Left "abc") When some values cannot be combined, we get 'Nothing'. >>> partialConcat [Left "a", Left "b", Right "c"] Nothing When the list is empty, we get 'Nothing'. >>> partialConcat [] Nothing -} partialConcat :: PartialSemigroup a => [a] -> Maybe a partialConcat x = nonEmpty x >>= partialConcat1 {- | Like 'partialConcat', but for non-empty lists. ==== Examples When all values can combine, we get a 'Just' of their combination. >>> partialConcat1 (Left "a" :| [Left "b", Left "c"]) Just (Left "abc") When some values cannot be combined, we get 'Nothing'. >>> partialConcat1 (Left "a" :| [Left "b", Right "c"]) Nothing -} partialConcat1 :: PartialSemigroup a => NonEmpty a -> Maybe a partialConcat1 (x :| []) = Just x partialConcat1 (x :| (y : zs)) = do a <- x <>? y partialConcat1 (a :| zs) {- | ==== Examples If lists are the same length and each pair of elements successfully, then we get a 'Just' result. >>> xs = [Left "a", Left "b", Right "c"] >>> ys = [Left "1", Left "2", Right "3"] >>> partialZip xs ys Just [Left "a1",Left "b2",Right "c3"] If the pairs do not all combine, then we get 'Nothing'. >>> xs = [Left "a", Left "b", Right "c"] >>> ys = [Left "1", Right "2", Right "3"] >>> partialZip xs ys Nothing If the lists have different lengths, then we get 'Nothing'. >>> xs = [Left "a", Left "b", Right "c"] >>> ys = [Left "1", Left "2"] >>> partialZip xs ys Nothing -} partialZip :: PartialSemigroup a => [a] -> [a] -> Maybe [a] partialZip [] [] = Just [] partialZip [] _ = Nothing partialZip _ [] = Nothing partialZip (x:xs) (y:ys) = (:) <$> (x <>? y) <*> partialZip xs ys {- | Like 'partialZip', but for non-empty lists. ==== Examples If lists are the same length and each pair of elements successfully, then we get a 'Just' result. >>> xs = Left "a" :| [Left "b", Right "c"] >>> ys = Left "1" :| [Left "2", Right "3"] >>> partialZip1 xs ys Just (Left "a1" :| [Left "b2",Right "c3"]) If the pairs do not all combine, then we get 'Nothing'. >>> xs = Left "a" :| [Left "b", Right "c"] >>> ys = Left "1" :| [Right "2", Right "3"] >>> partialZip1 xs ys Nothing If the lists have different lengths, then we get 'Nothing'. >>> xs = Left "a" :| [Left "b", Right "c"] >>> ys = Left "1" :| [Left "2"] >>> partialZip1 xs ys Nothing -} partialZip1 :: PartialSemigroup a => NonEmpty a -> NonEmpty a -> Maybe (NonEmpty a) partialZip1 (x :| xs) (y :| ys) = (:|) <$> (x <>? y) <*> partialZip xs ys -- | 'partialZip' instance PartialSemigroup a => PartialSemigroup (ZipList a) where ZipList x <>? ZipList y = ZipList <$> partialZip x y -------------------------------------------------------------------------------- {- $partial For every type @a@ with a 'PartialSemigroup', we can construct a total 'Semigroup' for @'Maybe' a@ as: @ 'Just' x <> 'Just' y = x '<>?' y _ '<>' _ = 'Nothing' @ We don't actually define this instance for 'Maybe' because it already has a different 'Semigroup' defined over it, but we do provide the 'Partial' wrapper which has this instance. -} {- | A wrapper for 'Maybe' with an error-propagating 'Semigroup'. -} newtype Partial a = Partial { unPartial :: Maybe a } deriving (Eq, Ord, Read, Show) instance PartialSemigroup a => Semigroup (Partial a) where Partial (Just x) <> Partial (Just y) = Partial (x <>? y) _ <> _ = Partial Nothing -------------------------------------------------------------------------------- {- $total For every type with a 'Semigroup', we can trivially construct a 'PartialSemigroup' as: @ x '<>?' y = 'Just' (x '<>' y) @ Additionally, any type with a 'Semigroup' can be treated as a 'PartialSemigroup' by lifting it into 'Total'. -} {- | A wrapper to turn any value with a 'Semigroup' instance into a value with a 'PartialSemigroup' instance whose '<>?' operator always returns 'Just'. ==== Examples >>> Total "ab" <>? Total "cd" Just (Total {unTotal = "abcd"}) >>> f = getProduct . unTotal >>> g = Total . Product >>> fmap f . partialConcat . fmap g $ [1..4] Just 24 -} newtype Total a = Total { unTotal :: a } deriving (Eq, Ord, Read, Show) instance Semigroup a => PartialSemigroup (Total a) where Total x <>? Total y = Just (Total (x <> y)) -------------------------------------------------------------------------------- {- | A wrapper for 'Either' where the 'PartialSemigroup' operator is defined only over 'Left' values. ==== Examples Two 'Left's make a 'Just'. >>> AppendLeft (Left "ab") <>? AppendLeft (Left "cd") Just (AppendLeft {unAppendLeft = Left "abcd"}) Anything else produces 'Nothing' >>> AppendLeft (Right "ab") <>? AppendLeft (Right "cd") Nothing 'groupAndConcat' combines consecutive 'Left' values, leaving the 'Right' values unmodified. >>> xs = [Left "a", Left "b", Right "c", Right "d", Left "e", Left "f"] >>> fmap unAppendLeft . groupAndConcat . fmap AppendLeft $ xs [Left "ab",Right "c",Right "d",Left "ef"] -} newtype AppendLeft a b = AppendLeft { unAppendLeft :: Either a b } deriving (Eq, Ord, Read, Show) instance PartialSemigroup a => PartialSemigroup (AppendLeft a b) where AppendLeft (Left x) <>? AppendLeft (Left y) = AppendLeft . Left <$> (x <>? y) _ <>? _ = Nothing -------------------------------------------------------------------------------- {- | A wrapper for 'Either' where the 'PartialSemigroup' operator is defined only over 'Right' values. ==== Examples Two 'Right's make a 'Just'. >>> AppendRight (Right "ab") <>? AppendRight (Right "cd") Just (AppendRight {unAppendRight = Right "abcd"}) Anything else produces 'Nothing' >>> AppendRight (Left "ab") <>? AppendRight (Left "cd") Nothing 'groupAndConcat' combines consecutive 'Right' values, leaving the 'Left' values unmodified. >>> xs = [Left "a", Left "b", Right "c", Right "d", Left "e", Left "f"] >>> fmap unAppendRight . groupAndConcat . fmap AppendRight $ xs [Left "a",Left "b",Right "cd",Left "e",Left "f"] -} newtype AppendRight a b = AppendRight { unAppendRight :: Either a b } deriving (Eq, Ord, Read, Show) instance PartialSemigroup b => PartialSemigroup (AppendRight a b) where AppendRight (Right x) <>? AppendRight (Right y) = AppendRight . Right <$> (x <>? y) _ <>? _ = Nothing -------------------------------------------------------------------------------- {- $refusing These are 'PartialSemigroup' instances that don't really combine their values at all; whenever more than one thing is present, '<>?' fails. -} {- | A partial semigroup operation which always fails. -} newtype One a = One { theOne :: a } deriving (Eq, Ord, Read, Show) instance PartialSemigroup (One a) where _ <>? _ = Nothing {- | A wrapper for 'Maybe' whose partial semigroup operation fails when two 'Just's are combined. -} newtype AtMostOne a = AtMostOne { theOneMaybe :: Maybe a } deriving (Eq, Ord, Read, Show) instance PartialSemigroup (AtMostOne a) where AtMostOne Nothing <>? x = Just x x <>? AtMostOne Nothing = Just x _ <>? _ = Nothing