假设一个简单的类型类约束签名:
f :: (Eq a, Num b) => a -> b
f str = 4
我想知道为什么这些没有用
f :: (Eq a) -> (Num b)
f str = 4
f :: Eq -> Num
f str = 4
我知道类型类的类型为* -> Constraint
,而类型签名只接受*
种。
但我的问题是为什么会有这种限制?为什么不能像类型一样使用类型类?允许使用像类型这样的类型类的能力有哪些优点和缺点?
答案 0 :(得分:7)
(如果我们忽略未装箱的类型)只有一种类型实际上有任何值:*
。所有其他类型都不包含类型,但只是"类型级实体"。编译器可以使用这些来确定要做什么与实际类型及其值,但它永远不可能在运行时具有"类型"的值。有些像* -> Constraint
。
*
类型的值类型只是游戏规则。这是一件好事,出于同样的原因,拥有一个强大的静态类型系统可以防止在运行时进行无意义的转换。 Wat?? 或者,让我们从字面上理解,出于同样的原因,你不能只是将你的王扯到你的棋子上,无论这个特征有多么吸引人。你遇到的特殊情况。
如果某些扩展程序确实允许从非*
- 种类型,特别是* -> Constraint
中创建值,我们需要一大堆非显而易见的定义来明确这些"类值"实际上应该被使用。可能,它将等于包含班级的记录类型'方法作为字典。但是,究竟是什么......规范几乎是一场噩梦。这种方式使语言本身复杂化并不值得花费,因为 1。使用类型类的标准方法对于所有应用程序的至少95%和 2都很好。 当你需要具体化的类型类时,你可以很容易地使用GADT,ConstraintKinds甚至普通的旧手动定义的字典记录。这些都不需要扭曲语言如何对待价值观的基本观点,如非*
类型那样。
无论如何......让我们探索一下这个可能的工作方式。有一件事是肯定的:它会不允许你写任何像f str = 4
这样简单的东西!
考虑
f1 :: forall a, b . Eq a -> Num b
Eq a, Num b :: Constraint
,我们都有类型Constraint
的值。这基本上是给定实例的特定方法字典。所以f1
的实现必须看起来像
f1 (EqDict (d_eq :: a -> a -> Bool))
= NumDict { dict_fromInteger = ??? :: Integer -> b
, dict_plus = ??? :: b -> b -> b
, ...
, dict_signum = ??? :: b -> b
}
显然,没有有意义的方法来定义结果中的所有方法。你可以用这样一个"类功能"是"项目"从较强的阶级到较弱的阶级。例如。你可以定义
monadApp :: forall m . Monad m -> Applicative m
monadApp (MonadDict {dict_return = d_ret, dict_bind = d_bd})
= ApplicativeDict { dict_pure = d_ret
, dict_app = \fs vs -> d_bd fs (\f -> d_bd vs $ d_ret . f) }
事实上,具体的一点是有用的,但只是因为Monad
(still, but not for long!)缺少Applicative
作为超级类而应该只具有它。通常情况下,没有必要明确"降级"任何类,因为超类关系(或tuple-ConstraintKinds)会自动执行此操作。
答案 1 :(得分:4)
类型变量在类型签名中可能会出现多次:
f :: Eq a => a -> a -> a -> Bool
将上述内容写为
f :: Eq a -> Eq a -> Eq a -> Bool
看起来很不方便。更糟糕的是,a
上可能有多个约束:
g :: (Show a, Eq a) => a -> Bool
我们如何用替代符号写这个?
g :: (Show & Eq) a -> Bool -- ??
如您在上一个示例中所建议的那样完全忘记a
会签名
含糊不清:考虑
h1 :: (Eq a) => a -> a -> a -> a -> Bool
h2 :: (Eq a, Eq b) => a -> a -> b -> b -> Bool
这些是完全不同的签名:您可以拨打h2 1 2 [1] [2]
但不能h1 1 2 [1] [2]
因为后者要求四个参数属于同一类型。
在使用提议的约定之后,它们被简化为相同的签名:
h12 :: Eq -> Eq -> Eq -> Eq -> Bool
通话h12 1 2 [1] [2]
有效吗?上面的签名太模糊不清楚了。