packages feed

primus-0.1.0.0: src/Primus/LRHist.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
-- need PolyKinds else could fail for callers using TP type families
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

{- |
Module      : Primus.LRHist
Description : like 'Either' but keeps history of all successes
Copyright   : (c) Grant Weyburne, 2022
License     : BSD-3

tracks one or more successes and optionally a failure

prefer the smart constructors to enforce correctness or use the apply methods
-}
module Primus.LRHist (
  -- * datatype
  LRHist (..),

  -- * smart constructors
  rhi,
  rh,
  lh,
  lhskip,

  -- * constructors with better type application order
  rhi',
  rh',
  lh',
  lhskip',

  -- * converters
  lhToEitherI,
  lhToEither,
  lhToEitherTuples,

  -- * function application
  lhBool,
  lhMaybe,
  lhMaybe',
  lhEither,
  lhEither',
  appLR,
  appLRS,
  appLRB,

  -- * traversals
  traverseLRHistB,
  traverseLRHist,

  -- * miscellaneous
  eitherToLH,
  maybeToLH,
  validateLRHist,
) where

import Data.Bifoldable
import Data.Bifunctor
import Data.Bitraversable
import Data.Bool
import Data.Kind
import qualified Data.List as L
import Data.Proxy
import Data.These
import qualified GHC.Read as GR
import Primus.AsMaybe
import Primus.Bool
import Primus.Error (programmError)
import qualified Primus.TypeLevel as TP
import qualified Text.ParserCombinators.ReadPrec as PC
import qualified Text.Read.Lex as TRL

{- | like 'Either' but keeps track of history of all successes
  if there is a failure it wraps the previous successes and stops adding data to 'LRHist'
 "e" the error type
 "as" is the typelevel list in reverse order that tracks all previous "a"s
 "a" is the latest success type
-}
type LRHist :: [Type] -> Type -> Type -> Type
data LRHist as e a where
  -- | wraps an existing error
  LhSkip ::
    LRHist as e a' ->
    LRHist (a' ': as) e a
  -- | wraps previous nested successes with an error
  Lh ::
    e ->
    LRHist as e a' ->
    LRHist (a' ': as) e a
  -- | initial success value
  Rhi ::
    a ->
    LRHist '[] e a
  -- | subsequent success
  Rh ::
    a ->
    LRHist as e a' ->
    LRHist (a' ': as) e a

deriving stock instance Functor (LRHist as e)
deriving stock instance Foldable (LRHist as e)
deriving stock instance Traversable (LRHist as e)
deriving stock instance (Show a, Show e, TP.ApplyConstraints '[Show] as) => Show (LRHist as e a)
deriving stock instance (TP.ApplyConstraints '[Eq, Ord] as, Eq e, Ord e, Ord a) => Ord (LRHist as e a)
deriving stock instance (TP.ApplyConstraints '[Eq] as, Eq e, Eq a) => Eq (LRHist as e a)
instance
  (Semigroup e, Monoid a) =>
  Monoid (LRHist '[] e a)
  where
  mempty = Rhi mempty
instance
  ( Monoid a
  , Monoid e
  , Monoid a'
  , TP.ApplyConstraints '[Semigroup, Monoid] as
  , Monoid (LRHist as e a')
  ) =>
  Monoid (LRHist (a' ': as) e a)
  where
  mempty = Rh mempty mempty

instance
  ( Semigroup a
  , Semigroup e
  , TP.ApplyConstraints '[Semigroup] as
  ) =>
  Semigroup (LRHist as e a)
  where
  x <> y = case (x, y) of
    (Rhi a, Rhi a') -> Rhi (a <> a')
    (Rh a ls, Rh a' ls') -> Rh (a <> a') (ls <> ls')
    (Lh e ls, Lh e' ls') -> Lh (e <> e') (ls <> ls')
    (LhSkip ls, LhSkip ls') -> LhSkip (ls <> ls')
    (z@LhSkip{}, _) -> z
    (_, z@LhSkip{}) -> z
    (z@Lh{}, _) -> z
    (_, z@Lh{}) -> z

-- | constructor for 'Rhi' with more convenient type application order
rhi' :: forall e a. a -> LRHist '[] e a
rhi' = Rhi

-- | constructor for 'Rh' with more convenient type application order
rh' :: forall e a a' as. a -> LRHist as e a' -> LRHist (a' : as) e a
rh' = Rh

-- | constructor for 'Lh' with more convenient type application order
lh' :: forall a e a' as. e -> LRHist as e a' -> LRHist (a' : as) e a
lh' = Lh

-- | constructor for 'LhSkip' with more convenient type application order
lhskip' :: forall a e a' as. LRHist as e a' -> LRHist (a' : as) e a
lhskip' = LhSkip

-- | smart constructor for 'Rhi'
rhi :: forall e a. a -> (Proxy 'True, LRHist '[] e a)
rhi a = (Proxy, Rhi a)

-- | smart constructor for 'Rh'
rh ::
  forall e a a' as proxy.
  a ->
  (proxy 'True, LRHist as e a') ->
  (proxy 'True, LRHist (a' : as) e a)
rh a = second (Rh a)

-- | smart constructor for 'Lh'
lh ::
  forall a e a' as proxy.
  e ->
  (proxy 'True, LRHist as e a') ->
  (Proxy 'False, LRHist (a' : as) e a)
