ParetoFront.hs
{-# LANGUAGE DerivingVia, GeneralizedNewtypeDeriving, StandaloneDeriving #-}
module Math.ParetoFront (
Comparison(..),
Debatable(..),
Front,
Strata,
singleton,
stratum,
getStrata,
getFront,
quota,
nestedFold
) where
import Data.Foldable
import Data.List (partition, transpose)
import Data.Ord (Down(..))
import Data.Semigroup(Arg(..), Max(..), Min(..))
-- | The outcome of comparing two items by possibly multiple contradicting
-- criteria.
data Comparison =
-- | Where the second item is preferred by all used criteria
Dominated |
-- | Where no item is preferred by any used criterion
WeakTie |
-- | Where each item is preferred by at least one criterion
StrongTie |
-- | Where the first item is preferred by all used critera
Dominates
deriving(Ord,Eq,Show,Read)
instance Semigroup Comparison where
WeakTie <> b = b
a <> WeakTie = a
Dominates <> Dominates = Dominates
Dominated <> Dominated = Dominated
_ <> _ = StrongTie
instance Monoid Comparison where
mempty = WeakTie
-- | Items which can be compared by possibly multiple criteria contradicting
-- criteria
class Debatable a where
weigh :: a -> a -> Comparison
instance Ord a => Debatable (Min a) where
weigh (Min a) (Min b) = case compare a b of
LT -> Dominates
GT -> Dominated
EQ -> WeakTie
deriving via Min Int instance Debatable Int
deriving via Min Integer instance Debatable Integer
deriving via Min Double instance Debatable Double
deriving via Min Float instance Debatable Float
deriving via Min (Down a) instance Ord a => Debatable (Max a)
instance Debatable a => Debatable (Down a) where
weigh (Down a) (Down b) = case weigh a b of
Dominates -> Dominated
Dominated -> Dominates
r -> r
instance Debatable a => Debatable (Arg a b) where
weigh (Arg a _) (Arg b _) = weigh a b
instance (Debatable a, Debatable b) => Debatable (a,b) where
weigh ~(a1,a2) ~(b1,b2) = weigh a1 b1 <> weigh a2 b2
instance (Debatable a, Debatable b, Debatable c) => Debatable (a,b,c) where
weigh ~(a1,a2,a3) ~(b1,b2,b3) = weigh a1 b1 <> weigh a2 b2 <> weigh a3 b3
instance (Debatable a, Debatable b, Debatable c, Debatable d) =>
Debatable (a,b,c,d) where
weigh ~(a1,a2,a3,a4) ~(b1,b2,b3,b4) = weigh a1 b1 <>
weigh a2 b2 <>
weigh a3 b3 <>
weigh a4 b4
-- | A collection of items where no item is preferred by all criteria.
newtype Front a = Front [a] deriving (Foldable, Show)
-- | A series of 'Front's such that each subsequent 'Front' consists of
-- items for which some item in the previous front is preferable by all
-- criteria.
newtype Strata a = Strata [Front a] deriving (Show)
instance Foldable Strata where
foldMap f (Strata l) = foldMap (foldMap f) l
singleton :: a -> Front a
singleton a = Front [a]
stratum :: a -> Strata a
stratum a = Strata [singleton a]
fuse :: Debatable a => Front a -> Front a -> (Front a, Front a, Front a)
fuse (Front a) (Front b) = let
m = map (flip map b . weigh) a
m' = transpose m
s = map (not . any (== Dominated)) m
s' = map (not . any (== Dominates)) m'
(f1, t1) = partition snd $ zip a s
(f2, t2) = partition snd $ zip b s'
in (Front $ map fst (f1 ++ f2), Front $ map fst t1, Front $ map fst t2)
-- | Where two 'Front's are combined, all items are retained except those for
-- which at least one other item is preferred by all criteria.
instance Debatable a => Semigroup (Front a) where
a <> b = let
(r, _, _) = fuse a b
in r
instance Debatable a => Monoid (Front a) where
mempty = Front []
instance Debatable a => Semigroup (Strata a) where
a <> b = mconcat [a,b]
instance Debatable a => Monoid (Strata a) where
mempty = Strata []
mconcat = Strata . rebuild . transpose . map getStrata where
rebuild [] = []
rebuild ([] : r) = rebuild r
rebuild ([x] : r) = x : rebuild r
rebuild ((x : y : s) : r) = let
(f, p, q) = fuse x y
r' = push p $ push q r
in rebuild ((f: s): r')
push (Front []) r = r
push q [] = [[q]]
push q (s : r) = ((q:s) : r)
getFront :: Front a -> [a]
getFront (Front l) = l
getStrata :: Strata a -> [Front a]
getStrata (Strata l) = l
-- | Drop fronts after those accounting for the first n items.
quota :: Int -> Strata a -> Strata a
quota _ (Strata []) = Strata []
quota n (Strata (a:r))
| n > 0 = let
Strata r' = quota (n - length (getFront a)) (Strata r)
in Strata (a : r')
| otherwise = Strata []
-- | 'foldMap' each front separately with one function, then 'foldMap' the
-- results.
nestedFold :: (Monoid m, Monoid n) => (a -> m) -> (m -> n) -> Strata a -> n
nestedFold f g (Strata l) = foldMap (g . foldMap f) l