morley-1.9: src/Util/Peano.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
{-# OPTIONS_GHC -Wno-orphans #-}
{-# LANGUAGE UndecidableSuperClasses #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
-- | 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 Util.Peano
( -- * General
Peano
-- @gromak: I think we should not export Nat, but we want to
-- export `Z` and `S` and I do not know how to do it without
-- exporting `Nat`.
, Nat (Z, S)
, ToPeano
, FromPeano
, KnownPeano (..)
, SingNat (SZ, SS)
, peanoVal'
, peanoValSing
-- * Lists
, Length
, At
, Drop
, Take
-- * Morley-specific utils
, IsLongerThan
, LongerThan
, RequireLongerThan
, IsLongerOrSameLength
, LongerOrSameLength
, RequireLongerOrSameLength
-- * Length constraints 'Dict'ionaries
, requireLongerThan
, requireLongerOrSameLength
) where
import Data.Constraint (Dict(..))
import Data.Singletons (Sing, SingI(..))
import Data.Vinyl (Rec(..))
import Data.Vinyl.TypeLevel (Nat(..), RLength)
import GHC.TypeLits (ErrorMessage(..), TypeError)
import GHC.TypeNats (type (+), type (-))
import qualified GHC.TypeNats as GHC (Nat)
import Unsafe.Coerce (unsafeCoerce)
import Util.Type
----------------------------------------------------------------------------
-- 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
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
class KnownPeano (n :: Peano) where
peanoVal :: proxy n -> Natural
instance KnownPeano 'Z where
peanoVal _ = 0
instance KnownPeano a => KnownPeano ('S a) where
peanoVal _ = peanoVal' @a + 1
peanoVal' :: forall n. KnownPeano n => Natural
peanoVal' = peanoVal (Proxy @n)
-- | Get runtime value from singleton.
peanoValSing :: forall n. KnownPeano n => Sing n -> Natural
peanoValSing _ = peanoVal' @n
instance KnownPeano a => MockableConstraint (KnownPeano a) where
provideConstraintUnsafe = Dict
data SingNat (n :: Nat) where
SZ :: SingNat 'Z
SS :: (SingI n, KnownPeano n) => SingNat n -> SingNat ('S n)
deriving stock instance Show (SingNat (n :: Nat))
deriving stock instance Eq (SingNat (n :: Nat))
instance NFData (SingNat (n :: Nat)) where
rnf SZ = ()
rnf (SS n) = rnf n
type instance Sing = SingNat
instance SingI 'Z where
sing = SZ
instance (SingI n, KnownPeano n) => SingI ('S n) where
sing = SS (sing @n)
----------------------------------------------------------------------------
-- 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 try to access to 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
type family Take (n :: Peano) (s :: [k]) :: [k] where
Take 'Z _ = '[]
Take _ '[] = '[]
Take ('S n) (a ': s) = a ': Take n s
----------------------------------------------------------------------------
-- 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 =
If (IsLongerThan l a)
(() :: Constraint)
(TypeError
('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 =
If (IsLongerOrSameLength l a)
(() :: Constraint)
(TypeError
('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
instance MockableConstraint (RequireLongerOrSameLength l a) where
provideConstraintUnsafe = unsafeCoerce $ Dict @(RequireLongerOrSameLength '[] 'Z)
instance MockableConstraint (RequireLongerThan l a) where
provideConstraintUnsafe = unsafeCoerce $ Dict @(RequireLongerThan '[()] 'Z)
----------------------------------------------------------------------------
-- 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