packages feed

valuations-0.0.1: src/Data/Valuation/PartialOrder.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall -Werror #-}

-- | A partial order on a type, wrapping @a -> a -> 'Maybe' 'Ordering'@.
--
-- This is the partial order analogue of 'Data.Functor.Contravariant.Comparison'
-- (which represents total orders via @a -> a -> 'Ordering'@).
-- The 'Nothing' case represents incomparable elements.
--
-- @
-- 'Just' 'LT'  — a < b
-- 'Just' 'EQ'  — a = b
-- 'Just' 'GT'  — a > b
-- 'Nothing'  — a and b are incomparable
-- @
module Data.Valuation.PartialOrder
  ( PartialOrder (..),

    -- * optics
    HasPartialOrder (..),
    AsPartialOrder (..),
    isBinaryFunctionT,

    -- * combinators
    semigroupPartialOrder,
    runPartialOrder,
    partialOrderLeq,
    totalOrder,
    fromLeq,
  )
where

import Control.Lens
  ( Iso,
    Lens',
    Prism',
    Rewrapped,
    Wrapped (..),
    from,
    iso,
    review,
    _Wrapped,
  )
import Data.Functor.Contravariant (Contravariant (..))
import Data.Functor.Contravariant.Conclude (Conclude (..))
import Data.Functor.Contravariant.Decide (Decide (..))
import Data.Functor.Contravariant.Divise (Divise (..))
import Data.Functor.Contravariant.Divisible (Decidable (..), Divisible (..))
import Data.Valuation.BinaryFunction
  ( AsBinaryFunctionT (..),
    BinaryFunctionT,
    HasBinaryFunctionT (..),
  )
import Data.Valuation.Semigroup
  ( Semigroup,
    applySemigroup,
    runSemigroup,
  )
import Data.Void (absurd)
import Prelude hiding (Semigroup)
import qualified Prelude

-- $setup
-- >>> :set -Wno-name-shadowing -Wno-type-defaults
-- >>> import Data.Void (Void)

-- |
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 1 2
-- Just LT
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 2 2
-- Just EQ
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 3 2
-- Just GT
--
-- >>> import qualified Data.Set as Set
-- >>> runPartialOrder (fromLeq Set.isSubsetOf) (Set.fromList [1,2]) (Set.fromList [2,3 :: Int])
-- Nothing
newtype PartialOrder a
  = PartialOrder (a -> a -> Maybe Ordering)

