Подібно до ловця дітей у Чітті-Чітті-Банг-Бенг, що заманює дітей у полон солодощами та іграшками, вербувальники, які навчаються на бакалавраті фізики, люблять дуріти з бульбашками мила та бумерангами, але коли стукіт дверей зачиняється, це „Правильно, діти, час вчитися про часткову диференціацію! ". Я також. Не кажіть, що я вас не попередив.
Ось ще одне попередження: потрібен наступний код {-# 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'
Ми можемо використовувати Bifunctors, щоб дати структуру вузла рекурсивних контейнерів. Кожен вузол має підвузли та елементи . Це можуть бути лише два типи підструктури.
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-структурі. Але це залежно від типу розваги, інший раз.