nom-0.1.0.0: src/Language/Nominal/Binder.hs
{-|
Module : Binder
Description : Typeclass theory of binders
Copyright : (c) Murdoch J. Gabbay, 2020
License : GPL-3
Maintainer : murdoch.gabbay@gmail.com
Stability : experimental
Portability : POSIX
Typeclass theory of binders
-}
{-# LANGUAGE DataKinds
, DeriveFunctor
, DeriveGeneric
, DeriveTraversable
, DerivingVia
, FlexibleContexts
, FlexibleInstances
, FunctionalDependencies
, GeneralizedNewtypeDeriving
, InstanceSigs
, MultiParamTypeClasses
, PartialTypeSignatures
, PatternSynonyms
, ScopedTypeVariables
, StandaloneDeriving
, TypeApplications
, TypeOperators
, ViewPatterns
, UndecidableInstances
#-}
{-# OPTIONS_GHC -fno-warn-orphans #-} -- Suppress orphan instance of Data instance of Binder ctxbody.
module Language.Nominal.Binder
( -- * The @'Binder'@ typeclass
-- $binder
Binder (..), (@@!), (@@.), pattern (:@@!), nomApp, nomAppC, genApp, genAppC, resApp, resAppC, resAppC'
-- * @'new'@ quantifier
, newA, new, freshForA, freshFor
-- * Detecting trivial @'KNom'@s, that bind no atoms in their argument
, isTrivialNomBySupp, isTrivialNomByEq
-- * @'Nom'@ and @'supp'@
, kfreshen, freshen, BinderSupp (..)
-- * Binder isomorphism
-- $binderiso
, kbinderToNom, knomToBinder, knomToMaybeBinder, binderToNom, nomToBinder, nomToMaybeBinder
-- * Binder with concretion
, BinderConc (..)
-- * Tests
-- $tests
)
where
import Data.Data
import Data.Maybe
import Data.Set as S (toList) -- , Set) -- , union)
import GHC.Generics hiding (Prefix) -- avoid clash with Data.Data.Prefix
import Language.Nominal.Utilities
import Language.Nominal.NameSet
import Language.Nominal.Name
import Language.Nominal.Nom
-- * Binder
{- $binder
'Binder' is a typeclass for creating and unpacking local bindings.
The current development contains three instances of 'Binder':
* 'KNom' itself: the primitive binding construct of this package.
* 'Language.Nominal.Abs.KAbs': abstraction by a single name.
* 'Language.Nominal.Examples.IdealisedEUTxO.Chunk': a (generalisation of) EUTxO-style blockchains (the binding is in essence the names of edges connecting input-output pairs).
The simplest way to make a binder is to wrap a 'KNom' in further structure:
* 'Language.Nominal.Examples.IdealisedEUTxO.Chunk' has this design.
Its internal representation is structurally just a 'KNom' (what makes it interesting is that it is wrapped in some smart contructors that validate nontrivial extra structure, involving binding and more).
However, it is possible to design a binder without explicitly mentioning 'KNom' in its underlying representation:
* 'Language.Nominal.Abs.KAbs' is an instance. For technical reasons its underlying representation uses functions, although a bijection does exist with a 'KNom'-based structure (see 'Language.Nominal.Abs.absToNom' and 'Language.Nominal.Abs.nomToAbs').
What makes 'Binder' interesting is a rich array of destructors (e.g. '@@', '@@!', '@@.', 'nomApp', 'nomAppC', 'genApp', 'genAppC', 'resApp', 'resAppC', 'resAppC'').
These correspond to common patterns of unpacking a binder and mapping to other types, and provide a consistent interface across different binding constructs.
-}
-- | A typeclass for unpacking local binding.
--
-- Provides a function to unpack a binder, giving access to its locally bound names. Examples of binders include 'KNom', 'Language.Nominal.Abs.KAbs', and 'Language.Nominal.Examples.IdealisedEUTxO.Chunk'.
--
-- A 'Binder' comes with a rich array of destructors, like '@@', '@@!', '@@.', 'nomApp', 'nomAppC', 'genApp', 'genAppC', 'resApp', 'resAppC', 'resAppC''.
-- These correspond to common patterns of unpacking a binder and mapping to other types.
--
-- Below:
--
-- * @ctxbody@ is the type of the datum in its name-binding context, which consists intuitively of data wrapped in a local name binding. (Canonical example: 'KNom'.)
-- * @ctx@ is the type of a concrete name-carrying representation of the local binding (atom, names, list of atoms, etc). (Canonical examples: 'KAtom' and @['KAtom']@.)
-- * @body@ is the type of the body of the data.
-- * @s@ is the type of atoms output. Output will be wrapped in @'KNom' s@.
--
-- For example the 'Language.Nominal.Abs.KAbs' instance of 'Binder' has type @Binder (KAbs (KName s t) a) (KName s t) s a@; and this means that
--
-- * a datum in @ctxbody = KAbs (KName s t) a@ can be unpacked as
-- * something in @ctx = KName s t@ and something in @body = a@, and
-- * mapped by a function of type @KName s t -> a -> b@ using '@@',
-- * to obtain something in @KNom s b@.
--
-- The other destructors ('@@!', '@@.', 'nomApp', 'nomAppC', 'genApp', 'genAppC', 'resApp', 'resAppC', 'resAppC'') are variations on this basic pattern which the wider development shows are empirically useful.
--
-- See also 'kbinderToNom' and 'nomToMaybeBinder'.
class (Typeable s, Swappable ctx, Swappable body, Swappable ctxbody) => Binder ctxbody ctx body s | ctxbody -> ctx body s where
-- | A destructor for the binder (required)
(@@) :: ctxbody -- ^ the data in its local binding
-> (ctx -> body -> b) -- ^ function that inputs an explicit name context @ctx@ and a @body@ for that context, and outputs a @b@.
-> KNom s b -- ^ Result is a @b@ in a binding context.
(@>) :: ctx -> body -> ctxbody
-- ^ A constructor for the binder (optional, since e.g. your instance may impose additional well-formedness constraints on the input). Call this @res@.
(@>) = fromJust .: resMay
-- | A partial constructor for the binder, if preferred.
resMay :: ctx -> body -> Maybe ctxbody
resMay = Just .: (@>)
{-# MINIMAL (@@) #-} -- Please at least provide the destructor (@@). The constructor is optional, since e.g. your instance may impose additional well-formedness constraints on the input.
infixr 9 @@
infixr 1 @>
-- documentation doesn't appear above for (@>) unless I use -- ^
-- | Use this to map a binder to a type @b@, generating fresh atoms for any local bindings, and allowing them to escape.
(@@!) :: Binder ctxbody ctx body s => ctxbody -> (ctx -> body -> b) -> b
(@@!) = exit .: (@@)
infixr 9 @@!
-- | This pattern is not quite as useful as it could be, because it's too polymorphic to use a COMPLETE pragma on. You can use it, but you may get incomplete pattern match errors.
pattern (:@@!) :: Binder ctxbody ctx body s => ctx -> body -> ctxbody
pattern ctx :@@! body <- ((@@! (,)) -> (ctx, body))
where (:@@!) = (@>)
-- {-# COMPLETE (:@@!) #-} -- States this match is complete.
infixr 0 :@@!
-- | Acts on a 'KNom' binder by applying a function to the body and the context of locally bound names. Local names are preserved.
instance (Typeable s, Swappable a) => Binder (KNom s a) [KAtom s] a s where
(@@) :: KNom s a -> ([KAtom s] -> a -> b) -> KNom s b
(@@) = pami . reRes -- the @reRes@ ensures capture-avoidance, in case the input binding was formed using 'enter' instead of 'res'.
-- (@@) a' f = imap f $ withExit a' res -- flip imap
(@>) :: (Typeable s, Swappable a) => [KAtom s] -> a -> KNom s a
(@>) = res
-- | Use this to map a binder to a type @b@ with its own notion of restriction. Bindings do not escape.
(@@.) :: (Binder ctxbody ctx body s, KRestrict s b) => ctxbody -> (ctx -> body -> b) -> b
(@@.) x' f = unNom $ x' @@ f
infixr 9 @@.
-- | Unpacks a binder and maps into a @'KNom'@ environment.
--
-- 'nomApp' = 'flip' '(@@)'
nomApp :: Binder ctxbody ctx body s => (ctx -> body -> b) -> ctxbody -> KNom s b
nomApp = flip (@@)
-- | Unpacks a binder and maps into a @'KNom'@ environment.
-- Local bindings are not examined, and get carried over to the @'KNom'@.
--
-- 'nomAppC' = 'nomApp' . 'const'
nomAppC :: Binder ctxbody ctx body s => (body -> b) -> ctxbody -> KNom s b
nomAppC = nomApp . const
-- | Unpacks a binder. New names are generated and released.
--
-- 'genApp' = 'flip' '(@@!)'
genApp :: Binder ctxbody ctx body s => (ctx -> body -> b) -> ctxbody -> b
genApp = flip (@@!)
-- | Unpacks a binder. New names are generated and released.
-- Local bindings are not examined.
--
-- 'genAppC' = 'genApp' . 'const'
genAppC :: Binder ctxbody ctx body s => (body -> b) -> ctxbody -> b
genAppC = genApp . const
-- | Unpacks a binder and maps to a type with its own @'restrict'@ operation.
--
-- 'resApp' = 'flip' '(@@.)'
resApp :: (Binder ctxbody ctx body s, KRestrict s b) => (ctx -> body -> b) -> ctxbody -> b
resApp = flip (@@.)
-- | Unpacks a binder and maps to a type with its own @'restrict'@ operation.
-- Local bindings are not examined, and get carried over and @'restrict'@ed in the target type.
--
-- A common type instance is @(a -> Bool) -> KNom s a -> Bool@, in which case 'resAppC' acts to capture-avoidingly apply a predicate under a binder, and return the relevant truth-value. See for example the code for 'transposeNomFunc'.
--
-- 'resAppC' = 'resApp' . 'const'
resAppC :: (Binder ctxbody ctx body s, KRestrict s b) => (body -> b) -> ctxbody -> b
resAppC = resApp . const
-- | Unpacks a binder and maps to a type with its own @'restrict'@ operation. 'Flip'ped version, for convenience.
--
-- A common type instance is @KNom s a -> (a -> Bool) -> Bool @, in which case 'resAppC'' acts to apply a predicate inside a binder and return the relevant truth-value. See for example the code for 'transposeNomMaybe'.
--
-- 'resAppC'' = 'flip' 'resApp'
resAppC' :: (Binder ctxbody ctx body s, KRestrict s b) => ctxbody -> (body -> b) -> b
resAppC' = flip resAppC
-- * Variants on 'res'
{-- | A version of '@>' (@res@) that accepts a proxy to explicitly specify the type of atoms.
kres :: (Typeable s, Swappable body, Binder ctxbody ctx body s) => proxy s -> ctx -> body -> ctxbody
kres _ = (@>)
--}
-- * New quantifier
-- | Evaluate predicate on fresh atom.
newA :: KRestrict s b => (KAtom s -> b) -> b
newA = resAppC' freshKAtom
-- | Evaluate predicate on fresh name with label @t@.
new :: (KRestrict s b, Swappable t) => t -> (KName s t -> b) -> b
new = resAppC' . freshKName
-- | @n@ is @'freshFor'@ @a@ when @swp n' n a == a@ for a @'new'@ @n'@. Straight out of the paper (<https://link.springer.com/article/10.1007/s001650200016 publisher> and <http://www.gabbay.org.uk/papers.html#newaas-jv author's> pdfs).
--
-- /Note: In practice we mostly need this for names. This version is for atoms.
freshForA :: (Typeable s, Swappable a, Eq a) => KAtom s -> a -> Bool
freshForA n a = newA $ \n' -> kswp n' n a == a
-- | @n@ is @'freshFor'@ @a@ when @swp n' n a == a@ for a @'new'@ @n'@. Straight out of the paper (<https://link.springer.com/article/10.1007/s001650200016 publisher> and <http://www.gabbay.org.uk/papers.html#newaas-jv author's> pdfs).
freshFor :: (Typeable s, Swappable t, Swappable a, Eq a) => KName s t -> a -> Bool
freshFor = freshForA . nameAtom
-- * Detecting trivial @'KNom'@s, that bind no atoms in their argument
-- | Is the @'Nom'@ binding trivial?
--
-- This is the @'KSupport'@ version, for highly structured data that we can traverse (think: lists, tuples, sums, and maps).
--
-- See also @'isTrivialNomByEq'@, for less structured data when we have equality @'Eq'@ and swapping 'KSwappable'.
isTrivialNomBySupp :: forall s a. KSupport s a => KNom s a -> Bool
isTrivialNomBySupp = resApp $ kapart (Proxy :: Proxy s)
-- | Is the @'Nom'@ binding trivial?
--
-- This is the @'Eq'@ version, using @'freshFor'@.
-- Use this when we /do/ have equality and swapping, but /can't/ (or don't want to) traverse the type (think: sets).
--
-- See also @'isTrivialNomBySupp'@, for when we have @'KSupport'@.
isTrivialNomByEq :: (Typeable s, Swappable a, Eq a) => KNom s a -> Bool
isTrivialNomByEq = resApp $ \atms a -> and $ (`freshForA` a) <$> atms
-- * @'KNom'@ and @'KSupport'@
-- | Freshen all @s@-atoms, if we have @'KSupport'@. Returns result inside @'KNom'@ binding to store the freshly-generated atoms.
kfreshen :: KSupport s a => proxy s -> a -> KNom s a
kfreshen p a = S.toList (ksupp p a) @> a
-- | Freshen all atoms, if we have @'Support'@.
freshen :: Support a => a -> Nom a
freshen = kfreshen atTom
-- | Wrapper for generic method of deriving support of binder
newtype BinderSupp a = BinderSupp {getBinderSupp :: a}
deriving (Generic, Swappable)
instance (Binder ctxbody ctx body s, KSupport s ctx, KSupport s body) => KSupport s (BinderSupp ctxbody) where
ksupp p (BinderSupp ctxbody) = ctxbody @@. \ctx body -> ksupp p (ctx, body) -- Use of '(@@.)' here cleans out bound atoms.
{-- | Standard method to calculate the support of a binder
ksuppBinder :: (Binder ctxbody ctx body s, KSupport s ctx, KSupport s body) => proxy s -> ctxbody -> S.Set (KAtom s)
ksuppBinder = resAppC . ksupp -- Use of 'resAppC' here cleans out the (atom of the) abstracted name.
-- | @supp (ns @> x) == supp x \\ ns@
instance KSupport s a => KSupport s (KNom s a) where
ksupp = ksuppBinder
--}
-- * Binder isomorphism
{- $binderiso
The 'Binder' class has two functions pointing in opposite directions.
This suggests a standard isomorphism. We expect:
> binderToNom <$> nomToMaybeBinder x == Just x
> nomToBinder . binderToNom == id
For 'KNom' and 'KAbs' it's safe to use 'knomToBinder'. For others, like 'Language.Nominal.Examples.IdealisedEUTxO.Chunk', you would need 'knomToMaybeBinder', because additional well-formedness conditions are imposed on the constructor.
-}
kbinderToNom :: Binder ctxbody ctx body s => proxy ctxbody -> ctxbody -> KNom s (ctx, body)
kbinderToNom _ = nomApp (,)
knomToMaybeBinder :: (Typeable s, Swappable ctx, Swappable body, Binder ctxbody ctx body s) => proxy ctxbody -> KNom s (ctx, body) -> Maybe ctxbody
knomToMaybeBinder _ = genAppC $ uncurry resMay
knomToBinder :: (Typeable s, Swappable ctx, Swappable body, Binder ctxbody ctx body s) => proxy ctxbody -> KNom s (ctx, body) -> ctxbody
knomToBinder = fromJust .: knomToMaybeBinder
binderToNom :: Binder ctxbody ctx body s => ctxbody -> KNom s (ctx, body)
binderToNom = kbinderToNom (Proxy :: Proxy ctxbody)
nomToMaybeBinder :: (Typeable s, Swappable ctx, Swappable body, Binder ctxbody ctx body s) => KNom s (ctx, body) -> Maybe ctxbody
nomToMaybeBinder = knomToMaybeBinder (Proxy :: Proxy ctxbody)
nomToBinder :: (Typeable s, Swappable ctx, Swappable body, Binder ctxbody ctx body s) => KNom s (ctx, body) -> ctxbody
nomToBinder = knomToBinder (Proxy :: Proxy ctxbody)
-- * Scrap your boilerplate generic support
binderD :: DataType
binderD = mkDataType "Binder" [binderC]
binderC :: Constr
binderC = mkConstr binderD "(@>)" [] Prefix
instance {-# OVERLAPPABLE #-} (Binder ctxbody ctx body s, Typeable ctxbody, Data ctx, Data body) => Data ctxbody where
gfoldl k z ctxbody = ctxbody @@! \ctx body -> z (@>) `k` ctx `k` body
gunfold k z _ = k $ k $ z (@>)
toConstr = const binderC
dataTypeOf = const binderD
-- * Binder with concretion
-- | Sometimes we can /concrete/ a bound entity @ctxbody@ at a datum @a@ (where it need not be the case that @a == ctx@), to return a body @body@. In the case that @a == ctx@, we expect
--
-- > (a @> x) `conc` a == x
-- > a @> (x' `conc` a) == x
class Binder ctxbody ctx body s => BinderConc ctxbody ctx body s a where
conc :: ctxbody -> a -> body
conc = flip cnoc
cnoc :: a -> ctxbody -> body
cnoc = flip conc
{-# MINIMAL conc | cnoc #-}
-- | Suppose we have a nominal abstraction @x' :: KNom s a@ where @a@ has its own internal notion of name restriction.
-- Then @cnoc () x'@ folds the 'KNom'-bound names down into @x'@ to return a concrete element of @a@.
--
-- > cnoc () = unNom
instance (Typeable s, Swappable a, KRestrict s a) => BinderConc (KNom s a) [KAtom s] a s () where
cnoc = const unNom
-- | Suppose we have a nominal abstraction @x' :: KNom s a@.
-- Then @x' `conc` (Proxy :: Proxy s)@ triggers an IO action to strip the 'KNom' context and concrete @x'@ at some choice of fresh atoms.
--
-- > cnoc (Proxy :: Proxy s) = exit
instance (Typeable s, Swappable a) => BinderConc (KNom s a) [KAtom s] a s (Proxy s) where
cnoc = const exit
-- | Concrete a nominal abstraction at a particular list of atoms.
-- Dangerous for two reasons:
--
-- * The list needs to be long enough.
-- * There are no guarantees about the order the bound atoms come out in.
instance (Typeable s, Swappable a) => BinderConc (KNom s a) [KAtom s] a s [KAtom s] where
conc a' st = a' @@! \atms a -> perm (zip st atms) a
{- $tests Property-based tests are in "Language.Nominal.Properties.NomSpec". -}