Подібно до ловця дітей у Чітті-Чітті-Банг-Бенг, що заманює дітей у полон солодощами та іграшками, вербувальники, які навчаються на бакалавраті фізики, люблять дуріти з бульбашками мила та бумерангами, але коли стукіт дверей зачиняється, це „Правильно, діти, час вчитися про часткову диференціацію! ". Я також. Не кажіть, що я вас не попередив.
Ось ще одне попередження: потрібен наступний код {-# LANGUAGE KitchenSink #-}
, вірніше
{-# LANGUAGE TypeFamilies, FlexibleContexts, TupleSections, GADTs, DataKinds,
TypeOperators, FlexibleInstances, RankNTypes, ScopedTypeVariables,
StandaloneDeriving, UndecidableInstances #-}
у певному порядку.
Різні функтори дають комонадичні блискавки
Що взагалі є диференційованим функтором?
class (Functor f, Functor (DF f)) => Diff1 f where
type DF f :: * -> *
upF :: ZF f x -> f x
downF :: f x -> f (ZF f x)
aroundF :: ZF f x -> ZF f (ZF f x)
data ZF f x = (:<-:) {cxF :: DF f x, elF :: x}
Це функтор, який має похідну, який також є функтором. Похідна представляє контекст з одним отвором для елемента . Тип блискавки ZF f x
представляє пару контексту з одним отвором та елемент у отворі.
Операції для Diff1
опису видів навігації, які ми можемо робити на блискавках (без будь-якого поняття "ліворуч" і "праворуч", про що див. Мої клоуни та жартівники статтю про ). Ми можемо піти «вгору», зібравши структуру, заткнувши елемент у його отворі. Ми можемо йти "вниз", знаходячи всі способи відвідати елемент у структурі надання: ми прикрашаємо кожен елемент його контекстом. Ми можемо обійтись, взявши існуючу застібку-блискавку і прикрасивши кожен елемент його контекстом, тому ми знаходимо всі способи перефокусуватись (і як зберегти наш поточний фокус).
Тепер тип aroundF
може нагадувати деяким з вас
class Functor c => Comonad c where
extract :: c x -> x
duplicate :: c x -> c (c x)
і ви маєте рацію, коли вам нагадали! У нас, із стрибком і стрибком,
instance Diff1 f => Functor (ZF f) where
fmap f (df :<-: x) = fmap f df :<-: f x
instance Diff1 f => Comonad (ZF f) where
extract = elF
duplicate = aroundF
і ми наполягаємо на цьому
extract . duplicate == id
fmap extract . duplicate == id
duplicate . duplicate == fmap duplicate . duplicate
Це нам теж потрібно
fmap extract (downF xs) == xs
fmap upF (downF xs) = fmap (const xs) xs
Поліноміальні функтори диференційовані
Постійні функтори можна диференціювати.
data KF a x = KF a
instance Functor (KF a) where
fmap f (KF a) = KF a
instance Diff1 (KF a) where
type DF (KF a) = KF Void
upF (KF w :<-: _) = absurd w
downF (KF a) = KF a
aroundF (KF w :<-: _) = absurd w
Покласти елемент нікуди, тому неможливо сформувати контекст. Нікуди йти, upF
ні downF
звідки, і ми легко знаходимо всі шляхи, якими ми можемо пітиdownF
.
Тотожне функтор дифференцируема.
data IF x = IF x
instance Functor IF where
fmap f (IF x) = IF (f x)
instance Diff1 IF where
type DF IF = KF ()
upF (KF () :<-: x) = IF x
downF (IF x) = IF (KF () :<-: x)
aroundF z@(KF () :<-: x) = KF () :<-: z
Є один елемент у тривіальному контексті, downF
знаходить його, upF
перепаковує іaroundF
може залишатися на місці.
Сума зберігає диференційованість.
data (f :+: g) x = LF (f x) | RF (g x)
instance (Functor f, Functor g) => Functor (f :+: g) where
fmap h (LF f) = LF (fmap h f)
fmap h (RF g) = RF (fmap h g)
instance (Diff1 f, Diff1 g) => Diff1 (f :+: g) where
type DF (f :+: g) = DF f :+: DF g
upF (LF f' :<-: x) = LF (upF (f' :<-: x))
upF (RF g' :<-: x) = RF (upF (g' :<-: x))
Інших шматочків трохи більше, ніж кілька. Щоб піти downF
, ми повинні увійти downF
всередину позначеного компонента, а потім виправити отримані блискавки, щоб показати тег у контексті.
downF (LF f) = LF (fmap (\ (f' :<-: x) -> LF f' :<-: x) (downF f))
downF (RF g) = RF (fmap (\ (g' :<-: x) -> RF g' :<-: x) (downF g))
Щоб продовжити aroundF
, ми знімаємо тег, з’ясовуємо, як об’їжджати непозначену річ, а потім відновлюємо тег на всіх отриманих блискавках. Елемент у фокусі, x
замінюється на всій блискавки, z
.
aroundF z@(LF f' :<-: (x :: x)) =
LF (fmap (\ (f' :<-: x) -> LF f' :<-: x) . cxF $ aroundF (f' :<-: x :: ZF f x))
:<-: z
aroundF z@(RF g' :<-: (x :: x)) =
RF (fmap (\ (g' :<-: x) -> RF g' :<-: x) . cxF $ aroundF (g' :<-: x :: ZF g x))
:<-: z
Зауважте, що мені довелося використати, ScopedTypeVariables
щоб усунути неоднозначність рекурсивних дзвінків на aroundF
. Як функція типу, DF
не є ін'єктивною, тому факт, що f' :: D f x
недостатньо, щоб змуситиf' :<-: x :: Z f x
.
Продукт зберігає диференційованість.
data (f :*: g) x = f x :*: g x
instance (Functor f, Functor g) => Functor (f :*: g) where
fmap h (f :*: g) = fmap h f :*: fmap h g
Щоб зосередитись на елементі в парі, ви або фокусуєтесь на лівому, а правий залишаєте в спокої, або навпаки. Знамените правило продукту Лейбніца відповідає простій просторовій інтуїції!
instance (Diff1 f, Diff1 g) => Diff1 (f :*: g) where
type DF (f :*: g) = (DF f :*: g) :+: (f :*: DF g)
upF (LF (f' :*: g) :<-: x) = upF (f' :<-: x) :*: g
upF (RF (f :*: g') :<-: x) = f :*: upF (g' :<-: x)
Тепер це downF
працює подібно до того, як це було для сум, за винятком того, що ми маємо виправити контекст блискавки не тільки тегом (щоб показати, яким шляхом ми пройшли), але і незайманим іншим компонентом.
downF (f :*: g)
= fmap (\ (f' :<-: x) -> LF (f' :*: g) :<-: x) (downF f)
:*: fmap (\ (g' :<-: x) -> RF (f :*: g') :<-: x) (downF g)
Але aroundF
це величезна сумка сміху. Яку б сторону ми зараз не відвідували, у нас є два варіанти:
- Рухайся
aroundF
на цьому боці.
- Перемістіться
upF
з тієї сторони downF
на іншу сторону.
Кожен випадок вимагає від нас використання операцій над підструктурою, а потім виправлення контекстів.
aroundF z@(LF (f' :*: g) :<-: (x :: x)) =
LF (fmap (\ (f' :<-: x) -> LF (f' :*: g) :<-: x)
(cxF $ aroundF (f' :<-: x :: ZF f x))
:*: fmap (\ (g' :<-: x) -> RF (f :*: g') :<-: x) (downF g))
:<-: z
where f = upF (f' :<-: x)
aroundF z@(RF (f :*: g') :<-: (x :: x)) =
RF (fmap (\ (f' :<-: x) -> LF (f' :*: g) :<-: x) (downF f) :*:
fmap (\ (g' :<-: x) -> RF (f :*: g') :<-: x)
(cxF $ aroundF (g' :<-: x :: ZF g x)))
:<-: z
where g = upF (g' :<-: x)
Фу! Поліноми всі диференційовані і, таким чином, дають нам комонади.
Хм Це все трохи абстрактно. Тому я додав deriving Show
скрізь, де міг, і кинув
deriving instance (Show (DF f x), Show x) => Show (ZF f x)
що дозволило наступну взаємодію (приведене в порядок від руки)
> downF (IF 1 :*: IF 2)
IF (LF (KF () :*: IF 2) :<-: 1) :*: IF (RF (IF 1 :*: KF ()) :<-: 2)
> fmap aroundF it
IF (LF (KF () :*: IF (RF (IF 1 :*: KF ()) :<-: 2)) :<-: (LF (KF () :*: IF 2) :<-: 1))
:*:
IF (RF (IF (LF (KF () :*: IF 2) :<-: 1) :*: KF ()) :<-: (RF (IF 1 :*: KF ()) :<-: 2))
Вправа Покажіть, що склад диференційованих функторів диференціюється, використовуючи правило ланцюга .
Солодко! Чи можемо ми зараз додому? Звичайно, ні. Ми ще не диференціювали жодної рекурсивної структури.
Створення рекурсивних функторів з біфункторів
Як Bifunctor
пояснює існуюча література про загальне програмування типів даних (див. Роботу Патріка Янссона та Йохана Йерінга або чудові конспекти лекцій Джеремі Гіббонса), це конструктор типів із двома параметрами, що відповідають двом типам підструктур. Ми повинні мати можливість "нанести на карту" обидва.
class Bifunctor b where
bimap :: (x -> x') -> (y -> y') -> b x y -> b x' y'
Ми можемо використовувати Bifunctor
s, щоб дати структуру вузла рекурсивних контейнерів. Кожен вузол має підвузли та елементи . Це можуть бути лише два типи підструктури.
data Mu b y = In (b (Mu b y) y)
Подивитися? Ми "зав'язуємо рекурсивний вузол" у b
першому аргументі, а параметр зберігаємо y
у другому. Відповідно, отримуємо раз і назавжди
instance Bifunctor b => Functor (Mu b) where
fmap f (In b) = In (bimap (fmap f) f b)
Для цього нам знадобиться набір Bifunctor
екземплярів.
Набір Bifunctor
Константи є біфункціональними.
newtype K a x y = K a
instance Bifunctor (K a) where
bimap f g (K a) = K a
Ви можете сказати, що я написав цей біт першим, оскільки ідентифікатори коротші, але це добре, оскільки код довший.
Змінні є двофункціональними.
Нам потрібні біфункціонери, що відповідають тому чи іншому параметру, тому я створив тип даних для їх розрізнення, а потім визначив відповідний GADT.
data Var = X | Y
data V :: Var -> * -> * -> * where
XX :: x -> V X x y
YY :: y -> V Y x y
Це робить V X x y
копію x
та V Y x y
копію y
. Відповідно
instance Bifunctor (V v) where
bimap f g (XX x) = XX (f x)
bimap f g (YY y) = YY (g y)
Суми і продукти з bifunctors є bifunctors
data (:++:) f g x y = L (f x y) | R (g x y) deriving Show
instance (Bifunctor b, Bifunctor c) => Bifunctor (b :++: c) where
bimap f g (L b) = L (bimap f g b)
bimap f g (R b) = R (bimap f g b)
data (:**:) f g x y = f x y :**: g x y deriving Show
instance (Bifunctor b, Bifunctor c) => Bifunctor (b :**: c) where
bimap f g (b :**: c) = bimap f g b :**: bimap f g c
Поки що це так, але зараз ми можемо визначити такі речі
List = Mu (K () :++: (V Y :**: V X))
Bin = Mu (V Y :**: (K () :++: (V X :**: V X)))
Якщо ви хочете використовувати ці типи для фактичних даних і не сліпити в пуантилістській традиції Жоржа Сера, використовуйте синоніми зразків .
Але що з блискавками? Як ми покажемо, що Mu b
можна диференціювати? Нам потрібно буде показати, що b
диференційовано в обох змінних. Кланг! Пора дізнатися про часткову диференціацію.
Часткові похідні біфункціонерів
Оскільки ми маємо дві змінні, нам потрібно мати можливість говорити про них інколи, а інколи - окремо. Нам знадобиться сімейство одиноких:
data Vary :: Var -> * where
VX :: Vary X
VY :: Vary Y
Тепер ми можемо сказати, що означає для Біфункціонера часткові похідні при кожній змінній, і дати відповідне поняття блискавки.
class (Bifunctor b, Bifunctor (D b X), Bifunctor (D b Y)) => Diff2 b where
type D b (v :: Var) :: * -> * -> *
up :: Vary v -> Z b v x y -> b x y
down :: b x y -> b (Z b X x y) (Z b Y x y)
around :: Vary v -> Z b v x y -> Z b v (Z b X x y) (Z b Y x y)
data Z b v x y = (:<-) {cxZ :: D b v x y, elZ :: V v x y}
Ця D
операція повинна знати, на яку змінну націлити. Відповідна блискавка Z b v
повідомляє нам, яка змінна v
повинна бути у фокусі. Коли ми "прикрашаємо контекстом", ми маємо прикрашати x
-елементи X
-контекстами та y
-елементи Y
-контекстами. Але в іншому випадку це та сама історія.
У нас залишилось два завдання: по-перше, показати, що наш комплект біфункціональних пристроїв є диференційованим; по-друге, показати, що Diff2 b
дозволяє нам встановити Diff1 (Mu b)
.
Розрізнення набору Bifunctor
Я боюся, що цей біт є хитрим, а не повчальним. Не соромтеся пропускати далі.
Константи такі, як і раніше.
instance Diff2 (K a) where
type D (K a) v = K Void
up _ (K q :<- _) = absurd q
down (K a) = K a
around _ (K q :<- _) = absurd q
З цієї нагоди життя занадто коротке, щоб розвивати теорію рівня типу Кронекер-дельта, тому я просто розглянув змінні окремо.
instance Diff2 (V X) where
type D (V X) X = K ()
type D (V X) Y = K Void
up VX (K () :<- XX x) = XX x
up VY (K q :<- _) = absurd q
down (XX x) = XX (K () :<- XX x)
around VX z@(K () :<- XX x) = K () :<- XX z
around VY (K q :<- _) = absurd q
instance Diff2 (V Y) where
type D (V Y) X = K Void
type D (V Y) Y = K ()
up VX (K q :<- _) = absurd q
up VY (K () :<- YY y) = YY y
down (YY y) = YY (K () :<- YY y)
around VX (K q :<- _) = absurd q
around VY z@(K () :<- YY y) = K () :<- YY z
Для структурних випадків я знайшов корисним ввести помічник, що дозволяє мені рівномірно обробляти змінні.
vV :: Vary v -> Z b v x y -> V v (Z b X x y) (Z b Y x y)
vV VX z = XX z
vV VY z = YY z
Потім я створив ґаджети для полегшення того типу "перемаркірування", який нам потрібен down
і around
. (Звичайно, я бачив, які гаджети мені потрібні під час роботи.)
zimap :: (Bifunctor c) => (forall v. Vary v -> D b v x y -> D b' v x y) ->
c (Z b X x y) (Z b Y x y) -> c (Z b' X x y) (Z b' Y x y)
zimap f = bimap
(\ (d :<- XX x) -> f VX d :<- XX x)
(\ (d :<- YY y) -> f VY d :<- YY y)
dzimap :: (Bifunctor (D c X), Bifunctor (D c Y)) =>
(forall v. Vary v -> D b v x y -> D b' v x y) ->
Vary v -> Z c v (Z b X x y) (Z b Y x y) -> D c v (Z b' X x y) (Z b' Y x y)
dzimap f VX (d :<- _) = bimap
(\ (d :<- XX x) -> f VX d :<- XX x)
(\ (d :<- YY y) -> f VY d :<- YY y)
d
dzimap f VY (d :<- _) = bimap
(\ (d :<- XX x) -> f VX d :<- XX x)
(\ (d :<- YY y) -> f VY d :<- YY y)
d
І з цією партією, готовою до роботи, ми можемо подрібнити деталі. Суми легкі.
instance (Diff2 b, Diff2 c) => Diff2 (b :++: c) where
type D (b :++: c) v = D b v :++: D c v
up v (L b' :<- vv) = L (up v (b' :<- vv))
down (L b) = L (zimap (const L) (down b))
down (R c) = R (zimap (const R) (down c))
around v z@(L b' :<- vv :: Z (b :++: c) v x y)
= L (dzimap (const L) v ba) :<- vV v z
where ba = around v (b' :<- vv :: Z b v x y)
around v z@(R c' :<- vv :: Z (b :++: c) v x y)
= R (dzimap (const R) v ca) :<- vV v z
where ca = around v (c' :<- vv :: Z c v x y)
Продукти - це важка робота, саме тому я математик, а не інженер.
instance (Diff2 b, Diff2 c) => Diff2 (b :**: c) where
type D (b :**: c) v = (D b v :**: c) :++: (b :**: D c v)
up v (L (b' :**: c) :<- vv) = up v (b' :<- vv) :**: c
up v (R (b :**: c') :<- vv) = b :**: up v (c' :<- vv)
down (b :**: c) =
zimap (const (L . (:**: c))) (down b) :**: zimap (const (R . (b :**:))) (down c)
around v z@(L (b' :**: c) :<- vv :: Z (b :**: c) v x y)
= L (dzimap (const (L . (:**: c))) v ba :**:
zimap (const (R . (b :**:))) (down c))
:<- vV v z where
b = up v (b' :<- vv :: Z b v x y)
ba = around v (b' :<- vv :: Z b v x y)
around v z@(R (b :**: c') :<- vv :: Z (b :**: c) v x y)
= R (zimap (const (L . (:**: c))) (down b):**:
dzimap (const (R . (b :**:))) v ca)
:<- vV v z where
c = up v (c' :<- vv :: Z c v x y)
ca = around v (c' :<- vv :: Z c v x y)
Концептуально це так само, як і раніше, але з більшою бюрократією. Я побудував їх за технологією попереднього типу отвору, використовуючиundefined
як заглушку в місцях, де я не був готовий працювати, і вводячи навмисну помилку типу в одному місці (у будь-який момент часу), де я хотів отримати корисну підказку від перевірки друку . Ви також можете використовувати перевірку друку як досвід відеоігор, навіть у Haskell.
Блискавки підвузлів для рекурсивних контейнерів
Часткова похідна по b
відношенню до X
говорить нам, як знайти підвузол на один крок всередині вузла, тож ми отримаємо загальноприйняте поняття блискавки.
data MuZpr b y = MuZpr
{ aboveMu :: [D b X (Mu b y) y]
, hereMu :: Mu b y
}
Ми можемо збільшувати масштаб до кореня, повторюючи підключення X
позицій.
muUp :: Diff2 b => MuZpr b y -> Mu b y
muUp (MuZpr {aboveMu = [], hereMu = t}) = t
muUp (MuZpr {aboveMu = (dX : dXs), hereMu = t}) =
muUp (MuZpr {aboveMu = dXs, hereMu = In (up VX (dX :<- XX t))})
Але нам потрібні елемент- блискавки.
Елементи-блискавки для точок кріплення біфункціонерів
Кожен елемент знаходиться десь усередині вузла. Цей вузол сидить під стопкою X
похідних. Але позиція елемента у цьому вузлі задається Y
похідною-похідною. Ми отримуємо
data MuCx b y = MuCx
{ aboveY :: [D b X (Mu b y) y]
, belowY :: D b Y (Mu b y) y
}
instance Diff2 b => Functor (MuCx b) where
fmap f (MuCx { aboveY = dXs, belowY = dY }) = MuCx
{ aboveY = map (bimap (fmap f) f) dXs
, belowY = bimap (fmap f) f dY
}
Сміливо, стверджую я
instance Diff2 b => Diff1 (Mu b) where
type DF (Mu b) = MuCx b
але перш ніж розробляти операції, мені знадобляться шматочки.
Я можу торгувати даними між застібками-блискавками та біфункціональними застібками наступним чином:
zAboveY :: ZF (Mu b) y -> [D b X (Mu b y) y]
zAboveY (d :<-: y) = aboveY d
zZipY :: ZF (Mu b) y -> Z b Y (Mu b y) y
zZipY (d :<-: y) = belowY d :<- YY y
Цього достатньо, щоб дозволити мені визначити:
upF z = muUp (MuZpr {aboveMu = zAboveY z, hereMu = In (up VY (zZipY z))})
Тобто, ми піднімаємося вгору, спочатку зібравши вузол, де знаходиться елемент, перетворивши елемент-блискавку на підвузол-блискавку, а потім збільшивши масштаб, як зазначено вище.
Далі, кажу я
downF = yOnDown []
щоб спуститися вниз, починаючи з порожнього стека, і визначити допоміжну функцію, яка down
повторюється знизу будь-якого стека:
yOnDown :: Diff2 b => [D b X (Mu b y) y] -> Mu b y -> Mu b (ZF (Mu b) y)
yOnDown dXs (In b) = In (contextualize dXs (down b))
Тепер down b
нас веде лише всередину вузла. Застібки-блискавки також повинні містити контекст вузла. Ось що contextualise
:
contextualize :: (Bifunctor c, Diff2 b) =>
[D b X (Mu b y) y] ->
c (Z b X (Mu b y) y) (Z b Y (Mu b y) y) ->
c (Mu b (ZF (Mu b) y)) (ZF (Mu b) y)
contextualize dXs = bimap
(\ (dX :<- XX t) -> yOnDown (dX : dXs) t)
(\ (dY :<- YY y) -> MuCx {aboveY = dXs, belowY = dY} :<-: y)
Для кожної Y
позиції ми повинні вказати елемент-блискавку, тому добре, що ми знаємо весь контекст dXs
назад до кореня, а також той, dY
який описує, як елемент сидить у своєму вузлі. Для кожної X
позиції існує ще одне піддерево, яке ми можемо дослідити, тому ми розвиваємо стек і продовжуємо!
Це залишає лише зміну фокусу. Ми можемо залишитися на місці, або спуститися з того місця, де ми перебуваємо, або піднятися, або піднятися, а потім спуститися якоюсь іншою стежкою. Ось іде.
aroundF z@(MuCx {aboveY = dXs, belowY = dY} :<-: _) = MuCx
{ aboveY = yOnUp dXs (In (up VY (zZipY z)))
, belowY = contextualize dXs (cxZ $ around VY (zZipY z))
} :<-: z
Як і раніше, існуючий елемент замінений на всю його блискавку. З іншого belowY
боку, ми розглянемо, куди ще ми можемо піти в існуючому вузлі: ми знайдемо або альтернативні позиції елемента, Y
або подальші X
-підвузли для дослідження, тому ми contextualise
їх. З іншого aboveY
боку, ми повинні попрацювати, повернувшись до стеку X
похідних, після повторного складання вузла, який ми відвідували.
yOnUp :: Diff2 b => [D b X (Mu b y) y] -> Mu b y ->
[D b X (Mu b (ZF (Mu b) y)) (ZF (Mu b) y)]
yOnUp [] t = []
yOnUp (dX : dXs) (t :: Mu b y)
= contextualize dXs (cxZ $ around VX (dX :<- XX t))
: yOnUp dXs (In (up VX (dX :<- XX t)))
На кожному кроці ми можемо або повернутись кудись ще, що це around
, або продовжувати йти вгору.
І це все! Я не дав офіційного підтвердження законів, але мені здається, ніби операції ретельно підтримують контекст правильно, коли вони сканують структуру.
Що ми дізналися?
Диференційованість спонукає уявлення про річ-у-своєму-контексті, індукуючи комонадичну структуру, яка extract
дає вам річ і duplicate
досліджує контекст, шукаючи інші речі для контекстуалізації. Якщо ми маємо відповідну диференціальну структуру для вузлів, ми можемо розробити диференціальну структуру для цілих дерев.
О, і ставитися до кожної окремої сутності конструктора типу окремо є відверто жахливим. Кращий спосіб - це робота з функторами між індексованими наборами
f :: (i -> *) -> (o -> *)
де ми робимо o
різні типи структур, що зберігають i
різні типи елементів. Вони закриті під будівництво Якобія
J f :: (i -> *) -> ((o, i) -> *)
де кожна з отриманих (o, i)
-структур є частковою похідною, яка розповідає вам, як зробити i
дірку -елемент в o
-структурі. Але це залежно від типу розваги, інший раз.