在Haskell中,为什么eqT返回Maybe(a:〜:b)比返回Bool更好?

时间:2018-10-25 19:20:54

标签: haskell ghc gadt

我制作了eqT的变体,使我可以像其他Bool一样处理结果,以编写类似eqT' @a @T1 || eqT' @a @T2的东西。但是,尽管在某些情况下效果很好,但是我发现无法用它代替每次使用eqT。例如,我想用它来写一个readMaybe的变体,当它应该返回Just时它就是String。使用eqT时,我可以将类型保留为String -> Maybe a,而使用eqT'时,类型必须为String -> Maybe String。这是为什么?我知道我可以通过其他方式做到这一点,但是我想知道为什么这行不通。我想这与GADT的case表达式(a :~: b是GADT)的特殊处理有关,但是这种特殊处理是什么?

以下是我正在谈论的示例代码:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

import Data.Typeable
import Text.Read

eqT' :: forall a b. (Typeable a, Typeable b) => Bool
eqT' = case eqT @a @b of
    Just Refl -> True
    _ -> False

readMaybeWithBadType1 :: forall a. (Typeable a, Read a) => String -> Maybe String
readMaybeWithBadType1 = if eqT' @a @String
    then Just
    else readMaybe

readMaybeWithBadType2 :: forall a. (Typeable a, Read a) => String -> Maybe String
readMaybeWithBadType2 = case eqT' @a @String of
    True -> Just
    False -> readMaybe

readMaybeWithGoodType :: forall a. (Typeable a, Read a) => String -> Maybe a
readMaybeWithGoodType = case eqT @a @String of
    Just Refl -> Just
    _ -> readMaybe

main :: IO ()
main = return ()

更改readMaybeWithBadType的类型以返回Maybe a会导致ghc抱怨:

u.hs:16:14: error:
    • Couldn't match type ‘a’ with ‘String’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          readMaybeWithBadType1 :: forall a.
                                   (Typeable a, Read a) =>
                                   String -> Maybe a
        at u.hs:14:5-80
      Expected type: String -> Maybe a
        Actual type: a -> Maybe a
    • In the expression: Just
      In the expression: if eqT' @a @String then Just else readMaybe
      In an equation for ‘readMaybeWithBadType1’:
          readMaybeWithBadType1 = if eqT' @a @String then Just else readMaybe
    • Relevant bindings include
        readMaybeWithBadType1 :: String -> Maybe a (bound at u.hs:15:5)
   |
16 |         then Just
   |              ^^^^

u.hs:21:17: error:
    • Couldn't match type ‘a’ with ‘String’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          readMaybeWithBadType2 :: forall a.
                                   (Typeable a, Read a) =>
                                   String -> Maybe a
        at u.hs:19:5-80
      Expected type: String -> Maybe a
        Actual type: a -> Maybe a
    • In the expression: Just
      In a case alternative: True -> Just
      In the expression:
        case eqT' @a @String of
          True -> Just
          False -> readMaybe
    • Relevant bindings include
        readMaybeWithBadType2 :: String -> Maybe a (bound at u.hs:20:5)
   |
21 |         True -> Just
   |                 ^^^^

我有点理解为什么会抱怨,但是我不明白为什么在readMaybeWithGoodType中这不是问题。

3 个答案:

答案 0 :(得分:4)

从本质上讲,这是GADT与非GADT淘汰的情况。

当我们要使用值x :: T(其中T是某种代数数据类型)时,我们求助于模式匹配(又称“消除”)

case x of
  K1 ... -> e1
  K2 ... -> e2
  ...

Ki涵盖了所有可能的构造函数。

有时,我们使用其他形式的模式匹配(例如,定义方程式)来代替case,但这并不重要。另外,if then else完全等同于case of True -> .. ; False -> ...,因此无需讨论。

现在,关键点是我们要消除的类型T可能不是GADT。

如果不是GADT,则对所有分支e1,e2,...进行类型检查,并要求它们具有相同的类型。无需利用任何其他类型信息即可完成此操作。

如果我们写case eqT' @a @b of ...if eqT' @a @b then ...,我们将消除类型Bool,它不是GADT。类型检查器未获得关于a,b的信息,并且两个分支被检查为具有相同的类型(可能会失败)。

相反,如果T是GADT,则类型检查器将进一步利用类型信息。特别是,如果我们有case x :: a :~: b of Refl -> e,类型检查器将学习a~b,并在类型检查e时加以利用。

如果我们有多个分支机构

