我有以下代码,概述了布尔和算术表达式的语言:
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
赋予哪种类型的签名,以使其完整。但是,我不知道对一个类型签名进行总计意味着什么。任何见解都会受到赞赏。
答案 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
在有限输入上的任何类型正确的应用都将在有限时间内产生一个非底答案。 (这是功能“合计”的一种明智含义。)
为什么我们可以忽略Not
,And
和Greater
的情况?为什么,因为我们已要求输入的类型为Exp Int
,而外部构造函数为Not
,And
或Greater
的任何类型良好的术语实际上都将具有类型{ {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
不是这样。