Могу ли я ограничить семейство типов?
В этот недавний мой ответ, я случайно открыл этот старый каштан (программа такая старая, половина ее была написана в семнадцатом веке Лейбницем и написанный на компьютере в семидесятых моим папой). Я не использую современный бит, чтобы сэкономить место.
class Differentiable f where
type D f :: * -> *
newtype K a x = K a
newtype I x = I x
data (f :+: g) x = L (f x)
| R (g x)
data (f :*: g) x = f x :&: g x
instance Differentiable (K a) where
type D (K a) = K Void
instance Differentiable I where
type D I = K ()
instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
Теперь, вот что расстраивает. Я не знаю, как оговаривать, что D f
должен быть дифференцируемым. Конечно, эти случаи уважают это свойство, и могут быть интересные программы, которые вы можете написать, которые используют возможность продолжать дифференцировать функтор, снимать дыры во все большем количестве мест: расширения Тейлора, что-то типа.
Я хотел бы сказать что-то вроде
class Differentiable f where
type D f
instance Differentiable (D f)
и требуют проверки того, что объявления экземпляров имеют определения type
, для которых существуют необходимые экземпляры.
Возможно, более мирские вещи, такие как
class SortContainer c where
type WhatsIn c
instance Ord (WhatsIn c)
...
также будет приятным. Это, конечно, имеет обходное решение
class Ord w => SortContainer c w | c -> w where ...
но попытаться сделать тот же трюк для Differentiable
кажется... ну... инволютивным.
Итак, есть ли замечательное решение, которое позволяет мне повторимую дифференцируемость? (Я думаю, я мог бы создать представление GADT и... и есть ли способ, который работает с классами?)
И есть ли какие-либо очевидные недостатки с предположением, что мы должны будем требовать ограничений на типы (и, я полагаю, данные), когда мы объявляем их, а затем проверяем, удовлетворяют ли им экземпляры?
Ответы
Ответ 1
Конечно, очевидным было бы просто написать требуемое ограничение напрямую:
class (Differentiable (D f)) => Differentiable (f :: * -> *) where
Увы, GHC получает оскорбительный об этом и отказывается играть вместе:
ConstrainTF.hs:17:1:
Cycle in class declaration (via superclasses):
Differentiable -> Differentiable
In the class declaration for `Differentiable'
Итак, как это часто бывает при попытке описать ограничения типа, достаточно подходящие для того, чтобы оставить GHC непоколебимым, мы должны прибегнуть к какому-то обманутому обману.
Вспоминая некоторые релевантные функции GHC, в которых используется хакерство типов:
- Это параноидальное явление, связанное с прерыванием уровня типа, далеко не соответствующее фактическим неудобствам, которые оно влечет за собой для пользователя.
- Он будет радостно принимать решения о классах и экземплярах, прежде чем он рассмотрит всю имеющуюся информацию.
- Он будет покорно пытаться проверить все, что вы обманули, чтобы совершить сделку.
Это коварные принципы, лежащие в основе привычных старых экземпляров faux-generic, где типы унифицированы после hoc с помощью (~)
, чтобы улучшить поведение вывода типа для конструкций типа хакеров.
В этом случае, однако, вместо того, чтобы скрывать информацию о типе за GHC, нам нужно каким-то образом не допустить, чтобы GHC заметил ограничение класса, которое является совершенно другим видом... heeeey, waaaitaminute....
import GHC.Prim
type family DiffConstraint (f :: * -> *) :: Constraint
type instance DiffConstraint f = Differentiable f
class (DiffConstraint (D f)) => Differentiable (f :: * -> *) where
type D f :: * -> *
Подъем по собственной петарде!
Это тоже реальная сделка. Они принимаются, как вы надеетесь:
instance Differentiable (K a) where
type D (K a) = K Void
instance Differentiable I where
type D I = K ()
Но если мы предложим вместо этого какую-то бессмыслицу:
instance Differentiable I where
type D I = []
GHC представляет нам именно сообщение об ошибке, которое мы хотели бы видеть:
ConstrainTF.hs:29:10:
No instance for (Differentiable [])
arising from the superclasses of an instance declaration
Possible fix: add an instance declaration for (Differentiable [])
In the instance declaration for `Differentiable I'
Существует, конечно, одна небольшая ошибка, а именно:
instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
... оказывается менее обоснованным, так как мы сказали GHC, что если (f :+: g)
Differentiable
, то есть (D f :+: D g)
, который не заканчивается хорошо (или вообще).
Самый простой способ избежать этого, как правило, заключается в том, чтобы скомпилировать кучу явных базовых случаев, но здесь GHC кажется намеренным расходиться в любое время, когда в контексте экземпляра появляется ограничение Differentiable
. Я бы предположил, что он излишне стремится к проверке ограничений экземпляра каким-то образом и может быть отвлечен достаточно долго с другим слоем обмана, но я не сразу уверен, с чего начать и исчерпал свои возможности для хакерства после полуночи сегодня вечером.
Немного об IRC-обсуждении на #haskell удалось немного размахивать моей память о том, как GHC обрабатывает ограничения контекста класса, и, похоже, мы можем немного исправить ситуацию с помощью семейства ограничений pickier. Используя это:
type family DiffConstraint (f :: * -> *) :: Constraint
type instance DiffConstraint (K a) = Differentiable (K a)
type instance DiffConstraint I = Differentiable I
type instance DiffConstraint (f :+: g) = (Differentiable f, Differentiable g)
Теперь у нас есть гораздо более корректная рекурсия для сумм:
instance (Differentiable (D f), Differentiable (D g)) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
Рекурсивный случай не может быть так легко делит пополам для продуктов, однако, и, применяя те же самые изменения, улучшилось дело только в том случае, если я получил переполнение стека контекстной репликации, а не просто висит в бесконечном цикле.
Ответ 2
Лучше всего определить что-то, используя пакет constraints
:
import Data.Constraint
class Differentiable (f :: * -> *) where
type D f :: * -> *
witness :: p f -> Dict (Differentiable (D f))
тогда вы можете вручную открыть словарь всякий раз, когда вам нужно рекурсивно.
Это позволит вам использовать общую форму решения в ответе Кейси, но при этом не нужно, чтобы компилятор (или время выполнения) вращался навсегда при индукции.
Ответ 3
С новым UndecidableSuperclasses
в GHC 8
class (Differentiable (D f)) => Differentiable (f :: * -> *) where
работы.
Ответ 4
Это можно сделать так же, как предлагает Эдвард с крошечной реализацией Dict
. Во-первых, позвольте получить языковые расширения и импорт в сторону.
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
import Data.Proxy
TypeOperators
предназначен только для вашего примера.
Tiny Dict
Мы можем сделать свою крошечную реализацию Dict
. Dict
использует GADT и ConstraintKinds
для захвата любого ограничения в конструкторе для GADT.
data Dict c where
Dict :: c => Dict c
withDict
и withDict2
повторно установите ограничение путем сопоставления шаблонов на GADT. Нам нужно только иметь возможность рассуждать о терминах с одним или двумя источниками ограничений.
withDict :: Dict c -> (c => x) -> x
withDict Dict x = x
withDict2 :: Dict a -> Dict b -> ((a, b) => x) -> x
withDict2 Dict Dict x = x
Бесконечно дифференцируемые типы
Теперь мы можем говорить о бесконечно дифференцируемых типах, производные которых также должны быть дифференцируемы
class Differentiable f where
type D f :: * -> *
d2 :: p f -> Dict (Differentiable (D f))
-- This is just something to recover from the dictionary
make :: a -> f a
d2
принимает прокси для типа и восстанавливает словарь для принятия второй производной. Прокси-сервер позволяет нам легко указать тип d2
, о котором мы говорим. Мы можем перейти к более глубоким словарям, применив d
:
d :: Dict (Differentiable t) -> Dict (Differentiable (D t))
d d1 = withDict d1 (d2 (pt (d1)))
where
pt :: Dict (Differentiable t) -> Proxy t
pt = const Proxy
Захват диктатора
Типы полиномиальных функторов, произведения, суммы, константы и нуль, все бесконечно дифференцируемы. Мы будем определять свидетелей d2
для каждого из этих типов
data K x = K deriving (Show)
newtype I x = I x deriving (Show)
data (f :+: g) x = L (f x)
| R (g x)
deriving (Show)
data (f :*: g) x = f x :&: g x deriving (Show)
Ноль и константы не требуют каких-либо дополнительных знаний для фиксации своей производной Dict
instance Differentiable K where
type D K = K
make = const K
d2 = const Dict
instance Differentiable I where
type D I = K
make = I
d2 = const Dict
Сумма и произведение требуют словарей от обоих их производных от компонента, чтобы иметь возможность выводить словарь для своей производной.
instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
make = R . make
d2 p = withDict2 df dg $ Dict
where
df = d2 . pf $ p
dg = d2 . pg $ p
pf :: p (f :+: g) -> Proxy f
pf = const Proxy
pg :: p (f :+: g) -> Proxy g
pg = const Proxy
instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
make x = make x :&: make x
d2 p = withDict2 df dg $ Dict
where
df = d2 . pf $ p
dg = d2 . pg $ p
pf :: p (f :*: g) -> Proxy f
pf = const Proxy
pg :: p (f :*: g) -> Proxy g
pg = const Proxy
Восстановление словаря
Мы можем восстановить словарь для ограничений, которые в противном случае у нас не было бы достаточной информации для вывода. Differentiable f
обычно допускает использование только make :: a -> f a
, но не для make :: a -> D f a
или make :: a -> D (D f) a
.
make1 :: Differentiable f => p f -> a -> D f a
make1 p = withDict (d2 p) make
make2 :: Differentiable f => p f -> a -> D (D f) a
make2 p = withDict (d (d2 p)) make