case x :: a :~: b of
   Just Refl -> e1
   Nothing   -> e2

然后,根据直觉,a~b仅用于e1

如果您想要自定义eqT',建议您尝试以下操作:

data Eq a b where
   Equal   :: Eq a a
   Unknown :: Eq a b

eqT' :: forall a b. (Typeable a, Typeable b) => Eq a b
eqT' = case eqT @a @b of
   Just Refl -> Equal
   Nothing   -> Unknown

readMaybe3 :: forall a. (Typeable a, Read a) => String -> Maybe String
readMaybe3 = case eqT' @a @String of
    Equal -> Just
    Unknown -> readMaybe

诀窍是消除GADT,它可以提供有关手头类型变量的正确信息,例如在这种情况下。

如果您想更深入一点,可以检查具有完全依存类型(Coq,Idris,Agda等)的语言,我们在依存消除和非依存消除中会发现类似的行为。这些语言比Haskell + GADT难一点-我必须警告您。我只补充说,依存消除起初对我来说是一个神秘的事物。在了解了Coq中模式匹配的一般形式之后,一切都变得很有意义了。

答案 1 :(得分:1)

您已经发现the documentation所描述的

  

要在实践中使用这种相等性,请在a:〜:b上进行pattern-match以获得Refl构造函数;在模式匹配的主体中,编译器知道a〜b。

case上的大多数Maybe a匹配项中,如果可以使用Just类型,则在a分支中还有一个附加值。同样,在Just的{​​{1}}分支中,还有一个附加值。 readMaybeWithGoodType在术语级别不是很有趣,但是在类型级别却很有趣。在这里,它向GHC传达了一个事实,即通过检查我们知道-只有且Refla时,此分支才可以访问。

您是正确的,其他GADT构造函数也可以将类型信息(通常是其参数上的类型类约束)带入范围。

答案 2 :(得分:0)

多亏了贝吉和齐,我想我理解导致GHC将错误返回给我的一系列步骤。它们都是很好的答案,但是我认为我的很多误解不是理解Haskell进行类型检查的具体步骤以及在这种情况下它与GADT模式匹配的关系。我将尽我所能写一个答案来描述这一点。

因此,首先,构成GADT的一件事是,您可以定义一个sum-type,其中每个选项可以具有比数据声明开头给出的类型更具体的不同类型。这使得以下成为可能:

data a :~: b where
  Refl :: a :~: a

因此,在这里,我们只有一个构造函数Refl,它是一个a :~: b,但更具体地说,这个特定的构造函数(尽管只有一个)产生了a :~: a。如果我们将其与Maybe组合在一起以获得类型Maybe (a :~: b),则我们有2个可能的值:Just Repl :: Maybe (a :~: a)Nothing :: Maybe (a :~: b)。这就是类型通过模式匹配传递有关类型相等性的信息的方式。

现在,要使GADT与模式匹配一​​起使用,必须做一些很酷的事情。这就是说,与每个模式匹配的表达式可能比整个模式匹配表达式(例如case表达式)更专业。 Haskell在模式匹配中对GADT进行了特殊处理,利用GADT构造函数中包含的附加类型细化功能进一步对匹配表达式所需的类型进行特殊处理。因此,当我们这样做时:

readMaybeWithGoodType :: forall a. (Typeable a, Read a) => String -> Maybe a
readMaybeWithGoodType = case eqT @a @String of
    Just Refl -> Just
    _ -> readMaybe

eqTMaybe (a :~: b)eqT @a @String,匹配的_(Typeable a, Read a) => Maybe (a :~: String),而Just ReflMaybe (String :~: String)

Haskell将要求整个case表达式是(Typeable a, Read a) => String -> Maybe a的类型超集。仅_的{​​{1}}匹配项就是类型readMaybe,它是一个超集。但是,Read a => String -> Maybe a的类型为Just,它不是超集,因为大小写表达式应包含诸如a -> Maybe a之类的内容,但是String -> Maybe Int不能返回该值,因为它需要Just。与String ~ Int匹配时就是这种情况。 GHC告知它无法与Bool Maybe String匹配,并返回要求的更通用的Just

这是在包含此类型相等性信息的构造函数上进行模式匹配很重要的地方。通过在Read a => Maybe a上进行匹配,Haskell将不需要该匹配表达式为Just Refl :: Maybe (String :~: String)的类型超集,只需要使其为(Typeable a, Read a) => String -> Maybe a的超集(原始必填类型),即为String -> Maybe String