lh e (_p, z) = (Proxy, Lh e z)

-- | smart constructor for 'LhSkip'
lhskip ::
  forall a e a' as proxy.
  (proxy 'False, LRHist as e a') ->
  (proxy 'False, LRHist (a' : as) e a)
lhskip = second LhSkip

-- | initialise 'LRHist' with an 'Either' by wrapping a unit
eitherToLH :: Either e a -> LRHist '[()] e a
eitherToLH lr = either Lh Rh lr (Rhi ())

-- | initialise 'LRHist' with an 'Maybe' by wrapping a unit
maybeToLH :: Monoid e => Maybe a -> LRHist '[()] e a
maybeToLH = eitherToLH . maybe (Left mempty) Right

-- | returns an inductive tuple on success and Either for failure
lhToEitherI ::
  forall e a as.
  RHistC as =>
  LRHist as e a ->
  Either e (RHistT a as)
lhToEitherI = rhist

-- | convert 'LRHist' to an 'Either'
lhToEither :: forall e a as. LRHist as e a -> Either e a
lhToEither = \case
  Rhi a -> Right a
  Rh a _ -> Right a
  Lh e _ -> Left e
  z@LhSkip{} -> Left $ go z
 where
  go :: forall as' a'. LRHist as' e a' -> e
  go = \case
    Rhi{} -> programmError "malformed LRHist: LhSkip expects inner LhSkip or Lh but found Rhi"
    Rh{} -> programmError "malformed LRHist: LhSkip expects LhSkip or Lh but found Rh"
    Lh e _ -> e
    LhSkip ls -> go ls

