Почему подстановочный знак работает, когда перечисление всех случаев не работает? - PullRequest
7 голосов
/ 02 апреля 2020

Рассмотрим этот код:

{-# LANGUAGE GADTs #-}

data P t where
 PA :: P Int
 PB :: P Double
 PC :: P Char

isA PA = True
isA _ = False

Он компилируется и работает нормально. Теперь рассмотрим этот код:

{-# LANGUAGE GADTs #-}

data P t where
 PA :: P Int
 PB :: P Double
 PC :: P Char

isA PA = True
isA PB = False
isA PC = False

Не удается скомпилировать:

Main.hs:8:10: error:
    • Couldn't match expected type ‘p’ with actual type ‘Bool’
        ‘p’ is untouchable
          inside the constraints: t ~ Int
          bound by a pattern with constructor: PA :: P Int,
                   in an equation for ‘isA’
          at Main.hs:8:5-6
      ‘p’ is a rigid type variable bound by
        the inferred type of isA :: P t -> p
        at Main.hs:(8,1)-(10,14)
      Possible fix: add a type signature for ‘isA’
    • In the expression: True
      In an equation for ‘isA’: isA PA = True
    • Relevant bindings include isA :: P t -> p (bound at Main.hs:8:1)
  |
8 | isA PA = True
  |          ^^^^

Main.hs:9:10: error:
    • Couldn't match expected type ‘p’ with actual type ‘Bool’
        ‘p’ is untouchable
          inside the constraints: t ~ Double
          bound by a pattern with constructor: PB :: P Double,
                   in an equation for ‘isA’
          at Main.hs:9:5-6
      ‘p’ is a rigid type variable bound by
        the inferred type of isA :: P t -> p
        at Main.hs:(8,1)-(10,14)
      Possible fix: add a type signature for ‘isA’
    • In the expression: False
      In an equation for ‘isA’: isA PB = False
    • Relevant bindings include isA :: P t -> p (bound at Main.hs:8:1)
  |
9 | isA PB = False
  |          ^^^^^

Main.hs:10:10: error:
    • Couldn't match expected type ‘p’ with actual type ‘Bool’
        ‘p’ is untouchable
          inside the constraints: t ~ Char
          bound by a pattern with constructor: PC :: P Char,
                   in an equation for ‘isA’
          at Main.hs:10:5-6
      ‘p’ is a rigid type variable bound by
        the inferred type of isA :: P t -> p
        at Main.hs:(8,1)-(10,14)
      Possible fix: add a type signature for ‘isA’
    • In the expression: False
      In an equation for ‘isA’: isA PC = False
    • Relevant bindings include isA :: P t -> p (bound at Main.hs:8:1)
   |
10 | isA PC = False
   |          ^^^^^

Почему? Что здесь происходит?

Редактировать: Добавление сигнатуры типа isA :: P t -> Bool заставляет ее работать, поэтому мой вопрос теперь звучит так: почему вывод типа не работает во втором случае, а в первом - так?

Ответы [ 2 ]

2 голосов
/ 04 апреля 2020

При вводе конструкции case (будь то явное выражение case или неявное определение функции на основе шаблона) при отсутствии GADT, отдельные альтернативы:

pattern -> body

можно объединить, набрав все шаблоны и объединив их с типом проверяемого, затем набрав все тела и объединив их с типом выражения case в целом. Таким образом, в простом примере, таком как:

data U = UA | UB | UC
isA1 u = case u of
  UA -> True
  UB -> False
  x  -> False

, мы можем изначально набирать шаблоны UA :: U, UB :: U, x :: a, объединять их, используя равенство типов a ~ U, чтобы вывести тип scrutinee u :: U и аналогичным образом унифицируют True :: Bool и оба False :: Bool с типом общего выражения регистра Bool, объединяя его с типом isA для получения isA :: U -> Bool.

Обратите внимание, что процесс унификации может специализироваться на типах. Здесь тип шаблона x :: a был общим, но к концу процесса объединения он был специализирован для x :: U. Это может случиться и с телами тоже. Например:

len mstr = case mstr of
  Nothing -> 0
  Just str -> length str

Здесь 0 :: Num a => a - это полиморф c, а потому что length возвращает Int, к концу процесса, тела (и, таким образом, все выражение case) ) были объединены в тип Int.

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

Ситуация меняется в присутствии ГАДЦ. Когда конструкции случая проверки типа с помощью GADT, шаблоны в альтернативе могут вводить «уточнение типа», набор дополнительных привязок переменных типа, которые будут использоваться при проверке типа тела альтернативы. (Это то, что делает ГАДЦ полезными в первую очередь.)

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

data Term a where
  Lit :: Int -> Term Int
  IsZ :: Term Int -> Term Bool
  If :: Term Bool -> Term a -> Term a -> Term a

eval :: Term a -> a
eval t = case t of
  Lit n -> n
  IsZ t -> eval t == 0
  If b t e -> if eval b then eval t else eval e

Если бы мы наивно унифицировали тела n :: Int, eval t == 0 :: Bool и if eval b then eval t else eval e :: a, программа не провела бы проверку типа ( наиболее очевидно, потому что Int и Bool не объединяются!).

В общем, поскольку уточнения типов позволяют вычисленным типам тел альтернатив быть больше конкретизировать c, чем последний тип, нет очевидного «наиболее общего» / «наименее ограничительного» типа, к которому можно объединить все тела, как это было для выражения case без GADT.

Вместо этого нам обычно нужно сделать доступным тип «target» для общего выражения case (например, для eval, тип возврата a в сигнатуре типа), а затем определить, вводится ли при каждом уточнении конструктор (например, IsZ вводит уточнение a ~ Bool), тело eval t == 0 :: Bool имеет в качестве типа связанный уточнение из a.

Если целевой тип явно не указан, то лучшее, что мы можем сделать - в общем случае - это использование переменной типа fre sh p в качестве цели и попытка проверить каждый уточненный тип по этому.

Это означает, что, учитывая следующее определение без сигнатуры типа для isA2:

data P t where
  PA :: P Int
  PB :: P Double
  PC :: P Char

isA2 = \p -> case p of
  PA -> True
  PB -> False
  PC -> False

то, что пытается сделать GH C, это тип isA2 :: P t -> p. Для альтернативы:

PA -> True

он набирает PA :: P t, давая уточнение t ~ Int, и при этом уточнении пытается набрать True :: p. К сожалению, p не является Bool ни при каком уточнении, связанном с несвязанной переменной типа a, и мы получаем ошибку. Аналогичные ошибки генерируются и для других альтернатив.

На самом деле, есть еще одна вещь, которую мы можем сделать. Если есть альтернативы, которые не вводят уточнение типа, то вычисленные типы их тел на NOT более конкретны c, чем окончательный тип. Таким образом, если мы унифицируем типы тела для «неопределяемых» альтернатив, результирующий тип обеспечивает законную цель объединения для уточненных альтернатив.

Это означает, что, например,

isA3 = \p -> case p of
  PA -> True
  x  -> False

вторая альтернатива:

x -> False

набирается путем сопоставления с шаблоном x :: P t, который не вводит уточнения типа. Неопределенный тип тела - Bool, и этот тип является подходящей целью для объединения других альтернатив.

В частности, первая альтернатива:

PA -> True

соответствует типу уточнение a ~ Int. При этом уточнении фактический тип тела True :: Bool соответствует «уточнению» целевого типа Bool (который «уточнен» до Bool), и определено, что альтернатива имеет действительный тип.

Таким образом, интуиция заключается в том, что без альтернативы с подстановочными символами предполагаемый тип для выражения case является переменной произвольного типа p, которая является слишком общей, чтобы ее можно было объединить с альтернативами уточнения типа. Однако при добавлении альтернативы подстановочного знака _ -> False в процесс объединения вводится более ограничительный тип тела Bool, который, будучи выведенным без какого-либо уточнения типа шаблоном _, может информировать алгоритм объединения, предоставляя более ограничительный тип Bool, к которому могут быть унифицированы другие, уточненные альтернативы типа.

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

Фактически, происходит то, что процесс уточнения вводит переменные fre sh в процесс объединения, который, даже когда они объединены, не влияют на контекст большего типа. Таким образом, все альтернативы объединяются одновременно, но объединение нерафинированных альтернатив влияет на более широкий контекст типа, в то время как унификация уточненных альтернатив влияет на кучу переменных fre sh, давая тот же конечный результат, как если бы нерафинированные и уточненные альтернативы были обрабатывается отдельно.

1 голос
/ 02 апреля 2020

Отказ от ответственности : Я пишу это как ответ, потому что он не помещается в комментарии. Но я могу ошибаться

Такое поведение ожидается при совпадении паттерна на GADTs. До GHC * Руководство пользователя :

Уточнение типа выполняется только на основе предоставленных пользователем аннотаций типов. Таким образом, если для eval не предоставлена ​​сигнатура типа, уточнение типа не происходит, и много непонятных сообщений об ошибках

Также из руководства пользователя:

При сопоставлении шаблонов с конструкторами данных, извлеченными из GADT, например, в выражении case, применяются следующие правила:

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

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

До этого, когда сопоставление с шаблоном GADT, вы должны предоставили сигнатуру типа (причина в том, что вывод типа затруднен для GADTs). Следовательно, по-видимому, первое определение isA не должно компилироваться, но в статье , какой вывод типа для GADTs объясняется (раздел 6.4):

Мы отметили в разделе 4.3, что в PCON-R было бы неправильно использовать любой унификатор, кроме самого общего. Но должно ли уточнение быть объединителем вообще? Например, даже если выражение case может выполнять уточнение, уточнение не требуется для проверки этой функции:

f :: Term a -> Int
f (Lit i) = i
f _       = 0 

Приведенный выше пример - именно ваш случай !. В статье это называется pre-unifier , и есть очень техническое объяснение того, как это работает, но насколько я понимаю, при написании:

data P t where
 PA :: P Int
 PB :: P Double
 PC :: P Char

isA PA = True
isA PB = False
isA PC = False

компилятор запускается выводя isA :: P t -> p и отказываясь продолжать, потому что переменные типа не являются жесткими (то есть не определены пользователем)

, тогда как при записи:

data P t where
 PA :: P Int
 PB :: P Double
 PC :: P Char

isA PA = True
isA _  = False

компилятор может сделать вывод, что вывод любого типа будет менее общим, чем вывод Bool в качестве возвращаемого типа, следовательно, он может безопасно вывести isA :: P t -> Bool.

Возможно, это кажется вам столь же неясным, как и мне, но наверняка в двух случаях вы запрос действительно задокументирован, так что, вероятно, это желаемое поведение для GHC разработчиков, а не странная ошибка.

...