Могу ли я ограничить семейство типов?

В этот недавний мой ответ, я случайно открыл этот старый каштан (программа такая старая, половина ее была написана в семнадцатом веке Лейбницем и написанный на компьютере в семидесятых моим папой). Я не использую современный бит, чтобы сэкономить место.

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