dimensions-1.0.1.1: src/Numeric/Dim.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Strict #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
-----------------------------------------------------------------------------
-- |
-- Module : Numeric.Dim
-- Copyright : (c) Artem Chirkin
-- License : BSD3
--
-- Maintainer : chirkin@arch.ethz.ch
--
-- This module is based on `GHC.TypeLits` and re-exports its functionality.
-- It provides `KnownDim` class that is similar to `KnownNat`, but keeps
-- `Word`s instead of `Integer`s;
-- Also it provides `Dim` data type serving as a singleton
-- suitable for recovering an instance of the `KnownDim` class.
-- A set of utility functions provide inference functionality, so
-- that `KnownDim` can be preserved over some type-level operations.
--
-----------------------------------------------------------------------------
module Numeric.Dim
( -- * Type level numbers that can be unknown.
XNat (..), XN, N, XNatType (..)
-- * Term level dimension
, Dim (Dim, D, Dn, Dx), SomeDim
, KnownDim (..), KnownXNatType (..)
, dimVal, dimVal', someDimVal
, sameDim, sameDim'
, compareDim, compareDim'
, constrain, constrainBy, relax
-- * Simple Dim arithmetics
--
-- The functions below create singleton values that work as a witness
-- of `KnownDim` instance for type-level Nat operations.
-- For example, to show that @(a + b)@ is a @KnownDim@, one writes:
--
-- > case plusDim dA dB of
-- > D -> ... -- here we know KnownDim ( a + b )
--
-- There is a bug and a feature in these functions though:
-- they are implemented in terms of @Num Word@, which means that
-- their results are subject to integer overflow.
-- The good side is the confidence that they behave exactly as
-- their @Word@ counterparts.
, plusDim, minusDim, minusDimM, timesDim, powerDim
-- * Re-export part of `GHC.TypeLits` for convenience
, TL.Nat, TL.CmpNat, type (TL.+), type (TL.-), type (TL.*), type (TL.^)
, MinDim, FixedDim, inferDimLE
-- * Inferring kind of type-level dimension
, KnownDimKind (..), DimKind (..)
) where
import Data.Type.Bool
import Data.Type.Equality
import GHC.Base (Type)
import GHC.Exts (Constraint, Proxy#, proxy#, unsafeCoerce#)
import GHC.TypeLits as TL
import Numeric.Type.Evidence
-- | Either known or unknown at compile-time natural number
data XNat = XN Nat | N Nat
-- | Unknown natural number, known to be not smaller than the given Nat
type XN (n::Nat) = 'XN n
-- | Known natural number
type N (n::Nat) = 'N n
-- | Find out whether @XNat@ is of known or constrained type.
data XNatType :: XNat -> Type where
-- | Given @XNat@ is known
Nt :: XNatType ('N n)
-- | Given @XNat@ is constrained unknown
XNt :: XNatType ('XN m)
-- | Same as `SomeNat`
type SomeDim = Dim ('XN 0)
-- | Singleton type to store type-level dimension value.
--
-- On the one hand, it can be used to let type-inference system know
-- relations between type-level naturals.
-- On the other hand, this is just a newtype wrapper on the @Word@ type.
--
-- Usually, the type parameter of @Dim@ is either @Nat@ or @XNat@.
-- If dimensionality of your data is known in advance, use @Nat@;
-- if you know the size of some dimensions, but do not know the size
-- of others, use @XNat@s to represent them.
newtype Dim (x :: k) = DimSing Word
-- Starting from GHC 8.2, compiler supports specifying lists of complete
-- pattern synonyms.
#if __GLASGOW_HASKELL__ >= 802
{-# COMPLETE D #-}
{-# COMPLETE Dn, Dx #-}
{-# COMPLETE Dim #-}
#endif
-- | Independently of the kind of type-level number,
-- construct an instance of `KnownDim` from it.
--
-- Match against this pattern to bring `KnownDim` instance into scope
-- when you don't know the kind of the @Dim@ parameter.
pattern Dim :: forall (n :: k) . () => KnownDim n => Dim n
pattern Dim <- (dimEv -> E)
where
Dim = dim @_ @n
-- | Same as @Dim@ pattern, but constrained to @Nat@ kind.
pattern D :: forall (n :: Nat) . () => KnownDim n => Dim n
pattern D <- (dimEv -> E)
where
D = dim @_ @n
-- | Statically known `XNat`
pattern Dn :: forall (xn :: XNat) . KnownXNatType xn
=> forall (n :: Nat) . (KnownDim n, xn ~ 'N n) => Dim n -> Dim xn
pattern Dn k <- (dimXNEv (xNatType @xn) -> PatN k)
where
Dn k = unsafeCoerce# k
-- | `XNat` that is unknown at compile time.
-- Same as `SomeNat`, but for a dimension:
-- Hide dimension size inside, but allow specifying its minimum possible value.
pattern Dx :: forall (xn :: XNat) . KnownXNatType xn
=> forall (n :: Nat) (m :: Nat)
. (KnownDim n, MinDim m n, xn ~ 'XN m) => Dim n -> Dim xn
pattern Dx k <- (dimXNEv (xNatType @xn) -> PatXN k)
where
Dx k = unsafeCoerce# k
-- | This class provides the `Dim` associated with a type-level natural.
class KnownDim (n :: k) where
-- | Get value of type-level dim at runtime.
--
-- Note, this function is supposed to be used with @TypeApplications@,
-- and the @KnownDim@ class has varying kind of the parameter;
-- thus, the function has two type paremeters (kind and type of @n@).
-- For example, you can type:
--
-- >>>:set -XTypeApplications
-- >>>:set -XDataKinds
-- >>>:t dim @Nat @3
-- dim @Nat @3 :: Dim 3
--
-- >>>:set -XTypeOperators
-- >>>:t dim @_ @(13 - 6)
-- dim @_ @(13 - 6) :: Dim 7
--
--
-- >>>:t dim @_ @(N 17)
-- dim @_ @(N 17) :: Dim (N 17)
--
dim :: Dim n
-- | Find out the type of `XNat` constructor
class KnownXNatType (n :: XNat) where
-- | Pattern-match against this to out the type of `XNat` constructor
xNatType :: XNatType n
instance KnownXNatType ('N n) where
xNatType = Nt
{-# INLINE xNatType #-}
instance KnownXNatType ('XN n) where
xNatType = XNt
{-# INLINE xNatType #-}
-- | Similar to `natVal` from `GHC.TypeLits`, but returns `Word`.
dimVal :: Dim (x :: k) -> Word
dimVal = unsafeCoerce#
{-# INLINE dimVal #-}
-- | Similar to `natVal` from `GHC.TypeLits`, but returns `Word`.
dimVal' :: forall n . KnownDim n => Word
dimVal' = unsafeCoerce# (dim @_ @n)
{-# INLINE dimVal' #-}
-- | Friendly error message if `m <= n` constraint is not satisfied.
-- Use this type family instead of @(<=)@ if possible
-- or try `inferDimLE` function as the last resort.
type family MinDim (m :: Nat) (n :: Nat) :: Constraint where
MinDim m n =
If (CmpNat m n == 'GT)
(TypeError
('Text "Minimum Dim size constraint ("
':<>: 'ShowType m
':<>: 'Text " <= "
':<>: 'ShowType n
':<>: 'Text ") is not satisfied."
':$$: 'Text "Minimum Dim: " ':<>: 'ShowType m
':$$: 'Text " Actual Dim: " ':<>: 'ShowType n
) :: Constraint
)
(m <= n)
-- | Constraints given by an XNat type on possible values of a Nat hidden inside.
type family FixedDim (x :: XNat) (n :: Nat) :: Constraint where
FixedDim ('N a) b = a ~ b
FixedDim ('XN m) b = MinDim m b
instance {-# OVERLAPPABLE #-} KnownNat n => KnownDim n where
{-# INLINE dim #-}
dim = DimSing (fromInteger (natVal' (proxy# :: Proxy# n)))
instance {-# OVERLAPPING #-} KnownDim 0 where
{ {-# INLINE dim #-}; dim = DimSing 0 }
instance {-# OVERLAPPING #-} KnownDim 1 where
{ {-# INLINE dim #-}; dim = DimSing 1 }
instance {-# OVERLAPPING #-} KnownDim 2 where
{ {-# INLINE dim #-}; dim = DimSing 2 }
instance {-# OVERLAPPING #-} KnownDim 3 where
{ {-# INLINE dim #-}; dim = DimSing 3 }
instance {-# OVERLAPPING #-} KnownDim 4 where
{ {-# INLINE dim #-}; dim = DimSing 4 }
instance {-# OVERLAPPING #-} KnownDim 5 where
{ {-# INLINE dim #-}; dim = DimSing 5 }
instance {-# OVERLAPPING #-} KnownDim 6 where
{ {-# INLINE dim #-}; dim = DimSing 6 }
instance {-# OVERLAPPING #-} KnownDim 7 where
{ {-# INLINE dim #-}; dim = DimSing 7 }
instance {-# OVERLAPPING #-} KnownDim 8 where
{ {-# INLINE dim #-}; dim = DimSing 8 }
instance {-# OVERLAPPING #-} KnownDim 9 where
{ {-# INLINE dim #-}; dim = DimSing 9 }
instance {-# OVERLAPPING #-} KnownDim 10 where
{ {-# INLINE dim #-}; dim = DimSing 10 }
instance {-# OVERLAPPING #-} KnownDim 11 where
{ {-# INLINE dim #-}; dim = DimSing 11 }
instance {-# OVERLAPPING #-} KnownDim 12 where
{ {-# INLINE dim #-}; dim = DimSing 12 }
instance {-# OVERLAPPING #-} KnownDim 13 where
{ {-# INLINE dim #-}; dim = DimSing 13 }
instance {-# OVERLAPPING #-} KnownDim 14 where
{ {-# INLINE dim #-}; dim = DimSing 14 }
instance {-# OVERLAPPING #-} KnownDim 15 where
{ {-# INLINE dim #-}; dim = DimSing 15 }
instance {-# OVERLAPPING #-} KnownDim 16 where
{ {-# INLINE dim #-}; dim = DimSing 16 }
instance {-# OVERLAPPING #-} KnownDim 17 where
{ {-# INLINE dim #-}; dim = DimSing 17 }
instance {-# OVERLAPPING #-} KnownDim 18 where
{ {-# INLINE dim #-}; dim = DimSing 18 }
instance {-# OVERLAPPING #-} KnownDim 19 where
{ {-# INLINE dim #-}; dim = DimSing 19 }
instance {-# OVERLAPPING #-} KnownDim 20 where
{ {-# INLINE dim #-}; dim = DimSing 20 }
instance KnownDim n => KnownDim ('N n) where
{-# INLINE dim #-}
dim = unsafeCoerce# (dim @Nat @n)
-- | Similar to `someNatVal` from `GHC.TypeLits`.
someDimVal :: Word -> SomeDim
someDimVal = unsafeCoerce#
{-# INLINE someDimVal #-}
-- | Change the minimum allowed size of a @Dim (XN x)@,
-- while testing if the value inside satisfies it.
constrain :: forall (m :: Nat) x . KnownDim m
=> Dim x -> Maybe (Dim (XN m))
constrain (DimSing x) | dimVal' @m > x = Nothing
| otherwise = Just (unsafeCoerce# x)
{-# INLINE constrain #-}
-- | `constrain` with explicitly-passed constraining @Dim@
-- to avoid @AllowAmbiguousTypes@.
constrainBy :: forall m x . Dim m -> Dim x -> Maybe (Dim (XN m))
constrainBy D = constrain @m
#if __GLASGOW_HASKELL__ < 802
constrainBy _ = error "Dim: Impossible pattern."
#endif
-- | Decrease minimum allowed size of a @Dim (XN x)@.
relax :: forall (m :: Nat) (n :: Nat) . (MinDim m n) => Dim (XN n) -> Dim (XN m)
relax = unsafeCoerce#
{-# INLINE relax #-}
-- | We either get evidence that this function
-- was instantiated with the same type-level numbers, or Nothing.
--
-- Note, this function works on @Nat@-indexed dimensions only,
-- because @Dim (XN x)@ does not have runtime evidence to infer @x@
-- and `KnownDim x` does not imply `KnownDim (XN x)`.
sameDim :: forall (x :: Nat) (y :: Nat)
. Dim x -> Dim y -> Maybe (Evidence (x ~ y))
sameDim (DimSing a) (DimSing b)
| a == b = Just (unsafeCoerce# (E @(x ~ x)))
| otherwise = Nothing
{-# INLINE sameDim #-}
-- | We either get evidence that this function
-- was instantiated with the same type-level numbers, or Nothing.
sameDim' :: forall (x :: Nat) (y :: Nat) p q
. (KnownDim x, KnownDim y)
=> p x -> q y -> Maybe (Evidence (x ~ y))
sameDim' _ _ = sameDim (dim @Nat @x) (dim @Nat @y)
{-# INLINE sameDim' #-}
-- | Ordering of dimension values.
compareDim :: Dim a -> Dim b -> Ordering
compareDim = unsafeCoerce# (compare :: Word -> Word -> Ordering)
{-# INLINE compareDim #-}
-- | Ordering of dimension values.
compareDim' :: forall a b p q
. (KnownDim a, KnownDim b) => p a -> q b -> Ordering
compareDim' _ _ = compareDim (dim @_ @a) (dim @_ @b)
{-# INLINE compareDim' #-}
instance Eq (Dim (n :: Nat)) where
_ == _ = True
{-# INLINE (==) #-}
instance Eq (Dim (x :: XNat)) where
DimSing a == DimSing b = a == b
{-# INLINE (==) #-}
instance Ord (Dim (n :: Nat)) where
compare _ _ = EQ
{-# INLINE compare #-}
instance Ord (Dim (x :: XNat)) where
compare = compareDim
{-# INLINE compare #-}
instance Show (Dim x) where
showsPrec p = showsPrec p . dimVal
{-# INLINE showsPrec #-}
instance KnownDim m => Read (Dim ('XN m)) where
readsPrec p xs = do (a,ys) <- readsPrec p xs
case constrain (someDimVal a) of
Nothing -> []
Just n -> [(n,ys)]
plusDim :: Dim n -> Dim m -> Dim (n + m)
plusDim (DimSing a) (DimSing b) = unsafeCoerce# (a + b)
{-# INLINE plusDim #-}
minusDim :: MinDim m n => Dim n -> Dim m -> Dim (n - m)
minusDim (DimSing a) (DimSing b) = unsafeCoerce# (a - b)
{-# INLINE minusDim #-}
minusDimM :: Dim n -> Dim m -> Maybe (Dim (n - m))
minusDimM (DimSing a) (DimSing b)
| a >= b = Just (unsafeCoerce# (a - b))
| otherwise = Nothing
{-# INLINE minusDimM #-}
timesDim :: Dim n -> Dim m -> Dim ((TL.*) n m)
timesDim (DimSing a) (DimSing b) = unsafeCoerce# (a * b)
{-# INLINE timesDim #-}
powerDim :: Dim n -> Dim m -> Dim ((TL.^) n m)
powerDim (DimSing a) (DimSing b) = unsafeCoerce# (a ^ b)
{-# INLINE powerDim #-}
-- | @MinDim@ implies @(<=)@, but this fact is not so clear to GHC.
-- This function assures the type system that the relation takes place.
inferDimLE :: forall m n . MinDim m n => Evidence (m <= n)
inferDimLE = unsafeCoerce# (E @(n <= n))
-- | GADT to support `KnownDimKind` type class.
-- Match against its constructors to know if @k@ is @Nat@ or @XNat@
data DimKind :: Type -> Type where
-- | Working on @Nat@.
DimNat :: DimKind Nat
-- | Working on @XNat@.
DimXNat :: DimKind XNat
-- | Figure out whether the type-level dimension is `Nat` or `XNat`.
-- Useful for generalized inference functions.
class KnownDimKind k where
dimKind :: DimKind k
instance KnownDimKind Nat where
dimKind = DimNat
instance KnownDimKind XNat where
dimKind = DimXNat
--------------------------------------------------------------------------------
-- | This function does GHC's magic to convert user-supplied `dim` function
-- to create an instance of `KnownDim` typeclass at runtime.
-- The trick is taken from Edward Kmett's reflection library explained
-- in https://www.schoolofhaskell.com/user/thoughtpolice/using-reflection
reifyDim :: forall r d . Dim d -> (KnownDim d => r) -> r
reifyDim d k = unsafeCoerce# (MagicDim k :: MagicDim d r) d
{-# INLINE reifyDim #-}
newtype MagicDim d r = MagicDim (KnownDim d => r)
dimEv :: Dim d -> Evidence (KnownDim d)
dimEv d = reifyDim d E
{-# INLINE dimEv #-}
data PatXDim (xn :: XNat) where
PatN :: KnownDim n => Dim n -> PatXDim ('N n)
PatXN :: (KnownDim n, MinDim m n) => Dim n -> PatXDim ('XN m)
dimXNEv :: forall (xn :: XNat) . XNatType xn -> Dim xn -> PatXDim xn
dimXNEv Nt (DimSing k) = reifyDim dd (PatN dd)
where
dd = DimSing @Nat @_ k
dimXNEv XNt xn@(DimSing k) = reifyDim dd (f dd xn)
where
dd = DimSing @Nat @_ k
f :: forall (d :: Nat) (m :: Nat)
. KnownDim d => Dim d -> Dim ('XN m) -> PatXDim ('XN m)
f d _ = case ( unsafeCoerce# (E @((CmpNat m m == 'GT) ~ 'False, m <= m))
:: Evidence ((CmpNat m d == 'GT) ~ 'False, m <= d)
) of
E -> PatXN d
{-# INLINE dimXNEv #-}