Вывод ограничений класса разных типов в разных случаях сопоставления с шаблоном

#haskell

#haskell

Вопрос:

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

 type family F (n :: Nat) (m :: Nat) :: Constraint where
  F 0 m = ()
  F n m = (m ~ 0)
 

Тогда у меня есть функция, которая имеет это ограничение.

 f :: forall n m. (KnownNat n, KnownNat m, m ~ 0) => ()
f = ()
 

Когда я пытаюсь использовать эту функцию в сопоставлении с шаблоном, где мое семейство типов
должно создавать это ограничение, ghc сообщает, что он не может вывести ограничение

Вот пример:

 g :: forall n m. (KnownNat n, KnownNat m, F n m) => ()
g =
  case (natVal (Proxy @n)) of
    0 -> ()
    n -> f @n @m

 

Это приводит к ошибке

Could not deduce: m ~ 0 arising from a use of ‘f’
      from the context: (KnownNat n, KnownNat m, F n m)
        bound by the type signature for:
                   g :: forall (n :: Nat) (m :: Nat).
                        (KnownNat n, KnownNat m, F n m) =>
                        ()
    ‘m’ is a rigid type variable bound by
        the type signature for:
          g :: forall (n :: Nat) (m :: Nat).
               (KnownNat n, KnownNat m, F n m) =>
               ()
 • In the expression: f @n @m
      In a case alternative: n -> f @n @m
      In the expression:
        case (natVal (Proxy @n)) of
          0 -> ()
          n -> f @n @m
    |
168 |     n -> f @n @m
    |          ^^^^^^^
 

Комментарии:

1. Я тоже борюсь с проблемами ограничений. Я думаю, вы, возможно, захотите рассмотреть, как GADTs могут быть включены в ваше решение, поскольку они позволяют вам накладывать ограничения на конструктор вашего типа данных. Действительно, вы можете наложить различные ограничения на каждый конструктор.

Ответ №1:

Основная причина, по которой ваше сопоставление с шаблоном не приводит к каким-либо ограничениям, заключается в том, что вы сопоставляете регистр natVal (Proxy @n) :: Integer , который не является GADT. Согласно ответу @chi, вам необходимо сопоставить GADT, чтобы получить информацию о типе в области видимости.

Для слегка измененной версии вашего семейства типов:

 type family F (n :: Nat) (m :: Nat) :: Constraint where
  F 0 m = (m ~ 0)
  F n m = ()
 

способ, которым вы могли бы добиться этого, был бы следующим:

 f :: forall n m. (m ~ 0) => ()
f = ()

g :: forall n m. (KnownNat n, F n m) => ()
g = case sameNat (Proxy @n) (Proxy @0) of
  Just Refl -> f @n @m
  Nothing -> ()
 

Этот случай совпадает с GADT, чтобы ввести ограничение n ~ 0 в Just Refl ветку. Это позволяет разрешить семейство типов F n m m ~ 0 . Обратите внимание, что я удалил посторонние KnownNat ограничения; это несколько важно, поскольку ответ @chi показывает, что ваша проблема легко решается, если у вас есть KnownNat m ограничение, доступное в g сигнатуре типа. В конце концов, если m известно, то вы можете напрямую определить, является ли это 0 или нет, и F m n ограничение является избыточным, независимо от значений m и n .

К сожалению, для вашего исходного семейства типов с измененным условием:

 type family F (n :: Nat) (m :: Nat) :: Constraint where
  F 0 m = ()
  F n m = (m ~ 0)
 

все гораздо сложнее. Семейства типов разрешаются с помощью довольно простого «прямого решателя», из-за отсутствия лучшего термина, поэтому для вашей версии F выражение типа F n m может быть разрешено только для определенного n , например 0 , или 5 . Нет никакого ограничения, выражающего тот факт, что n это неопределенный тип, отличный 0 от того, который вы могли бы каким- то образом использовать для разрешения F n m = (m ~ 0) . Итак, вы могли бы написать:

 g :: forall n m. (KnownNat n, F n m) => ()
g = case sameNat (Proxy @n) (Proxy @1) of
      Just Refl -> f @n @m
      Nothing -> ()
 

который использует тот факт, что в Just Refl -> ветке ограничение n ~ 1 находится в области видимости, которая позволяет F n m быть разрешенной. Но, похоже, нет способа заставить его работать для произвольного ненулевого n значения.

Есть несколько подходов, которые могли бы сработать. Переход на Peano naturals решил бы эту проблему:

 data Nat = Z | S Nat
data SNat n where
  SZ :: SNat Z
  SS :: SNat n -> SNat (S n)
class KnownNat n where
  natSing :: SNat n
instance KnownNat Z where natSing = SZ
instance KnownNat n => KnownNat (S n) where natSing = SS natSing

type family F (n :: Nat) (m :: Nat) :: Constraint where
  F Z m = ()
  F (S n) m = (m ~ Z)

f :: forall n m. (m ~ Z) => ()
f = ()

g :: forall n m. (KnownNat n, F n m) => ()
g = case natSing @n of
  SZ -> ()
  SS n -> f @n @m
 

Здесь ветвь SS n case вводит в область действия ограничение n ~ S n1 , которое действительно выражает тот факт , что n является неопределенным естественным , отличным от Z , что позволяет нам разрешить семейство типов F n m = (m ~ Z) .

Вы также можете представить F ограничение, используя условие уровня типа:

 type F n m = If (1 <=? n) (m ~ 0) (() :: Constraint)
 

и писать:

 import Data.Kind
import Data.Proxy
import Data.Type.Equality
import Data.Type.Bool
import GHC.TypeLits
import Unsafe.Coerce

type F n m = If (1 <=? n) (m ~ 0) (() :: Constraint)

f :: forall n m. (m ~ 0) => ()
f = ()

g :: forall n m. (KnownNat n, F n m) => ()
g = case leqNat (Proxy @1) (Proxy @n) of
  Just Refl -> f @n @m
  Nothing -> ()

leqNat :: (KnownNat a, KnownNat b) => Proxy a -> Proxy b -> Maybe ((a <=? b) :~: True)
leqNat x y | natVal x <= natVal y = Just (unsafeCoerce Refl)
           | otherwise            = Nothing
 

Здесь функция leqNat предоставляет соответствующие доказательства на уровне типа о том, что одно значение меньше или равно другому. Вероятно, это выглядит как мошенничество, но если вы сравните это с определением sameNat , вы увидите, что это обычный вид мошенничества.