instance (PartialOrder a ~ t) => Rewrapped (PartialOrder a') t

instance Wrapped (PartialOrder a) where
  type Unwrapped (PartialOrder a) = a -> a -> Maybe Ordering
  _Wrapped' = iso (\(PartialOrder x) -> x) PartialOrder

-- | Classy lens for types that contain a 'PartialOrder'.
class HasPartialOrder c a | c -> a where
  partialOrder :: Lens' c (PartialOrder a)

instance HasPartialOrder (PartialOrder a) a where
  partialOrder = id

-- | Classy prism for types that can be constructed from a 'PartialOrder'.
class AsPartialOrder c a | c -> a where
  _PartialOrder :: Prism' c (PartialOrder a)

instance AsPartialOrder (PartialOrder a) a where
  _PartialOrder = id

instance HasBinaryFunctionT (PartialOrder a) Maybe a Ordering where
  binaryFunctionT = isBinaryFunctionT

instance AsBinaryFunctionT (PartialOrder a) Maybe a Ordering where
  _BinaryFunctionT = isBinaryFunctionT

isBinaryFunctionT :: Iso (PartialOrder a) (PartialOrder a') (BinaryFunctionT Maybe a Ordering) (BinaryFunctionT Maybe a' Ordering)
isBinaryFunctionT = _Wrapped . from _Wrapped

-- | Apply the partial order comparison.
--
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 1 2
-- Just LT
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 2 2
-- Just EQ
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 3 2
-- Just GT
runPartialOrder :: PartialOrder a -> a -> a -> Maybe Ordering
runPartialOrder (PartialOrder f) = f

-- | Test whether @a <= b@ in the partial order.
-- Returns 'True' iff the comparison yields 'Just' 'LT' or 'Just' 'EQ'.
-- Returns 'False' for incomparable elements.
--
-- >>> partialOrderLeq (totalOrder :: PartialOrder Int) 1 2
-- True
-- >>> partialOrderLeq (totalOrder :: PartialOrder Int) 2 2
-- True
-- >>> partialOrderLeq (totalOrder :: PartialOrder Int) 3 2
-- False
--
-- >>> import qualified Data.Set as Set
-- >>> partialOrderLeq (fromLeq Set.isSubsetOf) (Set.fromList [1,2]) (Set.fromList [2,3 :: Int])
-- False
partialOrderLeq :: PartialOrder a -> a -> a -> Bool
partialOrderLeq po a b = case runPartialOrder po a b of
  Just LT -> True
  Just EQ -> True
  _ -> False

-- | Construct a 'PartialOrder' from a total order ('Ord' instance).
-- The result never yields 'Nothing' since all elements are comparable.
--
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 1 2
-- Just LT
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 2 2
-- Just EQ
-- >>> runPartialOrder (totalOrder :: PartialOrder Int) 3 2
-- Just GT
totalOrder :: (Ord a) => PartialOrder a
totalOrder = PartialOrder (\a b -> Just (compare a b))

-- | Construct a 'PartialOrder' from a less-than-or-equal predicate.
--
-- The predicate should satisfy the partial order laws
-- (reflexive, antisymmetric, transitive).
-- Elements where neither @leq a b@ nor @leq b a@ holds are incomparable ('Nothing').
--
-- >>> import qualified Data.Set as Set
-- >>> let po = fromLeq Set.isSubsetOf :: PartialOrder (Set.Set Int)
-- >>> runPartialOrder po (Set.fromList [1]) (Set.fromList [1,2])
-- Just LT
-- >>> runPartialOrder po (Set.fromList [1,2]) (Set.fromList [1,2])
-- Just EQ
-- >>> runPartialOrder po (Set.fromList [1,2]) (Set.fromList [1])
-- Just GT
-- >>> runPartialOrder po (Set.fromList [1,2]) (Set.fromList [2,3])
-- Nothing
fromLeq :: (a -> a -> Bool) -> PartialOrder a
fromLeq leq = PartialOrder $ \a b ->
  case (leq a b, leq b a) of
    (True, True) -> Just EQ
    (True, False) -> Just LT
    (False, True) -> Just GT
    (False, False) -> Nothing

-- |
-- >>> import Data.Functor.Contravariant (contramap)
-- >>> runPartialOrder (contramap negate (totalOrder :: PartialOrder Int)) 1 2
-- Just GT
-- >>> runPartialOrder (contramap negate (totalOrder :: PartialOrder Int)) 2 1
-- Just LT
instance Contravariant PartialOrder where
  contramap f (PartialOrder g) = PartialOrder (\a b -> g (f a) (f b))

-- | Lexicographic composition as a first-class 'Semigroup': compare by the
-- first partial order; if equal ('Just' 'EQ'), compare by the second.
-- If the first yields 'Nothing' (incomparable), the result is 'Nothing'.
--
-- >>> let po = totalOrder :: PartialOrder Int
-- >>> runPartialOrder (runSemigroup semigroupPartialOrder po po) 1 2
-- Just LT
-- >>> runPartialOrder (runSemigroup semigroupPartialOrder po po) 2 2
-- Just EQ
semigroupPartialOrder :: Semigroup (PartialOrder a)
semigroupPartialOrder = review applySemigroup $ \(PartialOrder f) (PartialOrder g) -> PartialOrder $ \a b ->
  case f a b of
    Just EQ -> g a b
    r -> r

-- |
-- >>> let po = totalOrder :: PartialOrder Int
-- >>> runPartialOrder (po <> po) 1 2
-- Just LT
-- >>> runPartialOrder (po <> po) 2 2
-- Just EQ
instance Prelude.Semigroup (PartialOrder a) where
  (<>) = runSemigroup semigroupPartialOrder

-- | The trivial partial order where all elements are equal.
--
-- >>> runPartialOrder (mempty :: PartialOrder Int) 1 2
-- Just EQ
-- >>> runPartialOrder (mempty :: PartialOrder Int) 42 99
-- Just EQ
instance Monoid (PartialOrder a) where
  mempty = PartialOrder (\_ _ -> Just EQ)

-- | Lexicographic product: split @a@ into @(b, c)@, compare by @b@ first,
-- if equal then compare by @c@. @conquer@ treats all elements as equal.
--
-- >>> import Data.Functor.Contravariant.Divisible (divide, conquer)
-- >>> let po = divide id (totalOrder :: PartialOrder Int) (totalOrder :: PartialOrder Int)
-- >>> runPartialOrder po (1, 2) (1, 3)
-- Just LT
-- >>> runPartialOrder po (1, 2) (2, 1)
-- Just LT
-- >>> runPartialOrder po (1, 2) (1, 2)
-- Just EQ
--
-- >>> import Data.Functor.Contravariant.Divisible (conquer)
-- >>> runPartialOrder (conquer :: PartialOrder Int) 1 2
-- Just EQ
instance Divisible PartialOrder where
  conquer = mempty
  divide f pb pc = contramap (fst . f) pb <> contramap (snd . f) pc

-- | Disjoint sum: classify @a@ as 'Left' @b@ or 'Right' @c@.
-- Elements on the same side are compared by that side's order.
-- Elements on different sides are incomparable ('Nothing').
--
-- >>> import Data.Functor.Contravariant.Divisible (choose, lose)
-- >>> import Data.Void (Void, absurd)
-- >>> let po = choose id (totalOrder :: PartialOrder Int) (totalOrder :: PartialOrder String)
-- >>> runPartialOrder po (Left 1) (Left 2)
-- Just LT
-- >>> runPartialOrder po (Right "a") (Right "b")
-- Just LT
-- >>> runPartialOrder po (Left 1) (Right "a")
-- Nothing
-- >>> runPartialOrder po (Right "a") (Left 1)
-- Nothing
--
-- >>> import Data.Functor.Contravariant.Divisible (lose)
-- >>> import Data.Void (Void, absurd)
-- >>> let po = lose absurd :: PartialOrder Void
-- >>> seq po ()
-- ()
instance Decidable PartialOrder where
  lose f = PartialOrder (\a _ -> absurd (f a))
  choose f pb pc = PartialOrder $ \a1 a2 ->
    case (f a1, f a2) of
      (Left b1, Left b2) -> runPartialOrder pb b1 b2
      (Right c1, Right c2) -> runPartialOrder pc c1 c2
      _ -> Nothing

-- |
-- >>> import Data.Functor.Contravariant.Divise (divise)
-- >>> let po = divise id (totalOrder :: PartialOrder Int) (totalOrder :: PartialOrder Int)
-- >>> runPartialOrder po (1, 2) (1, 3)
-- Just LT
-- >>> runPartialOrder po (1, 2) (1, 2)
-- Just EQ
instance Divise PartialOrder where
  divise = divide

-- |
-- >>> import Data.Functor.Contravariant.Decide (decide)
-- >>> let po = decide id (totalOrder :: PartialOrder Int) (totalOrder :: PartialOrder String)
-- >>> runPartialOrder po (Left 1) (Left 2)
-- Just LT
-- >>> runPartialOrder po (Left 1) (Right "a")
-- Nothing
instance Decide PartialOrder where
  decide = choose

-- |
-- >>> import Data.Functor.Contravariant.Conclude (conclude)
-- >>> import Data.Void (absurd)
-- >>> let po = conclude absurd :: PartialOrder Void
-- >>> seq po ()
-- ()
instance Conclude PartialOrder where
  conclude = lose