strongweak-0.6.0: src/Strongweak/Strengthen.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
module Strongweak.Strengthen
(
-- * 'Strengthen' class
Strengthen(..)
, restrengthen
, Result
-- ** Helpers
, strengthenBounded
-- * Strengthen failures
, Fails
, Fail(..)
, prettyFail
-- ** Helpers
, fail1
, failOther
, failShow
, maybeFailShow
-- * Re-exports
, Strongweak.Weaken.Weak
) where
import Strongweak.Util.Typeable ( typeRep' )
import Strongweak.Util.Text ( tshow )
import Strongweak.Weaken ( Weaken(..) )
import Data.Either.Validation
import Data.Typeable ( Typeable, TypeRep )
import Prettyprinter qualified as Pretty
import Prettyprinter ( Pretty(pretty), (<+>) )
import Prettyprinter.Render.String qualified as Pretty
import Prettyprinter.Render.Text qualified as Pretty
import Data.Text ( Text )
import Data.Text.Lazy qualified as Text.Lazy
import GHC.TypeNats ( Natural, KnownNat )
import Data.Word
import Data.Int
import Refined hiding ( Weaken, weaken, strengthen, NonEmpty )
import Data.Vector.Generic.Sized qualified as VGS -- Shazbot!
import Data.Vector.Generic qualified as VG
import Data.Foldable qualified as Foldable
import Control.Applicative ( liftA2 )
import Data.Functor.Identity
import Data.Functor.Const
import Acc.NeAcc
import Data.List.NonEmpty qualified as NonEmpty
import Data.List.NonEmpty ( NonEmpty )
type Result = Validation Fails
type Fails = NeAcc Fail
{- | Attempt to strengthen some @'Weak' a@, asserting certain invariants.
We take 'Weaken' as a superclass in order to maintain strong/weak type pair
consistency. We choose this dependency direction because we treat the strong
type as the "canonical" one, so 'Weaken' is the more natural (and
straightforward) class to define. That does mean the instances for this class
are a little confusingly worded. Alas.
See "Strongweak" for class design notes and laws.
-}
class Weaken a => Strengthen a where
-- | Attempt to strengthen some @'Weak' a@ to its associated strong type
-- @a@.
strengthen :: Weak a -> Result a
-- | Weaken a strong value, then strengthen it again.
--
-- Potentially useful if you have previously used
-- 'Strongweak.Strengthen.Unsafe.unsafeStrengthen' and now wish to check the
-- invariants. For example:
--
-- >>> restrengthen $ unsafeStrengthen @(Vector 2 Natural) [0]
-- Failure ...
restrengthen
:: (Strengthen a, Weaken a)
=> a -> Result a
restrengthen = strengthen . weaken
-- | A failure encountered during strengthening.
data Fail
-- | A failure containing lots of detail. Use in concrete instances where you
-- already have the 'Show's and 'Typeable's needed.
= FailShow
TypeRep -- ^ weak type
TypeRep -- ^ strong type
(Maybe Text) -- ^ weak value
[Text] -- ^ failure description
-- | A failure. Use in abstract instances to avoid heavy contexts. (Remember
-- that generic strengtheners should wrap these nicely anyway!)
| FailOther
[Text] -- ^ failure description
-- | Some failures occurred when strengthening from one data type to another.
--
-- Field indices are from 0 in the respective constructor. Field names are
-- provided if are present in the type.
--
-- This is primarily intended to be used by generic strengtheners.
| FailField
String -- ^ weak datatype name
String -- ^ strong datatype name
String -- ^ weak constructor name
String -- ^ strong constructor name
Natural -- ^ weak field index
(Maybe String) -- ^ weak field name (if present)
Natural -- ^ strong field index
(Maybe String) -- ^ strong field name (if present)
Fails -- ^ failures
prettyFail :: Fail -> Text.Lazy.Text
prettyFail = Pretty.renderLazy . prettyLayoutFail
prettyLayoutFail :: Fail -> Pretty.SimpleDocStream ann
prettyLayoutFail = Pretty.layoutPretty Pretty.defaultLayoutOptions . pretty
fail1 :: Fail -> Result a
fail1 = Failure . pure
failOther :: [Text] -> Result a
failOther = fail1 . FailOther
buildFailShow
:: forall w s. (Typeable w, Typeable s)
=> Maybe Text -> [Text] -> Result s
buildFailShow mwv = fail1 . FailShow (typeRep' @w) (typeRep' @s) mwv
failShow'
:: forall s w. (Typeable w, Show w, Typeable s)
=> (w -> Text) -> w -> [Text] -> Result s
failShow' f w = buildFailShow @w @s (Just (f w))
failShow
:: forall s w. (Typeable w, Show w, Typeable s)
=> w -> [Text] -> Result s
failShow = failShow' tshow
-- | This reports the weak and strong type, so no need to include those in the
-- failure detail.
failShowNoVal :: forall w s. (Typeable w, Typeable s) => [Text] -> Result s
failShowNoVal = buildFailShow @w @s Nothing
instance Show Fail where
showsPrec _ = Pretty.renderShowS . prettyLayoutFail
-- TODO shorten value if over e.g. 50 chars. e.g. @[0,1,2,...,255] -> FAIL@
instance Pretty Fail where
pretty = \case
FailShow wt st mwv detail -> Pretty.vsep $
case mwv of
Nothing -> [typeDoc, detailDoc]
Just wv ->
let valueDoc = "value: "<+>pretty wv<+>"->"<+>"FAIL"
in [typeDoc, valueDoc, detailDoc]
where
typeDoc = "type: "<+>prettyTypeRep wt<+>"->"<+>prettyTypeRep st
detailDoc = case detail of
[] -> "<no detail>"
[detailLine] -> "detail:"<+>pretty detailLine
_ -> "detail:"<>Pretty.line<>prettyList detail
FailOther detail -> pretty detail
-- TODO should inspect meta, shorten if identical (currently only using
-- weak)
FailField dw _ds cw _cs iw fw _is _fs es ->
let sw = maybe (show iw) id fw
in Pretty.nest 0 $ pretty dw<>"."<>pretty cw<>"."<>pretty sw<>Pretty.line<>prettyList es
-- mutually recursive with its 'Pretty' instance. safe, but a bit confusing -
-- clean up
prettyList :: (Foldable f, Pretty a) => f a -> Pretty.Doc ann
prettyList = Pretty.vsep . map go . Foldable.toList
where go e = "-"<+>Pretty.indent 0 (pretty e)
-- | Succeed on 'Just', fail with given detail on 'Nothing'.
maybeFailShow
:: forall a. (Typeable (Weak a), Typeable a)
=> [Text] -> Maybe a -> Result a
maybeFailShow detail = \case
Just a -> Success a
Nothing -> failShowNoVal @(Weak a) detail
-- | Assert a predicate to refine a type.
instance (Predicate p a, Typeable a)
=> Strengthen (Refined p a) where
strengthen = refine .> \case
Left rex -> failShowNoVal @a
[ "refinement: "<>tshow (typeRep' @p)
, "failed with..."
, tshow (displayRefineException rex)
]
Right ra -> Success ra
-- | Assert a functor predicate to refine a type.
instance (Predicate1 p f, Typeable f, Typeable (a :: ak), Typeable ak)
=> Strengthen (Refined1 p f a) where
strengthen = refine1 .> \case
Left rex -> failShowNoVal @(f a)
[ "refinement: "<>tshow (typeRep' @p)
, "failed with..."
, tshow (displayRefineException rex)
]
Right ra -> Success ra
-- | Strengthen a plain list into a non-empty list by asserting non-emptiness.
instance Typeable a => Strengthen (NonEmpty a) where
strengthen = NonEmpty.nonEmpty .> maybeFailShow ["empty list"]
-- | Strengthen a plain list into a sized vector by asserting length.
instance
( VG.Vector v a, KnownNat n
, Typeable v, Typeable a
) => Strengthen (VGS.Vector v n a) where
strengthen = VGS.fromList .> maybeFailShow ["incorrect length"]
-- | Add wrapper.
instance Strengthen (Identity a) where
strengthen = pure <$> Identity
-- | Add wrapper.
instance Strengthen (Const a b) where
strengthen = pure <$> Const
{- TODO controversial. seems logical, but also kinda annoying.
instance (Show a, Typeable a) => Strengthen (Maybe a) where
strengthen = \case [a] -> pure $ Just a
[] -> pure Nothing
x -> strengthenFailBase x "list wasn't [a] or []"
-}
-- Strengthen 'Natural's into Haskell's bounded unsigned numeric types.
instance Strengthen Word8 where strengthen = strengthenBounded
instance Strengthen Word16 where strengthen = strengthenBounded
instance Strengthen Word32 where strengthen = strengthenBounded
instance Strengthen Word64 where strengthen = strengthenBounded
-- Strengthen 'Integer's into Haskell's bounded signed numeric types.
instance Strengthen Int8 where strengthen = strengthenBounded
instance Strengthen Int16 where strengthen = strengthenBounded
instance Strengthen Int32 where strengthen = strengthenBounded
instance Strengthen Int64 where strengthen = strengthenBounded
-- | Strengthen one numeric type into another.
--
-- @n@ must be "wider" than @m@.
strengthenBounded
:: forall m n
. ( Typeable n, Integral n, Show n
, Typeable m, Integral m, Show m, Bounded m
) => n -> Result m
strengthenBounded n
| n <= maxB && n >= minB = Success (fromIntegral n)
| otherwise = failShow n
[ "not well bounded, require: "
<>tshow minB<>" <= n <= "<>tshow maxB
]
where
maxB = fromIntegral @m @n maxBound
minB = fromIntegral @m @n minBound
--------------------------------------------------------------------------------
-- | Decomposer. Strengthen every element in a list.
instance Strengthen a => Strengthen [a] where
strengthen = traverse strengthen
-- | Decomposer. Strengthen both elements of a tuple.
instance (Strengthen a, Strengthen b) => Strengthen (a, b) where
strengthen (a, b) = liftA2 (,) (strengthen a) (strengthen b)
-- | Decomposer. Strengthen either side of an 'Either'.
instance (Strengthen a, Strengthen b) => Strengthen (Either a b) where
strengthen = \case Left a -> Left <$> strengthen a
Right b -> Right <$> strengthen b
--------------------------------------------------------------------------------
prettyTypeRep :: TypeRep -> Pretty.Doc a
prettyTypeRep = pretty . show
-- from flow
(.>) :: (a -> b) -> (b -> c) -> a -> c
f .> g = g . f