nom-0.1.0.0: src/Language/Nominal/Equivar.hs
{-|
Module : Equivar
Description : Theory of equivariant orbit-finite structures
Copyright : (c) Murdoch J. Gabbay, 2020
License : GPL-3
Maintainer : murdoch.gabbay@gmail.com
Stability : experimental
Portability : POSIX
This module contains functions for manipulating lists of representatives which represent orbit-finite equivariant maps, and applying them to arguments by matching representatives to arguments up to a swapping (permutation) action.
-}
{-# LANGUAGE ConstraintKinds
, DataKinds
, DeriveGeneric
, DeriveAnyClass
, FlexibleContexts
, FlexibleInstances
, GADTs
, InstanceSigs
, MultiParamTypeClasses
, PartialTypeSignatures
, ScopedTypeVariables
, TypeOperators
#-}
module Language.Nominal.Equivar
(
-- * Equivariance
-- $intro
KEvFun (..)
, EvFun
-- * Equivariant lists and maps
-- $basiclists
, kevRep
, evRep
, kevNub
, evNub
-- * Lookup
-- $lookup
, kevLookup
, evLookup
, kevLookupList'
, kevLookupList
, evLookupList
-- * Orbit-finite equivariant maps
-- $orbitfinite
, KEvFinMap
, EvFinMap
, ($$)
, constEvFinMap
, extEvFinMap
, evFinMap
, fromEvFinMap
-- * Tests
-- $tests
) where
import qualified Data.Map.Strict as DM -- for unifyPerm
import Data.List as L
import Data.Proxy (Proxy (..))
import qualified Data.Set as Set (fromList,empty)
import GHC.Generics
import Type.Reflection
import Data.Type.Equality -- for testEquality
import Language.Nominal.Name
import Language.Nominal.NameSet
import Language.Nominal.Unify
-- For doctest:
-- $setup
-- >>> :m +Language.Nominal.Nom
-- >>> let [a, b, c] = exit $ freshAtoms [(), (), ()]
{- $intro
A structure is __equivariant__ when it has a trivial swapping action:
> swp _ _ a == a
/__Example:__ Every element of a @Nameless@ type is equivariant./
/__Example:__ The mapping @const True :: Atoms -> Bool@ is equivariant./
This second example is a particularly important example because it illustrates that "being equivariant" is /not/ the same as "being nameless". To be equivariant, you only need to be /symmetric/.
A structure is __equivariant orbit-finite__ when it can be represented by
* taking finitely many representatives and
* closing under the swapping action.
/__Example:__ the graph of the identity mapping @id :: Atom -> Atom@ can be represented by closing @[(a,a)]@ under all swappings. So @id@ is equivariant orbit-finite./
Two equivariant orbit-finite maps may be compared for equality even though they may be infinite as functions, because they are finite as representatives-up-to-orbits.
Opearationally for us here in Haskell, it suffices to inspect the representatives and compare them for unifiability using the functions from "Unify".
We will build:
* An equivariant function type 'KEvFun'.
* An equivariant orbit-finite space of maps 'KEvFinMap', and
* an equivariant application operation @$$@, for applying a 'KEvFinMap' (which under the hood is just a finite list of representatives) equivariantly to an argument.
__Warning:__ When applying a map equivariantly using '$$', It should hold that if @(m $$ a) == b@ then @supp b `isSubsetOf` supp a@. If not, then unexpected behaviour may result. The general mathematical reasons for this are discussed with examples e.g. in <http://dx.doi.org/10.4204/EPTCS.34.5 closed nominal rewriting> (see also <http://www.gabbay.org.uk/papers.html#clonre author's pdfs>). Thus in the terminology of that paper, we need @m@ to be closed. One way to guarantee this is to ensure that @b@ be @'Nameless'@, so that @b@ is a type like 'Bool' or 'Int' and @supp b@ is guaranteed empty. In practice for the cases we care about, @b@ will indeed always be @'Nameless'@.
-}
-- | @'KEvFun' s a b@ is just @a -> b@ in a wrapper.
-- But, we give this wrapped function trivial swapping and support. (For a usage example see the source code for 'Language.Nominal.Examples.IdealisedEUTxO.Val'.)
--
-- Functions need not be finite and are not equality types in general, so we cannot computably test that the actual function wrapped by the programmer actually /does/ have a trivial swapping action (i.e. really is equivariant).
--
-- It's the programmer's job to only insert truly equivariant functions here. Non-equivariant elements may lead to incorrect runtime behaviour.
--
-- On an equivariant orbit-finite type, the compiler can step in with more stringent guarantees. See e.g. 'KEvFinMap'.
newtype KEvFun s a b = EvFun { runEvFun :: a -> b -- ^ Function in a wrapper
}
instance (Typeable s, Swappable a, Swappable b) => Swappable (KEvFun s a b) where
kswp (a1 :: KAtom t) (a2 :: KAtom t) (EvFun f) =
case testEquality (typeRep :: TypeRep t) (typeRep :: TypeRep s) of
Just Refl -> EvFun f -- s and t are the same type
Nothing -> EvFun $ kswp a1 a2 f
instance (Typeable s, Swappable a, Swappable b) => KSupport s (KEvFun s a b) where
ksupp _ _ = Set.empty
-- | @'Tom'@-equivariant function-space
type EvFun = KEvFun Tom
-- * Equivariant lists and maps
{- $basiclists
Now for some basic functions that filter an input list down to a nub of representatives.
-}
-- | Filter a list down to one representative from each atoms-orbit (choosing first representative of each orbit).
kevRep :: KUnifyPerm s a => proxy s -> [a] -> [a]
kevRep = L.nubBy . kunifiablePerm
-- | Instance for @'Tom'@ atom type.
evRep :: UnifyPerm a => [a] -> [a]
evRep = kevRep atTom
-- | Restrict a Map to its equivariant nub, discarding all but one of each key-orbit
kevNub :: (Ord a, KUnifyPerm s a) => proxy s -> DM.Map a b -> DM.Map a b
kevNub p m = DM.restrictKeys m ((Set.fromList . kevRep p . DM.keys) m)
-- | Instance for unit atom type.
evNub :: (Ord a, UnifyPerm a) => DM.Map a b -> DM.Map a b
evNub = kevNub atTom
-- * Lookup
{- $lookup
So we have a finite set of representative pairs.
How do we unify a putative input with a representative to find a matching output?
-}
-- | Equivariantly apply a list of pairs, which we assume represents a map, to an element.
-- Also returns the source element.
kevLookupList' :: (KUnifyPerm s a, KUnifyPerm s b) => proxy s -> [(a, b)] -> a -> Maybe (a, b)
kevLookupList' _ [] _ = Nothing -- Empty list? Stop.
kevLookupList' p ((a, b) : t) a' = -- Nonempty list?
let r = kunifyPerm p a a' in -- fetch unifier, if exists
if isJustRen r then Just (a, ren r b) -- if it does, Just apply unifying renaming
else kevLookupList' p t a' -- otherwise, continue down list.
-- | Equivariantly apply a list of pairs (which we assume represents a map), to an element
kevLookupList :: (KUnifyPerm s a, KUnifyPerm s b) => proxy s -> [(a,b)] -> a -> Maybe b
kevLookupList p xs a = snd <$> kevLookupList' p xs a
-- | Equivariantly apply a list of pairs (which we assume represents a map), to an element. @'Tom'@ version.
evLookupList :: (UnifyPerm a, UnifyPerm b) => [(a,b)] -> a -> Maybe b
evLookupList = kevLookupList atTom
-- | Equivariantly apply a map @m@ to an element @a@.
-- Also returns a unifier.
kevLookup' :: (KUnifyPerm s a, KUnifyPerm s b) => proxy s -> DM.Map a b -> a -> Maybe (a, b)
kevLookup' p m = kevLookupList' p (DM.toList m)
-- | Equivariantly apply a map @m@ to an element @a@.
--
-- * If @a == ren r a'@ for some @r :: Ren@ and @m a' == b'@,
--
-- * then @kevLookup m a == ren r b'@.
--
-- For this to work, we require types @a@ and @b@ to support a @'ren'@ action, meaning they should support notions of unification up to permutation.
kevLookup :: (KUnifyPerm s a, KUnifyPerm s b) => proxy s -> DM.Map a b -> a -> Maybe b
kevLookup p m = fmap snd . kevLookup' p m
-- | @'kevLookup'@ instantiated to a @'Tom'@.
evLookup :: (UnifyPerm a, UnifyPerm b) => DM.Map a b -> a -> Maybe b
evLookup = kevLookup atTom
-- * Orbit-finite equivariant maps
{- $orbitfinite
@'KEvFinMap'@ is a type for /orbit-finite/ equivariant maps (contrast with @'KEvFun'@, a type for equivariant functions which need not be orbit-finite).
Values of @'KEvFinMap'@ must be constructed using
* @'constEvFinMap'@,
* @'extEvFinMap'@ and
* @'evFinMap'@.
Under the hood, @KEvFinMap s a b@ has just one constructor:
> DefAndRep b [(a, b)]
@DefAndRep@ stands for /Default and list of Representatives/.
We represent an orbit-finite equivariant map from @a@ to @b@ as
* A default value in @b@, and
* a list of key-value pairs, to be applied up to permuting @s@-sorted atoms in the keys.
@DefAndRep@ does not store the atoms it wants permuted, in its structure. It's just a pair of an element in @b@ and a list of pairs from @[(a, b)]@. At the point where we use @'$$'@ to equivariantly apply some @DefAndRep@ structure to some argument in @a@, we specify over which atoms we wish to be equivariant.
Thus: calling this @KEvFinMap@ is convenient but slighly misleading: the equivariance lies in the @'$$'@-/application/ of a @KEvFinMap@-wrapped collection of representatives, to an argument.
-}
-- | A type for orbit-finite equivariant maps.
data KEvFinMap s a b = DefAndRep (Nameless b) [(a, Nameless b)]
deriving (Show, Generic, Swappable)
-- | We insist @a@ and @b@ be @k@-swappable so that the mathematical notion of support (which is based on nominal sets) makes sense.
--
-- Operationally, we don't care: if see something of type @KEvFinMap s a b@, we return @Set.empty@.
instance (Typeable s, Swappable a, Swappable b) => KSupport s (KEvFinMap s a b) where
ksupp _ _ = Set.empty
-- | @'KEvFinMap'@ at a @'Tom'@. Thus, a type for orbit-finite @'Tom'@-equivariant maps.
type EvFinMap a b = KEvFinMap Tom a b
-- | Equivariant application of an orbit-finite map.
-- Given a map (expressed as finitely many representative pairs) and an argument ...
--
-- * it tries to rename atoms to match the argument to the first element of one of its pairs and
-- * if it finds a match, it maps to the second element of that pair, suitably renamed.
($$) :: forall s a b. (KUnifyPerm s a, Eq b) => KEvFinMap s a b -> a -> b
DefAndRep (Nameless b') xs $$ a = maybe b' getNameless $ kevLookupList (Proxy :: Proxy s) xs a
infixr 0 $$
-- | A constant equivariant map. We assume the codomain is @'Nameless'@.
--
-- >>> (constEvFinMap 42 :: EvFinMap Char Int) $$ 'x'
-- 42
--
-- >>> (constEvFinMap 0 :: EvFinMap Atom Int) $$ a
-- 0
constEvFinMap :: KUnifyPerm s a => b -> KEvFinMap s a b
constEvFinMap b = DefAndRep (Nameless b) []
-- | Extends a map with a new orbit, by specifying a representative @a@ maps to @b@.
-- We assume codomain @b@ is @'Nameless'@, as discussed above.
--
-- >>> (extEvFinMap 'x' 7 $ (constEvFinMap 42 :: EvFinMap Char Int)) $$ 'x'
-- 7
--
-- >>> (extEvFinMap 'x' 7 $ (constEvFinMap 42 :: EvFinMap Char Int)) $$ 'y'
-- 42
--
-- >>> let [m, n, o] = exit $ freshNames [(), (), ()]
-- >>> m == n
-- False
-- >>> unifiablePerm m n
-- True
-- >>> (extEvFinMap m 7 $ (constEvFinMap 42 :: EvFinMap (Name ()) Int)) $$ n
-- 7
-- >>> (extEvFinMap o 8 (extEvFinMap m 7 $ (constEvFinMap 42 :: EvFinMap (Name ()) Int))) $$ n
-- 8
extEvFinMap :: forall s a b. (KUnifyPerm s a, Eq a, Eq b) => a -> b -> KEvFinMap s a b -> KEvFinMap s a b
extEvFinMap a b f@(DefAndRep (Nameless b') xs) = case kevLookupList' (Proxy :: Proxy s) xs a of
Nothing
-- No mapping but sending a to the default value? Then noop.
| b == b' -> f
-- No mapping and not sending a to the default value? Add (a,b)
| otherwise -> DefAndRep (Nameless b') $ (a, Nameless b) : xs
Just (a'', Nameless b'')
-- (a,b) is already there, up to permuting atoms in a (b is nameless). Noop.
| b == b'' -> f
-- (a,b) is not already there up to permuting atoms in a. Remove (a'',b'') and replace with (a'',b). We really rely on b being nameless, here.
| otherwise -> DefAndRep (Nameless b') [(c, if c == a'' then Nameless b else d) | (c, d) <- xs]
-- | Constructs an orbit-finite equivariant map by prescribing a default value, and
-- finitely many argument-value pairs.
-- We assume the codomain is @'Nameless'@, as discussed above.
--
-- >>> let f = evFinMap 42 [('x', 7), ('y', 5), ('x', 13)] :: EvFinMap Char Int
-- >>> map (f $$) ['x', 'y', 'z']
-- [13,5,42]
--
-- >>> let atmEq = evFinMap False [((a,a), True)] :: EvFinMap (Atom, Atom) Bool
-- >>> map (atmEq $$) [(b,b), (c,c), (a,c), (b,c)]
-- [True,True,False,False]
--
-- >>> let g = evFinMap 2 [((a,a), 0), ((b,c), 1)] :: EvFinMap (Atom, Atom) Int
-- >>> map (g $$) [(b,b), (c,c), (a,c), (b,c)]
-- [0,0,1,1]
evFinMap :: (KUnifyPerm s a, Eq a, Eq b)
=> b -- ^ Default value.
-> [(a, b)] -- ^ List of exceptional argument-value pairs. In case of conflict, later pairs overwrite earlier pairs.
-> KEvFinMap s a b
evFinMap = L.foldl' (\m (a, b) -> extEvFinMap a b m) . constEvFinMap
-- | Extracts default value und list of exceptional argument-value pairs from an @'EvFinMap'@.
--
-- >>> fromEvFinMap $ (evFinMap 42 [('x', 7), ('y', 5), ('x', 13)] :: EvFinMap Char Int)
-- (42,[('y',5),('x',13)])
--
fromEvFinMap :: KEvFinMap s a b -> (b, [(a, b)])
fromEvFinMap (DefAndRep (Nameless b') xs) = (b', [(a, b) | (a, Nameless b) <- xs])
-- | 'KEvFinMap' is compared for equality by comparing the default value and the representatives, up to permutations.
--
-- __Edge case:__ If a codomain type is orbit-finite (e.g. @'Bool'@ and @'(Atom,Atom)'@, with two orbits, or @'Atom'@ with one), and representatives exhaust all possibilities, then the default value will never be queried, yet it will still be considered in our equality test.
instance (KUnifyPerm s a, Eq b) => Eq (KEvFinMap s a b) where
f1@(DefAndRep b1 xs1) == f2@(DefAndRep b2 xs2)
= (b1 == b2) -- default values equal?
&& all (\(a, Nameless b) -> (f2 $$ a) == b) xs1 -- check equality by representatives
&& all (\(a, Nameless b) -> (f1 $$ a) == b) xs2
-- TODO: replace with extensionality test
-- This looks like 'Nameless', but 'Nameless' cannot be parameterised over s.
instance (KUnifyPerm s a, KUnifyPerm s b, Eq b) => KUnifyPerm s (KEvFinMap s a b) where
kunifyPerm _ f g
| f == g = mempty
| otherwise = Ren Nothing
ren = const id
{- $tests Property-based tests are in "Language.Nominal.Properties.EquivarSpec". -}