Категория подстановочных замен

Настройка

Рассмотрим тип терминов, параметризованных по типу 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:

  1. SubstCat node var Void должен быть типом наземных подстановок.
  2. SubstCat Void var var должен быть типом плоских замен.
  3. instance (Eq node, Eq dom, Eq cod) ⇒ Eq (SubstCat node dom cod) должен существовать (а также аналогичные экземпляры для Ord и Show).
  4. Должно быть возможным вычислить набор переменных домена, заданный набор изображений и введенный набор переменных, заданный SubstCat node dom cod.
  5. Описанные выше операции должны быть такими же быстрыми/эффективными по площади, как их эквиваленты в реализации 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 но мы можем определить экземпляр для его незаметного кулона.