Haskell:类型签名总和是什么意思?

时间:2019-08-02 11:50:18

标签: haskell functional-programming

我有以下代码,概述了布尔和算术表达式的语言:

data Exp a where
 Plus :: Exp Int -> Exp Int -> Exp Int
 Const :: (Show a) => a -> Exp a 
 Not :: Exp Bool -> Exp Bool
 And :: Exp Bool -> Exp Bool -> Exp Bool
 Greater :: Exp Int -> Exp Int -> Exp Bool

以下是仅计算算术表达式的函数的代码:

evalA (Plus a b) = evalA a + evalA b
evalA (Const a) = a 

我试图弄清楚应该为evalA赋予哪种类型的签名,以使其完整。但是,我不知道对一个类型签名进行总计意味着什么。任何见解都会受到赞赏。

2 个答案:

答案 0 :(得分:5)

另一个答案说明“合计”是函数的属性,而不是类型签名。然后继续说,如果您希望函数总计,则必须涵盖GADT的其他构造函数。但这还不是全部。

真正的故事是,对于具有高级类型系统(如Haskell)的语言,“总计”是函数和类型签名之间的关系。因此,它确实不是类型签名的属性(说“这种类型签名是总计”是没有道理的);但是它也不是函数的属性(单独说“这个函数是合计的!! 1 ”是没有意义的。)

现在,让我们回到您的问题。你说:

data Exp a where
 Plus :: Exp Int -> Exp Int -> Exp Int
 Const :: (Show a) => a -> Exp a 
 Not :: Exp Bool -> Exp Bool
 And :: Exp Bool -> Exp Bool -> Exp Bool
 Greater :: Exp Int -> Exp Int -> Exp Bool

evalA (Plus a b) = evalA a + evalA b
evalA (Const a) = a 

基于我们的最新理解,我们现在可以提出一个新的更好,更精确的问题,即:是否存在evalA的类型签名,当与该实现配对时,会导致配对总数?这个更好的问题的答案是,这与另一个答案中的说法相反,该说法要求您必须执行更多evalA的情况。特别是如果我们写

evalA :: Exp Int -> Int
evalA (Plus a b) = evalA a + evalA b
evalA (Const a) = a

然后,evalA在有限输入上的任何类型正确的应用都将在有限时间内产生一个非底答案。 (这是功能“合计”的一种明智含义。)

为什么我们可以忽略NotAndGreater的情况?为什么,因为我们已要求输入的类型为Exp Int,而外部构造函数为NotAndGreater的任何类型良好的术语实际上都将具有类型{ {1}} -因此该应用程序将无法正确键入。因此,这可能会因模式匹配错误而导致崩溃,这可能会让您担心!

1 一个可以说“该函数,给定类型检查的任何类型签名,总计”。实际上,通常说“此功能是全部功能”作为表示该功能的方便快捷方式。另一个答案显示了无论给出哪种(正确的)类型签名,如何使您的函数总合。

答案 1 :(得分:4)

类型签名不能为“总计”或“非总计”。充其量,使用这样的术语,有人可以引用声称总是返回结果(非终止除外)的类型:

foo :: .. -> .. -> Result

与将结果包装在Maybe中的类型相反,或者类似于表示结果可能根本不存在的类型:

foo :: .. -> .. -> Maybe Result

这是术语的延伸,我不会那样使用。

无论如何,您提到的Exp a类型是GADT,它是Haskell的一项高级功能。它允许您定义

evalA :: Exp a -> a
evalA (Plus a b) = evalA a + evalA b
evalA (Const a) = a 
-- you should cover the other cases as well here

不要求您使用Maybe或类似的东西来包装返回类型,这与常规代数类型一样。


让我们考虑一个更简单的示例:仅包含整数和布尔文字的语言。

data Exp where
  I :: Int -> Exp
  B :: Bool -> Exp

现在,如果不使用一些丑陋的技巧就无法定义semExpInt :: Exp -> Int

semExpInt :: Exp -> Int
semExpInt (I i) = i                      -- OK!
semExpInt (B b) = error "not an Int!"    -- ugly!

在后一种情况下,我们需要引发运行时错误,无法终止或返回任意整数。本质上,我们在Exp内发现了一个“运行时类型错误”,它表示错误类型的值(Bool而不是Int)。

如果我们尝试semExpBool :: Exp -> Bool,则会遇到类似的问题。

我们可以并且应该使用Maybe报告错误:

semExpInt :: Exp -> Maybe Int
semExpInt (I i) = Just i   -- OK
semExpInt (B b) = Nothing  -- OK, no result here

这很好,但不方便。我们仍然以某种方式(Nothing报告“表达式中的运行时错误”。如果我们可以避免这种情况,那就最好通过输入一个我们知道正确类型的表达式作为输入。借助GADT,我们可以编写

data Exp t where
  I :: Int -> Exp Int
  B :: Bool -> Exp Bool

semExpInt :: Exp Int -> Int
semExpInt (I i) = i     -- no other cases to handle!

semExpBool :: Exp Bool -> Bool
semExpBool (B b) = b     -- no other cases to handle!

或者更好的是,我们可以将两个功能合而为一:

semExp :: Exp t -> t
semExp (I i) = i
semExp (B b) = b

在这里,我们声称结果类型恰好是输入类型t所携带的类型Exp t。因此,该函数将根据输入类型返回一个Int或一个Bool

这在将运算符添加到表达式中时更加方便。例如,

data Exp where
  I :: Int -> Exp
  B :: Bool -> Exp
  And :: Exp -> Exp -> Exp

允许使用And (B True) (B False),这很好,但也允许And (I 2) (B False)荒谬,因为And仅应用于布尔值。 这必须用语义来处理:

semExpBool :: Exp -> Maybe Bool
semExpBool (I i) = Nothing
semExpBool (B b) = Just b
semExpBool (And e1 e2) = case (semExpBool e1, semExpBool e3) of
   (Just b1, Just b2) -> Just (b1 && b2)
   _                  -> Nothing          -- some arg was not a bool!

对于GADT,我们可以这样表达:

data Exp t where
  I :: Int -> Exp Int
  B :: Bool -> Exp Bool
  And :: Exp Bool -> Exp Bool -> Exp Bool

现在,And (I 2) (B False)被禁止,因为And需要一个Exp Bool参数,而I 2不是这样。