Категория подстановочных замен
Настройка
Рассмотрим тип терминов, параметризованных по типу node
функциональных символов и типа переменных var
:
data Term node var
= VarTerm !var
| FunTerm !node !(Vector (Term node var))
deriving (Eq, Ord, Show)
instance Functor (Term node) where
fmap f (VarTerm var) = VarTerm (f var)
fmap f (FunTerm n cs) = FunTerm n (Vector.map (fmap f) cs)
instance Monad (Term node) where
pure = VarTerm
join (VarTerm term) = term
join (FunTerm n cs) = FunTerm n (Vector.map join cs)
Это полезный тип, поскольку мы кодируем открытые термины с помощью Term node Var
, закрытые термины с Term node Void
и контексты с Term node()
.
Цель состоит в том, чтобы определить тип подстановок на Term
самым приятным способом. Здесь первый удар:
newtype Substitution (node ∷ Type) (var ∷ Type)
= Substitution { fromSubstitution ∷ Map var (Term node var) }
deriving (Eq, Ord, Show)
Теперь давайте определим некоторые вспомогательные значения, связанные с Substitution
:
subst ∷ Substitution node var → Term node var → Term node var
subst s (VarTerm var) = fromMaybe (MkVarTerm var)
(Map.lookup var (fromSubstitution s))
subst s (FunTerm n ts) = FunTerm n (Vector.map (subst s) ts)
identity ∷ Substitution node var
identity = Substitution Map.empty
-- Laws:
--
-- 1. Unitality:
-- ∀ s ∷ Substitution n v → s ≡ (s ∘ identity) ≡ (identity ∘ s)
-- 2. Associativity:
-- ∀ a, b, c ∷ Substitution n v → ((a ∘ b) ∘ c) ≡ (a ∘ (b ∘ c))
-- 3. Monoid action:
-- ∀ x, y ∷ Substitution n v → subst (y ∘ x) ≡ (subst y . subst x)
(∘) ∷ (Ord var)
⇒ Substitution node var
→ Substitution node var
→ Substitution node var
s1 ∘ s2 = Substitution
(Map.unionWith
(λ _ _ → error "this should never happen")
(Map.map (subst s1) (fromSubstitution s2))
((fromSubstitution s1) 'Map.difference' (fromSubstitution s2)))
Ясно, что (Substitution nv, ∘, identity)
является моноидом (игнорируя ограничение Ord
на ∘
) и (Term nv, subst)
является моноидным действием Substitution nv
.
Теперь предположим, что мы хотим сделать эту схему закодировать замены, которые изменяют тип переменной. Это будет выглядеть как некоторый тип SubstCat
который удовлетворяет подписи модуля ниже:
data SubstCat (node ∷ Type) (domainVar ∷ Type) (codomainVar ∷ Type)
= … ∷ Type
subst ∷ SubstCat node dom cod → Term node dom → Term node cod
identity ∷ SubstCat node var var
(∘) ∷ (Ord v1, Ord v2, Ord v3)
→ SubstCat node v2 v3
→ SubstCat node v1 v2
→ SubstCat node v1 v3
Это почти Category
Haskell, но для ограничений Ord
на ∘
. Вы можете подумать, что если (Substitution nv, ∘, identity)
раньше была моноидом, а subst
был моноидным действием раньше, то subst
теперь должен быть действием категории, но, по сути, действия категории - это просто функторы (в этом случае, функтор из подкатегории Хаска в другую подкатегорию Хаска).
Теперь есть некоторые свойства, которые, как мы надеемся, будут иметь отношение к SubstCat
:
-
SubstCat node var Void
должен быть типом наземных подстановок. -
SubstCat Void var var
должен быть типом плоских замен. -
instance (Eq node, Eq dom, Eq cod) ⇒ Eq (SubstCat node dom cod)
должен существовать (а также аналогичные экземпляры для Ord
и Show
). - Должно быть возможным вычислить набор переменных домена, заданный набор изображений и введенный набор переменных, заданный
SubstCat node dom cod
. - Описанные выше операции должны быть такими же быстрыми/эффективными по площади, как их эквиваленты в реализации
Substitution
выше.
Простейшим возможным подходом к написанию SubstCat
было бы просто обобщение Substitution
:
newtype SubstCat (node ∷ Type) (dom ∷ Type) (cod ∷ Type)
= SubstCat { fromSubstCat ∷ Map dom (Term node cod) }
deriving (Eq, Ord, Show)
К сожалению, это не работает, потому что, когда мы запускаем subst
может случиться, что термин, в котором мы выполняем подстановку, содержит переменные, которые не входят в область Map
. Мы могли бы избежать этого в Substitution
с dom ~ cod
, но в SubstCat
мы не имеем возможности справиться с этими переменными.
Моя следующая попытка состояла в том, чтобы решить эту проблему, включив также функцию типа dom → cod
:
data SubstCat (node ∷ Type) (dom ∷ Type) (cod ∷ Type)
= SubstCat
!(Map dom (Term node cod))
!(dom → cod)
Однако это вызывает несколько проблем. Во-первых, поскольку SubstCat
теперь содержит функцию, она больше не может иметь экземпляры Eq
, Ord
или Show
. Мы не можем просто игнорировать поле dom → cod
при сравнении для равенства, поскольку семантика подстановки изменяется в зависимости от ее значения. Во-вторых, теперь уже не так, что SubstCat node var Void
представляет собой тип наземных подстановок; на самом деле, SubstCat node var Void
является необитаемым!
Érdi Gergő предложил в Facebook, что я использую следующее определение:
newtype SubstCat (node ∷ Type) (dom ∷ Type) (cod ∷ Type)
= SubstCat (dom → Term node cod)
Это, безусловно, захватывающая перспектива. Для этого типа существует очевидная категория: категория Kleisli, заданная экземпляром Monad
на Term node
. Однако я не уверен, что это действительно соответствует обычному понятию подстановки. К сожалению, это представление не может иметь экземпляров для Eq
et al. и я подозреваю, что на практике это может быть очень неэффективным, так как в лучшем случае это будет башня затворов высоты Θ(n)
, где n
- количество вставок. Он также не позволяет вычислять набор переменных домена.
Вопрос
Есть ли разумный тип для SubstCat
который соответствует моим требованиям? Можете ли вы доказать, что этого не существует? Если я откажусь от правильных экземпляров Eq
, Ord
и Show
, возможно ли это?
Ответы
Ответ 1
Для этого типа существует очевидная категория: категория Kleisli, заданная экземпляром Monad на узле Term. Однако я не уверен, что это действительно соответствует обычному понятию подстановки.
Это соответствует этому, и это всестороннее правильное определение параллельной подстановки для хорошо охваченных терминов. Вы можете заметить, что эта замена является общей; это требование, если вы хотите, чтобы подстановка терминов была полной, т.е. это функтор из категории подстановок (Subst) в Set. Для простого примера того, почему частичные замены не работают, если у вас есть пустой SubstCat node() Void
, тогда вам придется изобретать произвольные замкнутые термины при попадании VarTerm()
в subst
и закрытые выражения даже не существуют, если вы выбираете Void
для node
.
Следовательно, если у вас есть Map dom (Term node cod)
то у вас есть частичная замена подменю, то есть функтор от Subst до категории Kleisli для Maybe
(без учета формальных осложнений из ограничений Ord
):
type Subst node i j = Map i (Term node j)
subst :: Ord i => Subst node i j -> Term node i -> Maybe (Term node j)
subst sub (VarTerm x) = Map.lookup x sub
subst sub (FunTerm f ts) = FunTerm f <$> traverse (subst sub) ts
Теперь возможна замена общего термина вместе с вашими 1-5 SubstCat
для SubstCat
, но не с текущими видами для SubstCat
и Term
. Как я уже упоминал, в этом случае подстановки должны быть полными, но в настоящее время мы можем просто иметь SubstCat node dom cod
с некоторым бесконечным dom
, что делает равенство подстановок неразрешимым.
Поэтому давайте переключимся на более точную формализацию, которая содержит только то, что нам здесь нужно. У нас есть хорошо выраженные нетипизированные термины, и мы предполагаем, что члены конечны и живут в конечных переменных контекстах.
Нетипированные термины означают, что важны только размеры переменных контекстов. Итак, Subst имеет натуральные числа как объекты:
data Nat = Z | S Nat deriving (Eq, Show)
Термины индексируются n :: Nat
, содержащие n
возможных значений для переменных:
-- {-# language TypeInType, GADTs, TypeFamilies, ScopedTypeVariables,
-- FlexibleInstances, MultiParamTypeClasses, RankNTypes,
-- TypeApplications, StandaloneDeriving #-}
data Fin n where
FZ :: Fin (S n)
FS :: Fin n -> Fin (S n)
deriving instance Eq (Fin n)
deriving instance Ord (Fin n)
deriving instance Show (Fin n)
data Term node (n :: Nat) where
VarTerm :: !(Fin n) -> Term node n
FunTerm :: !node -> !(Vector (Term node n)) -> Term node n
deriving instance Eq node => Eq (Term node n)
deriving instance Ord node => Ord (Term node n)
deriving instance Show node => Show (Term node n)
Замены (морфизмы) являются векторами термов:
data Vec a n where
Nil :: Vec a Z
(:>) :: a -> Vec a n -> Vec a (S n)
infixr 5 :>
deriving instance Eq a => Eq (Vec a n)
deriving instance Ord a => Ord (Vec a n)
deriving instance Show a => Show (Vec a n)
type Subst node i j = Vec (Term node j) i
Замена термина выглядит следующим образом:
subst :: Subst node i j -> Term node i -> Term node j
subst (t :> _ ) (VarTerm FZ) = t
subst (_ :> ts) (VarTerm (FS x)) = subst ts (VarTerm x)
subst sub (FunTerm f ts) = FunTerm f (Vector.map (subst sub) ts)
Состав просто точечно subst
:
comp :: Subst node i j -> Subst node j k -> Subst node i k
comp Nil sub2 = Nil
comp (t :> ts) sub2 = subst sub2 t :> comp ts sub2
Подстановка подстановки не так проста. Нам нужно послать на уровне типа Nat
. Для этого мы используем класс типа; singletons
были бы более тяжелым, но более принципиальным решением. Сама реализация также немного угасает. Он использует тот факт, что Subst node nm
изоморфен (Fin n → Term node m)
. Фактически, мы могли бы просто использовать функциональное представление все время, но тогда экземпляры Eq, Ord and Show
прежнему будут эффективно нуждаться в обратном преобразовании, и мы не будем иметь гарантии ограниченности пространства-времени (строгих) векторов.
class Tabulate n where
tabulate :: (Fin n -> Term node m) -> Subst node n m
instance Tabulate Z where
tabulate f = Nil
instance Tabulate n => Tabulate (S n) where
tabulate f = f FZ :> tabulate (f . FS)
idSubst :: Tabulate n => Subst node n n
idSubst = tabulate VarTerm
Это! Доказательства законов категории для законов Subst
и функтора для subst
остаются в виде упражнения.
PS: Я отмечаю, что в литературе и формальных разработках Agda/Coq индексы Nat
индексов Subst
обычно находятся в порядке subst
, а subst
имеет контравариантное действие.
Ответ 2
{-# LANGUAGE TypeFamilies, TypeOperators #-}
import Data.MemoTrie
import Control.Category.Constrained
newtype SubstCat node dom cod = SubstCat (dom :->: Term node cod)
instance Category (SubstCat node) where
type Object (SubstCat node) a = HasTrie a
id = SubstCat (trie VarTerm)
SubstCat f . SubstCat g = SubstCat $ fmap (untrie f =<<) g
Тип memo-trie :->:
в основном тип карты, но отображения всегда тотальны. То есть, они являются правильными функциями, но в фиксированном по размеру представлении - никаких закрытий не требуется. Это работает только для типов, которые не только упорядочиваются, но и полностью перечислимы, что и HasTrie
класс HasTrie
; таким образом, мы не можем определить экземпляр для Control.Category.Category
но мы можем определить экземпляр для его незаметного кулона.