morley-1.19.2: src/Morley/Util/Peano.hs
-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_GHC -Wno-unused-top-binds #-}
-- This is needed because `singletons-th` generates redundant constraints.
-- GHC may not throw the warning always, see #791.
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{-# LANGUAGE UndecidableSuperClasses #-}
{-# LANGUAGE DeriveLift #-}
-- | Type-nat utilities.
--
-- We take Peano numbers as base for operations because they make it
-- much easer to prove things to compiler. Their performance does not
-- seem to introduce a problem, because we use nats primarily along with
-- stack which is a linked list with similar performance characteristics.
--
-- Many of things we introduce here are covered in @type-natural@ package,
-- but unfortunatelly it does not work with GHC 8.6 at the moment of writing
-- this module. We use "Data.Vinyl" as source of Peano @Nat@ for now.
module Morley.Util.Peano
( -- * General
Peano
, pattern S
, pattern Z
, ToPeano
, FromPeano
, SingNat (SZ, SS)
, peanoSing
, peanoSing'
, withPeanoSingI
, withSomePeano
-- * Utility type synonyms
, IsoNatPeano
, SingIPeano
-- * Peano Arithmetic
, type (>)
, type (>=)
, peanoSingDecrement
, peanoSingAdd
, Decrement
, AddPeano
, SubPeano
, MinPeano
, MaxPeano
-- * Lists
, Length
, At
, Drop
, LazyTake
, Take
, Head
, Tail
-- * Morley-specific utils
, IsLongerThan
, LongerThan
, RequireLongerThan
, IsLongerOrSameLength
, LongerOrSameLength
, RequireLongerOrSameLength
-- * Length constraints 'Dict'ionaries
, requireLongerThan
, requireLongerOrSameLength
-- * Length constraints 'Dict'ionaries
, isGreaterThan
, isGreaterEqualThan
-- * Inductive proofs
, additivity
, associativity
, minIdempotency
, commutativity
, transitivity
-- * Helpers
, toNatural
, someSingNat
) where
import Data.Constraint (Dict(..), (\\))
import Data.Data (Data(..))
import Data.Singletons (Sing, SingI(..), SomeSing(..))
import Data.Type.Equality (type (:~:)(..))
import Data.Vinyl (Rec(..))
import Data.Vinyl.Core (Head, Tail)
import Data.Vinyl.TypeLevel (Nat(..), RLength)
import GHC.TypeLits (ErrorMessage(..), TypeError)
import GHC.TypeNats (type (+), type (-))
import GHC.TypeNats qualified as GHC
import Language.Haskell.TH.Syntax (Lift)
import Unsafe.Coerce (unsafeCoerce)
import Morley.Util.Sing (genSingletonsType)
import Morley.Util.StubbedProof
import Morley.Util.Type (FailUnless)
-- This is very obviously a false positive.
{-# ANN module ("HLint: ignore Use 'natVal' from Universum" :: Text) #-}
{-# ANN module ("HLint: ignore Use 'someNatVal' from Universum" :: Text) #-}
----------------------------------------------------------------------------
-- General
----------------------------------------------------------------------------
-- | A convenient alias.
--
-- We are going to use 'Peano' numbers for type-dependent logic and
-- normal 'Nat's in user API, need to distinguish them somehow.
type Peano = Nat
deriving stock instance Eq Nat
deriving stock instance Show Nat
deriving stock instance Generic Nat
deriving stock instance Data Nat
deriving anyclass instance NFData Nat
$(genSingletonsType ''Nat)
deriving stock instance Show (SingNat (n :: Nat))
deriving stock instance Eq (SingNat (n :: Nat))
deriving stock instance Lift (SingNat (n :: Nat))
instance NFData (SingNat (n :: Nat)) where
rnf SZ = ()
rnf (SS n) = rnf n
-- | A constraint asserting that GHC's @Nat@ @n@ and @Peano@ @p@ are the same (up to an
-- isomorphism)
type IsoNatPeano (n :: GHC.Nat) (p :: Peano) = (n ~ FromPeano p, ToPeano n ~ p)
-- | A synonym for @SingI (ToPeano n)@. Essentially requires that we can construct a 'Peano'
-- singleton for a given 'Nat'
type SingIPeano (n :: GHC.Nat) = SingI (ToPeano n)
type family ToPeano (n :: GHC.Nat) :: Peano where
ToPeano 0 = 'Z
ToPeano a = 'S (ToPeano (a - 1))
type family FromPeano (n :: Peano) :: GHC.Nat where
FromPeano 'Z = 0
FromPeano ('S a) = 1 + FromPeano a
-- | Get the peano singleton for a given type-level nat literal.
--
-- >>> peanoSing @2
-- SS (SS SZ)
peanoSing :: forall (n :: GHC.Nat). SingIPeano n => SingNat (ToPeano n)
peanoSing = sing @(ToPeano n)
-- | Same as 'peanoSing', but only requires 'KnownNat' instance.
--
-- Witnesses half the equivalence between @KnownNat n@ and @SingI (ToPeano n)@
peanoSing' :: forall (n :: GHC.Nat). KnownNat n => SingNat (ToPeano n)
peanoSing' = go (natVal @n Proxy)
where
go :: forall m. Natural -> SingNat m
go = \case
0 -> SZ \\ (unsafeCoerce Refl :: m :~: 'Z)
n -> SS (go @(Decrement m) (n - 1)) \\ (unsafeCoerce Refl :: m :~: 'S (Decrement m))
-- | Run a computation requiring @SingI (ToPeano n)@ in a context which only has
-- @KnownNat n@. Mostly useful when used with 'SomeNat'
withPeanoSingI :: forall (n :: GHC.Nat) r. KnownNat n => (SingIPeano n => r) -> r
withPeanoSingI act = act \\ go @(ToPeano n) (natVal @n Proxy)
where
go :: forall (m :: Peano). Natural -> Dict (SingI m)
go = \case
0 -> Dict \\ (unsafeCoerce Refl :: m :~: 'Z)
n -> Dict \\ go @(Decrement m) (n - 1) \\ (unsafeCoerce Refl :: m :~: 'S (Decrement m))
-- | Lift a given term-level 'Natural' to the type level for a given computation. The computation is
-- expected to accept a 'Proxy' for the sake of convenience: it's easier to get at the type-level
-- natural with @ScopedTypeVariables@ when pattern-matching on the proxy, e.g.
--
-- > (x :: Natural) `withSomePeano` \(_ :: Proxy n) -> doSomeNatComputation @n
withSomePeano :: Natural -> (forall n. (KnownNat n, SingIPeano n) => Proxy n -> r) -> r
withSomePeano n f = case someNatVal n of
SomeNat (pn :: Proxy n) -> withPeanoSingI @n $ f pn
----------------------------------------------------------------------------
-- Peano Arithmetic
----------------------------------------------------------------------------
type family Decrement (a :: Peano) :: Peano where
Decrement 'Z = TypeError ('Text "Expected n > 0")
Decrement ('S n) = n
-- | Utility to 'Decrement' a Peano 'Sing'leton.
--
-- Useful when dealing with the constraint
peanoSingDecrement :: Sing n -> Maybe (Sing (Decrement n))
peanoSingDecrement = \case
SZ -> Nothing
SS n -> pure n
-- | 'Peano' naturals comparisson
type family (>) (x :: Peano) (y :: Peano) :: Bool where
'Z > _ = 'False
'S _ > 'Z = 'True
'S x > 'S y = x > y
-- | 'Peano' naturals comparisson
type family (>=) (x :: Peano) (y :: Peano) :: Bool where
_ >= 'Z = 'True
'Z >= _ = 'False
('S x) >= ('S y) = x >= y
-- | 'Peano' naturals addition
type family AddPeano (n :: Peano) (m :: Peano) :: Peano where
AddPeano 'Z x = x
AddPeano ('S x) y = 'S (AddPeano x y)
-- | 'Peano' naturals subtraction
type family SubPeano (n :: Peano) (m :: Peano) :: Peano where
SubPeano 'Z ('S m) = TypeError ('Text "Subtracting " ':<>: 'ShowType (FromPeano ('S m))
':<>: 'Text " from zero")
SubPeano n 'Z = n
SubPeano ('S n) ('S m) = SubPeano n m
-- | Out of two 'Peano' naturals, return the smaller one
type family MinPeano (n :: Peano) (m :: Peano) :: Peano where
MinPeano _ 'Z = 'Z
MinPeano 'Z _ = 'Z
MinPeano ('S n) ('S m) = 'S (MinPeano n m)
-- | Out of two 'Peano' naturals, return the larger one
type family MaxPeano (n :: Peano) (m :: Peano) :: Peano where
MaxPeano n 'Z = n
MaxPeano 'Z m = m
MaxPeano ('S n) ('S m) = 'S (MaxPeano n m)
-- | Singleton addition
peanoSingAdd :: SingNat n -> SingNat m -> SingNat (AddPeano n m)
peanoSingAdd (SS n) (SS (m :: SingNat m')) = SS (SS (peanoSingAdd n m)) \\ associativity @m' n
peanoSingAdd SZ m = m
peanoSingAdd n SZ = n \\ commutativity n SZ
----------------------------------------------------------------------------
-- Lists
----------------------------------------------------------------------------
type family Length l :: Peano where
Length l = RLength l
type family At (n :: Peano) s where
At 'Z (x ': _) = x
At ('S n) (_ ': xs) = At n xs
At a '[] =
TypeError
('Text "You tried to access a non-existing element of the stack, n = " ':<>:
'ShowType (FromPeano a))
type family Drop (n :: Peano) (s :: [k]) :: [k] where
Drop 'Z s = s
Drop ('S _) '[] = '[]
Drop ('S n) (_ ': s) = Drop n s
-- | A type-level version of 'Data.List.take'. Note that in many cases,
-- using 'LazyTake' instead will improve type inference.
type family Take (n :: Peano) (s :: [k]) :: [k] where
Take 'Z _ = '[]
Take _ '[] = '[]
Take ('S n) (a ': s) = a ': Take n s
-- | A "lazier" version of 'Take'. @LazyTake n xs@ will always produce
-- a list of exactly @n@ elements. If @xs@ has less than @n@ elements, then
-- some of the elements of the result will be stuck. Similarly, if some tail
-- of @xs@ is stuck or ambiguous, then elements of the result past that point
-- will be stuck.
--
-- @
-- LazyTake ('ToPeano' 4) '[1,2,3,4,5] ~ '[1,2,3,4]
-- LazyTake (ToPeano 4) (1 ': 2 ': 3 ': 4 ': Any) ~ '[1,2,3,4]
-- LazyTake (ToPeano 4) '[1,2] ~
-- '[1, 2, Head '[], Head (Tail '[])]
-- @
--
-- @LazyTake@ is often better than @Take@ for driving type inference.
-- For example, given the constraint
--
-- @
-- xs ~ Take (ToPeano 3) xs ++ q ': Drop (ToPeano 4) xs
-- @
--
-- GHC can't infer anything about the shape of @xs@. However, the constraint
--
-- @
-- xs ~ LazyTake (ToPeano 3) xs ++ q ': Drop (ToPeano 4) xs
-- @
--
-- will allow GHC to infer
--
-- @
-- xs ~ x1 ': x2 ': x3 ': q ': Drop (ToPeano 4) xs
-- @
type LazyTake :: Peano -> [k] -> [k]
type family LazyTake n xs where
LazyTake 'Z _ = '[]
LazyTake ('S n) xs = Head xs ': LazyTake n (Tail xs)
----------------------------------------------------------------------------
-- Morley-specific utils
----------------------------------------------------------------------------
-- Note that we could define type families to return 'Constraint' instead
-- of defining standalone constraint in form `c ~ 'True`, but apparently
-- such constraint would be weaker, e. g. there is an example when with
-- current approach there is no warning, but if we change the approach
-- to return 'Constraint' from type family then GHC complains about
-- non-exhaustive patterns (@gromak).
-- Also we use these `Bool` type families in more than one place, we generate
-- two constraints: one gives more information to GHC and another one produces
-- better error messages on failure.
-- | Comparison of type-level naturals, as a function.
--
-- It is as lazy on the list argument as possible - there is no
-- need to know the whole list if the natural argument is small enough.
-- This property is important if we want to be able to extract reusable
-- parts of code which are aware only of relevant part of stack.
type family IsLongerThan (l :: [k]) (a :: Peano) :: Bool where
IsLongerThan (_ ': _) 'Z = 'True
IsLongerThan (_ ': xs) ('S a) = IsLongerThan xs a
IsLongerThan '[] _ = 'False
-- | Comparison of type-level naturals, as a constraint.
type LongerThan l a = IsLongerThan l a ~ 'True
-- | Similar to 'IsLongerThan', but returns 'True' when list length
-- equals to the passed number.
type family IsLongerOrSameLength (l :: [k]) (a :: Peano) :: Bool where
IsLongerOrSameLength _ 'Z = 'True
IsLongerOrSameLength (_ ': xs) ('S a) = IsLongerOrSameLength xs a
IsLongerOrSameLength '[] ('S _) = 'False
-- | 'IsLongerOrSameLength' in form of constraint that gives most
-- information to GHC.
type LongerOrSameLength l a = IsLongerOrSameLength l a ~ 'True
{- | Evaluates list length.
This type family is a best-effort attempt to display neat error messages
when list is known only partially.
For instance, when called on @Int ': Int ': s@, the result will be
@OfLengthWithTail 2 s@ - compare with result of simple 'Length' -
@1 + 1 + Length s@.
For concrete types this will be identical to calling @FromPeano (Length l)@.
-}
type family OfLengthWithTail (acc :: GHC.Nat) (l :: [k]) :: GHC.Nat where
OfLengthWithTail a '[] = a
OfLengthWithTail a (_ ': xs) = OfLengthWithTail (a + 1) xs
type LengthWithTail l = OfLengthWithTail 0 l
-- | Comparison of type-level naturals, raises human-readable compile error
-- when does not hold.
--
-- Here we use the same approach as for 'RequireLongerOrSameLength', this
-- type family is internal.
type family RequireLongerThan' (l :: [k]) (a :: Nat) :: Constraint where
RequireLongerThan' l a =
FailUnless
(IsLongerThan l a)
('Text "Stack element #" ':<>: 'ShowType (FromPeano a) ':<>:
'Text " is not accessible" ':$$:
'Text "Current stack has size of only " ':<>:
'ShowType (LengthWithTail l) ':<>:
'Text ":" ':$$: 'ShowType l
)
class (RequireLongerThan' l a, LongerThan l a) =>
RequireLongerThan (l :: [k]) (a :: Peano)
instance (RequireLongerThan' l a, LongerThan l a) =>
RequireLongerThan l a
-- | 'IsLongerOrSameLength' in form of constraint that produces
-- good error message. Should be used together with 'LongerThan'
-- because 'LongerThan' gives GHC more information.
-- We use it in combination, so that it gives enough information to
-- GHC and also producess good error messages.
type family RequireLongerOrSameLength' (l :: [k]) (a :: Peano) :: Constraint where
RequireLongerOrSameLength' l a =
FailUnless
(IsLongerOrSameLength l a)
('Text "Expected stack with length >= " ':<>: 'ShowType (FromPeano a) ':$$:
'Text "Current stack has size of only " ':<>:
'ShowType (LengthWithTail l) ':<>:
'Text ":" ':$$: 'ShowType l
)
-- | We can have
-- `RequireLongerOrSameLength = (RequireLongerOrSameLength' l a, LongerOrSameLength l a)`,
-- but apparently the printed error message can be caused by `LongerOrSameLength`
-- rather than `RequireLongerOrSameLength'`.
-- We do not know for sure how it all works, but we think that if we require constraint X before
-- Y (using multiple `=>`s) then X will always be evaluated first.
class (RequireLongerOrSameLength' l a, LongerOrSameLength l a) =>
RequireLongerOrSameLength (l :: [k]) (a :: Peano)
instance (RequireLongerOrSameLength' l a, LongerOrSameLength l a) =>
RequireLongerOrSameLength l a
----------------------------------------------------------------------------
-- Length constraints 'Dict'ionaries
----------------------------------------------------------------------------
requireLongerThan
:: Rec any stk
-> Sing n
-> Maybe (Dict (RequireLongerThan stk n))
requireLongerThan RNil _ = Nothing
requireLongerThan (_ :& _xs) SZ = Just Dict
requireLongerThan (_ :& xs) (SS n) = do
Dict <- requireLongerThan xs n
return Dict
requireLongerOrSameLength
:: Rec any stk
-> Sing n
-> Maybe (Dict (RequireLongerOrSameLength stk n))
requireLongerOrSameLength _ SZ = Just Dict
requireLongerOrSameLength RNil (SS _) = Nothing
requireLongerOrSameLength (_ :& xs) (SS n) = do
Dict <- requireLongerOrSameLength xs n
return Dict
----------------------------------------------------------------------------
-- Arith constraints 'Dict'ionaries
----------------------------------------------------------------------------
isGreaterThan
:: Sing a -> Sing b
-> Maybe (Dict ((a > b) ~ 'True))
isGreaterThan SZ _ = Nothing
isGreaterThan (SS _) SZ = pure Dict
isGreaterThan (SS a) (SS b) = isGreaterThan a b
isGreaterEqualThan
:: Sing a -> Sing b
-> Maybe (Dict ((a >= b) ~ 'True))
isGreaterEqualThan _ SZ = pure Dict
isGreaterEqualThan SZ _ = Nothing
isGreaterEqualThan (SS a) (SS b) = isGreaterEqualThan a b
----------------------------------------------------------------------------
-- Inductive proofs
----------------------------------------------------------------------------
-- | Proof that for naturals, @k + (m + 1) = n@ entails @n > k@
additivity :: forall k m n. AddPeano k ('S m) ~ n
=> SingNat k -> n > k :~: 'True
additivity k = stubProof $ case k of
SZ -> Refl
_ -> Refl \\ lemma k \\ lemma2 @_ @m k \\ associativity @m k
where
lemma2 :: forall k' m'. SingNat k' -> 'S (AddPeano k' m') > k' :~: 'True
lemma2 SZ = Refl
lemma2 (SS k') = Refl \\ lemma2 @_ @m' k'
lemma :: SingNat k' -> 'S k' > k' :~: 'True
lemma SZ = Refl
lemma (SS n') = Refl \\ lemma n'
-- | Proof that for naturals, @x + (y + 1) = (x + y) + 1@
associativity :: forall y x. SingNat x -> AddPeano x ('S y) :~: 'S (AddPeano x y)
associativity x = stubProof $ case x of
SZ -> Refl
SS x' -> Refl \\ associativity @y x'
-- | Proof that @x + y = y + x@
commutativity :: SingNat x -> SingNat y -> AddPeano x y :~: AddPeano y x
commutativity x y = stubProof $ case x of
SZ -> case y of
SZ -> Refl
SS y' -> Refl \\ commutativity x y'
SS (x' :: SingNat x') -> Refl \\ associativity @x' y \\ commutativity x' y
-- | Proof that for naturals, @min(n, n) = n@
minIdempotency :: SingNat n -> MinPeano n n :~: n
minIdempotency n = stubProof $ case n of
SZ -> Refl
SS n' -> Refl \\ minIdempotency n'
-- | Proof that for naturals, @x >= y > z@ implies @x > z@
transitivity :: (x >= y ~ 'True, y > z ~ 'True)
=> SingNat x -> SingNat y -> SingNat z -> x > z :~: 'True
transitivity x y z = stubProof $ case (x, y, z) of
(SS _, SS _, SZ) -> Refl
(SS x', SS y', SS z') -> Refl \\ transitivity x' y' z'
----------------------------------------------------------------------------
-- Helpers
----------------------------------------------------------------------------
toNatural :: Peano -> Natural
toNatural Z = 0
toNatural (S x) = 1 + toNatural x
someSingNat :: Natural -> SomeSing Peano
someSingNat 0 = SomeSing SZ
someSingNat n = case someSingNat (n - 1) of
SomeSing sn -> SomeSing (SS sn)