-- | extract the initial type for 'LRHist'
type OrgAT :: [Type] -> Type -> Type
type family OrgAT as a where
  OrgAT '[] a' = a'
  OrgAT (a ': as) _ = OrgAT as a

-- | extracts the initial value from 'LRHist'
type OrgAC :: [Type] -> Type -> Constraint
class OrgAC as a where
  orgA :: LRHist as e a -> OrgAT as a

instance OrgAC '[] a' where
  orgA = \case
    Rhi a -> a
instance OrgAC as a => OrgAC (a ': as) a' where
  orgA = \case
    Rh _ ls -> orgA ls
    Lh _ ls -> orgA ls
    LhSkip ls -> orgA ls

-- | returns flattened n-tuple with all the history of successes on success and Either for failure
lhToEitherTuples ::
  forall e a as tp.
  ( TP.ITupleC tp
  , RHistC as
  , TP.ToITupleT tp ~ RHistT a as
  ) =>
  LRHist as e a ->
  Either e tp
lhToEitherTuples = fmap TP.fromITupleC . rhist

-- | type family for creating an inductive tuple
type RHistT :: Type -> [Type] -> Type
type family RHistT a as where
  RHistT a '[] = (a, ())
  RHistT a (a' ': as) = (a, RHistT a' as)

-- | return an inductive tuple on success
type RHistC :: [Type] -> Constraint
class RHistC as where
  rhist :: LRHist as e a -> Either e (RHistT a as)

instance RHistC '[] where
  rhist = \case
    Rhi a -> Right (a, ())
instance RHistC as => RHistC (a ': as) where
  rhist = \case
    Rh a ls -> (a,) <$> rhist ls
    Lh e _ -> Left e
    LhSkip ls -> case rhist ls of
      Left e -> Left e
      Right _a -> programmError "malformed LRHist: LhSkip wrapping Rh or Rhi"

-- | validate that the composition of constructors for 'LRHist' is valid
validateLRHist :: forall e a as. LRHist as e a -> Either String ()
validateLRHist =
  \case
    Rhi{} -> Right ()
    Rh _ ls -> case ls of
      Lh{} -> Left "Rh cannot wrap Lh"
      LhSkip{} -> Left "Rh cannot wrap LhSkip"
      Rhi{} -> validateLRHist ls
      Rh{} -> validateLRHist ls
    Lh _ ls -> case ls of
      Lh{} -> Left "Lh cannot wrap Lh"
      LhSkip{} -> Left "Lh cannot wrap LhSkip"
      Rhi{} -> validateLRHist ls
      Rh{} -> validateLRHist ls
    LhSkip ls -> case ls of
      Lh{} -> validateLRHist ls
      LhSkip{} -> validateLRHist ls
      Rhi{} -> Left "LhSkip cannot wrap Rhi"
      Rh{} -> Left "LhSkip cannot wrap Rh"

-- | base case for 'LRHist' Read instance for '[] so only supports 'Rhi' constructor
instance
  (Read a, Read e) =>
  Read (LRHist '[] e a)
  where
  readPrec =
    GR.parens
      ( PC.prec
          10
          ( do
              GR.expectP (TRL.Ident "Rhi")
              a <- PC.step GR.readPrec
              return (Rhi a)
          )
      )

-- | successor case for 'LRHist' Read instance (a' ': as) so supports 'Rh', 'Lh', 'LhSkip' constructors
instance
  ( Read a
  , Read e
  , Read a'
  , Read (LRHist as e a')
  , TP.ApplyConstraints '[Read] as
  ) =>
  Read (LRHist (a' ': as) e a)
  where
  readPrec =
    GR.parens
      ( PC.prec
          10
          ( do
              GR.expectP (TRL.Ident "LhSkip")
              rst <- PC.step GR.readPrec
              return (LhSkip rst)
          )
          PC.+++ PC.prec
            10
            ( do
                GR.expectP (TRL.Ident "Lh")
                e <- PC.step GR.readPrec
                rst <- PC.step GR.readPrec
                return (Lh e rst)
            )
          PC.+++ PC.prec
            10
            ( do
                GR.expectP (TRL.Ident "Rh")
                a <- PC.step GR.readPrec
                rst <- PC.step GR.readPrec
                return (Rh a rst)
            )
      )

instance Bifunctor (LRHist as) where
  bimap f g = \case
    Rhi a -> Rhi (g a)
    Rh a ls -> Rh (g a) (first f ls)
    Lh e ls -> Lh (f e) (first f ls)
    LhSkip ls -> LhSkip (first f ls)

instance Bifoldable (LRHist as) where
  bifoldMap f g = \case
    Rhi a -> g a
    Rh a ls -> g a <> bifoldMap f (const mempty) ls
    Lh e ls -> f e <> bifoldMap f (const mempty) ls
    LhSkip ls -> bifoldMap f (const mempty) ls

instance Bitraversable (LRHist as) where
  bitraverse f g = \case
    Rhi a -> Rhi <$> g a
    Rh a ls -> Rh <$> g a <*> bitraverse f pure ls
    Lh e ls -> Lh <$> f e <*> bitraverse f pure ls
    LhSkip ls -> LhSkip <$> bitraverse f pure ls

-- | uses a boolean predicate to determine success or failure
lhBool ::
  forall e a a' as.
  (a ~ a', Monoid e) =>
  (a' -> Bool) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
lhBool f w =
  case w of
    Rhi a -> k a
    Rh a _ls -> k a
    Lh{} -> LhSkip w
    LhSkip{} -> LhSkip w
 where
  k a = bool (Lh mempty) (Rh a) (f a) w

-- | uses a maybe function to determine success or failure and also allow change of type "a"
lhMaybe ::
  forall e a a' as.
  Monoid e =>
  (a' -> Maybe a) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
lhMaybe f w =
  case w of
    Rhi a -> k a
    Rh a _ls -> k a
    Lh{} -> LhSkip w
    LhSkip{} -> LhSkip w
 where
  k a = maybe (Lh mempty) Rh (f a) w

-- | similar to 'lhMaybe' leveraging 'boolMaybe'
lhMaybe' ::
  forall e a a' as.
  Monoid e =>
  (a' -> Bool) ->
  (a' -> a) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
lhMaybe' p f w =
  case w of
    Rhi a -> k a
    Rh a _ls -> k a
    Lh{} -> LhSkip w
    LhSkip{} -> LhSkip w
 where
  k a = maybe (Lh mempty) Rh (boolMaybe p f a) w

-- | uses an either function to determine success or failure and also allow change of type "a"
lhEither ::
  forall e a a' as.
  (a' -> Either e a) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
lhEither f w =
  case w of
    Rhi a -> either Lh Rh (f a) w
    Rh a _ls -> either Lh Rh (f a) w
    Lh{} -> LhSkip w
    LhSkip{} -> LhSkip w

-- | similar to 'lhEither' leveraging 'boolEither'
lhEither' ::
  forall e a a' as.
  (a' -> Bool) ->
  (a' -> e) ->
  (a' -> a) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
lhEither' p l r w =
  case w of
    Rhi a -> k a
    Rh a _ls -> k a
    Lh{} -> LhSkip w
    LhSkip{} -> LhSkip w
 where
  k a = either Lh Rh (boolEither p l r a) w

-- | apply a function to 'LRHist' using 'ApTheseF'
appLR ::
  forall e a a' as x.
  (ApTheseF e a' x a) =>
  (a' -> x) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
appLR f w =
  case w of
    Rhi a -> k a
    Rh a _ls -> k a
    Lh{} -> LhSkip w
    LhSkip{} -> LhSkip w
 where
  k a =
    let th = apTheseF a (f a)
     in these Lh Rh (const Rh) th w

-- | similar to 'appLR' with state
appLRS ::
  forall e a' x a as z.
  (ApTheseF e a' x a) =>
  (z -> a' -> (z, x)) ->
  z ->
  LRHist as e a' ->
  (z, LRHist (a' ': as) e a)
appLRS f z w =
  case w of
    Rhi a -> k a
    Rh a _ls -> k a
    Lh{} -> (z, LhSkip w)
    LhSkip{} -> (z, LhSkip w)
 where
  k a =
    let (z1, th) = second (apTheseF a) (f z a)
     in (z1, these Lh Rh (const Rh) th w)

-- | apply a function to a 'LRHist' via 'boolEither'
appLRB ::
  forall e a a' as.
  (a' -> Bool) ->
  (a' -> e) ->
  (a' -> a) ->
  LRHist as e a' ->
  LRHist (a' ': as) e a
appLRB p l r = appLR (boolEither p l r)

-- | convenience method to apply 'appLR' to a container of 'LRHist' with state
traverseLRHist ::
  forall e a t a' as z.
  Traversable t =>
  (z -> a' -> (z, Either e a)) ->
  z ->
  t (LRHist as e a') ->
  (z, t (LRHist (a' ': as) e a))
traverseLRHist f = L.mapAccumL (appLRS f)

-- | convenience method to apply 'appLRB' to a container of 'LRHist'
traverseLRHistB ::
  forall e a t a' as.
  Functor t =>
  (a' -> Bool) ->
  (a' -> e) ->
  (a' -> a) ->
  t (LRHist as e a') ->
  t (LRHist (a' ': as) e a)
traverseLRHistB p l r = fmap (appLRB p l r)