Как я могу объединить это семейство закрытых типов с классом зависимых типов

Моя проблема

У меня есть следующее семейство типов, которое отделяет аргументы от функции:

type family
  SeparateArgs
    ( a :: Type )
      :: ( Type, [Type] )
  where
    SeparateArgs (a -> b) =
      SndCons2 a (SeparateArgs b)
    SeparateArgs a =
      '(a, '[])

У меня также есть этот класс типов, чтобы сделать наоборот:

class Refunct args goal newSig | args goal -> newSig where
  refunct :: (HList args -> goal) -> newSig
instance Refunct '[] goal goal where
  refunct makeA = makeA HNil
instance
  ( Refunct tailArgs goal c
  )
    => Refunct (headArg ': tailArgs) goal (headArg -> c)
  where
    refunct hFunct x = refunct $ hFunct . (x :>)

Практически каждый раз, когда я использую эти два, я использую их вместе, как показано ниже:

instance
  ( SeparateArgs a ~ '(goal, args)
  , Refunct goal args a
  , ...
  )
    => ...

Это сделано для того, чтобы я мог прервать аргументы, произвести некоторую обработку, чтобы создать функцию типа HList args -> goal, а затем снова превратить ее в обычную функцию.

Это работает, но это довольно неприятно, поскольку я знаю, что Separate a ~ '(goal, args) => Refunct args goal a, что означает, что требуется только одно из этих утверждений. Однако компилятор не может этого сказать, поэтому мне нужно сообщить об этом.

Это, конечно, не критично, так как мой код сейчас работает, но я бы хотел свести его в один оператор. В идеале, добавив вторую функциональную зависимость к Refunct следующим образом:

class
  ( SeparateArgs newSig ~ '(goal, args)
  )
    => Refunct args goal newSig
  | args goal -> newSig
  , newSig -> args goal
  where
    refunct :: (HList args -> goal) -> newSig

(Конечно, это не работает само по себе)

Есть ли способ свести эти два (семейство типов SeparateArgs и класс типов Refunct) в одно ограничение? Я все еще готов определять дополнительные конструкции, я просто хотел бы, чтобы в результате были неизбыточные ограничения. Мне еще понадобится функция refunct.

Если это необходимо, вот моя реализация HList:

data HList (a :: [ Type ]) where
  HNil :: HList '[]
  (:>) :: a -> HList b -> HList (a ': b)

hHead :: HList (a ': b) -> a
hHead (a :> _) = a

hTail :: HList (a ': b) -> HList b
hTail (_ :> b) = b

Что я пробовал

После обсуждения этого в другом месте было предложено попробовать:

type family
  IsAtomic
    ( a :: Type )
      :: Bool
  where
    IsAtomic (a -> b) = 'False
    IsAtomic a = 'True

class
  Refunct args goal newSig
  | args goal -> newSig
  , newSig -> args goal
  where
    refunct :: (HList args -> goal) -> newSig
instance
  ( IsAtomic goal ~ 'True
  )
    =>  Refunct '[] goal goal
  where
  refunct makeA = makeA HNil
instance
  ( Refunct tailArgs goal c
  , IsAtomic (headArg -> c) ~ 'False
  )
    => Refunct (headArg ': tailArgs) goal (headArg -> c)
  where
    refunct hFunct x = refunct $ hFunct . (x :>)

Здесь мы добавляем дополнительное ограничение, согласно которому первый экземпляр работает только если IsAtomic goal ~ 'True, а второй - только если IsAtomic goal ~ 'False, где IsAtomic - это определенное мною семейство типов, которое 'False для функций и 'True для всего остального.

Здесь компилятор, похоже, не может подтвердить, что два экземпляра не нарушают функциональную зависимость. Точная ошибка:

    Functional dependencies conflict between instance declarations:
      instance (IsAtomic goal ~ 'True) => Refunct '[] goal goal
      instance (Refunct tailArgs goal c, IsAtomic goal ~ 'False) =>
               Refunct (headArg : tailArgs) goal (headArg -> c)
    |
XXX |   ( IsAtomic goal ~ 'True
    |   ^^^^^^^^^^^^^^^^^^^^^^^...

(хорошо, это не совсем так, поскольку я удалил всю идентифицирующую информацию).

Моя интуиция заключается в том, что он не осознает, что IsAtomic goal ~ 'True и IsAtomic goal ~ 'False не могут быть истинными одновременно. Это разумно, поскольку без проверки мы не можем знать, что IsAtomic не является forall a. a, который удовлетворяет обоим ограничениям.


person Éamonn Olive    schedule 20.11.2019    source источник
comment
Боковое примечание: синтаксис * устарел. import Data.Kind и используйте вместо этого Type.   -  person HTNW    schedule 21.11.2019
comment
@HTNW Спасибо! Я обычно так и делаю, мне просто нужны минимальные зависимости для моего примера. Я изменил его, так как он понятнее.   -  person Éamonn Olive    schedule 21.11.2019


Ответы (1)


Что мы хотим?

Чтобы решить эту проблему, мы можем сначала разбить то, что мы хотим.

Мы хотим «провалиться» в поведении закрытого семейства типов (чтобы функции и нефункции соответствовали разным экземплярам), но мы также хотим конструировать данные как класс типа (чтобы мы могли получить refunct).

То есть нам нужен типовой класс с логикой близкого семейства типов. Итак, для этого мы можем просто разделить две части и реализовать их по отдельности; логика как замкнутое семейство типов, а все остальное как типовой класс.

Теперь для этого мы берем наше семейство типов и добавляем еще один параметр

class
  Foo
    (bar :: Type)
    (baz :: Type)
    (bax :: Type)

становится

class
  Foo'
    (flag :: Flag)
    (bar :: Type)
    (baz :: Type)
    (bax :: Type)

Этот параметр будет действовать как флаг, сообщающий нам, какой экземпляр использовать. Поскольку это своего рода Flag, нам нужно создать этот тип данных. У него должен быть конструктор для каждого экземпляра (в некоторых случаях мы можем немного потерять его, но в целом вам понадобится один к одному)

data Flag = Instance1 | Instance2 | Instance3 ...

(В случае моей проблемы, поскольку мы используем только два экземпляра Bool)

Теперь мы создаем семейство закрытых типов, которое вычисляет, какой экземпляр должен соответствовать. Он должен взять соответствующие аргументы из параметров Foo и создать Flag

type family
  FooInstance
    (bar :: Type)
    (baz :: Type)
    (bax :: Type)
      :: Flag
  where
    FooInstance ... = Instance1
    FooInstance ... = Instance2
    FooInstance ... = Instance3
    ...

В случае рассматриваемого вопроса мы называем это IsAtomic, поскольку это имя описывает то, что он делает.

Теперь мы модифицируем наши экземпляры, чтобы они соответствовали правильным Flags. Это довольно просто, мы просто добавляем флаг экземпляра в объявление:

instance
  ( Foo newBar newBaz newBax
  ...
  )
    => Foo' 'Instance3 foo bar baz bax
  where
    ...

Важно отметить, что мы не меняем рекурсивные вызовы Foo на вызовы Foo'. Мы собираемся создать Foo как оболочку для Foo', которая управляет нашим семейством закрытых типов (FooInstance в данном случае), поэтому мы хотим вызывать Foo, чтобы не вызывать каждый раз одну и ту же логику.

Это построено так:

class
  Foo
    (bar :: Type)
    (baz :: Type)
    (bax :: Type)
  where
    ...
instance
  ( Foo' (FooInstance bar baz bax) bar baz bax
  )
    => Foo bar baz bax
  where
    ...

Если мы хотим быть в большей безопасности, мы можем добавить строку к каждому экземпляру Foo', чтобы проверить, что он вызывается правильно:

instance
  ( Foo newBar newBaz newBax
  , FooInstance bar baz baz ~ 'Instance3
  ...
  )
    => Foo' 'Instance3 bar baz bax
  where
    ...

Мое решение

Итак, теперь мы используем эту стратегию для решения конкретного вопроса. Вот код полностью. Соответствующий класс SeparateArgs:

type family
  IsAtomic
    ( a :: Type )
      :: Bool
  where
    IsAtomic (a -> b) = 'False
    IsAtomic a = 'True

class
  SeparateArgs
    (args :: [Type])
    (goal :: Type)
    (newSig :: Type)
  | args goal -> newSig
  , newSig -> args goal
  where
    refunct :: (HList args -> goal) -> newSig

instance
  ( IsAtomic newSig ~ isAtomic -- For (possible? compilation time) speedup
  , SeparateArgs' isAtomic args goal newSig
  )
    => SeparateArgs args goal newSig
  where
    refunct = refunct' @isAtomic

class
  SeparateArgs'
    (isAtomic :: Bool)
    (args :: [Type])
    (goal :: Type)
    (newSig :: Type)
  | args goal -> newSig isAtomic
  , newSig isAtomic -> args goal
  where
    refunct' :: (HList args -> goal) -> newSig
instance
  ( IsAtomic goal ~ 'True -- Only exists to ensure we are not invoking this in an illegal manner
  )
    => SeparateArgs' 'True '[] goal goal
  where
    refunct' makeA = makeA HNil
instance
  ( IsAtomic (headArg -> c) ~ 'False -- Only exists to ensure we are not invoking this in an illegal manner
  , SeparateArgs tailArgs goal c
  )
    => SeparateArgs' 'False (headArg ': tailArgs) goal (headArg -> c)
  where
    refunct' hFunct x = refunct $ hFunct . (x :>)
person Éamonn Olive    schedule 21.11.2019