【问题标题】:in haskell, why do I need to specify type constraints, why can't the compiler figure them out?在haskell中,为什么我需要指定类型约束,为什么编译器不能弄清楚它们?
【发布时间】:2011-01-04 05:43:28
【问题描述】:

考虑函数,

add a b = a + b

这行得通:

*Main> add 1 2
3

但是,如果我添加一个类型签名,指定我想添加相同类型的东西:

add :: a -> a -> a
add a b = a + b

我收到一个错误:

test.hs:3:10:
    Could not deduce (Num a) from the context ()
      arising from a use of `+' at test.hs:3:10-14
    Possible fix:
      add (Num a) to the context of the type signature for `add'
    In the expression: a + b
    In the definition of `add': add a b = a + b

所以 GHC 显然 可以 推断出我需要 Num 类型约束,因为它只是告诉我:

add :: Num a => a -> a -> a
add a b = a + b

工作。

为什么 GHC 要求我添加类型约束?如果我在做泛型编程,为什么它不能只适用于知道如何使用+ 运算符的任何东西?

在 C++ 模板编程中,您可以轻松做到这一点:

#include <string>
#include <cstdio>

using namespace std;

template<typename T>
T add(T a, T b) { return a + b; }

int main()
{
    printf("%d, %f, %s\n",
           add(1, 2),
           add(1.0, 3.4),
           add(string("foo"), string("bar")).c_str());
    return 0;
}

编译器计算出add 的参数类型,并为该类型生成函数的版本。 Haskell 的方法似乎有根本的不同,你能描述一下,并讨论取舍吗?在我看来,如果 GHC 简单地为我填写类型约束,它就会得到解决,因为它显然决定它是需要的。不过,为什么要使用类型约束呢?只要该函数仅在参数位于Num 中的有效上下文中使用,为什么不直接编译成功?

【问题讨论】:

  • 为什么它应该通过添加一个约束当你明确告诉它你不需要约束(通过声明add :: a -&gt; a -&gt; a)来神奇地将类型签名变成不那么通用的东西?另请注意,在 Haskell 中,没有 (+) 重载但不是 Num 的实例(因为 (+)Num 中,所以要重载它,您必须声明一个 Num 实例)。

标签: haskell type-inference typeclass type-systems


【解决方案1】:

如果您不想指定函数的类型,只需将其省略,编译器将自动推断类型。但如果您选择指定类型,它们必须正确且准确。

【讨论】:

  • 我认为你在这里一针见血。如果您想指定类型,那么您不希望编译器忽略您的规范(或“更正”它)
【解决方案2】:

类型的全部意义是有一个正式的方式来声明使用函数的正确和错误方式。 (Num a) =&gt; a -&gt; a -&gt; a 类型准确地描述了参数的要求。如果您省略了类约束,您将拥有一个更通用的函数,可以(错误地)在更多地方使用。

这不仅会阻止您将非Num 值传递给add。函数走到哪里,类型肯定会走到哪里。考虑一下:

add :: a -> a -> a
add a b = a + b
foo :: [a -> a -> a]
foo = [add]
value :: [String]
value = [f "hello" "world" | f <- foo]

您希望编译器拒绝这个,对吗?它是如何做到的?通过添加类约束,并检查它们是否没有被删除,即使你没有直接命名函数。

C++ 版本有什么不同?没有类限制。编译器将intstd::string 替换为T,然后尝试编译生成的代码并查找它可以使用的匹配+ 运算符。模板系统“更宽松”,因为它接受更多无效程序,这是它在编译之前是一个单独阶段的症状。我喜欢修改 C++ 以添加来自 Java 泛型的 &lt;? extends T&gt; 语义。只需学习类型系统并认识到参数多态性比 C++ 模板“更强”,即它会拒绝更多无效程序。

【讨论】:

  • 我仍然不明白为什么编译器无法推断出要在第 2 行添加的限制,将其传播到第 4 行的 foo 并在第 6 行检测到错误。它可能不是很有用,但在技术上是可行的,对吧?
  • @adamax:是的,这在技术上是可行的,但是我如何向编译器实际断言函数具有不受约束的类型a -&gt; a -&gt; a?这是一个完全有意义的类型,如果我告诉编译器我的函数有这样的类型,我真的不希望它默默地对自己说“好吧,他说“a -> a -> a” ,但我知道他实际上是指 Foo a => a -> a -> a"。如果它对自己这么说,我希望它告诉我我错了,因为我曾经
  • mokus:您的评论比任何答案都更有说服力。 “如果它对自己这么说,我希望它告诉我我错了,因为我错了。”谢谢,这真的很清楚。
【解决方案3】:

我认为您可能会被 GHC 错误消息的“疯狂月亮诗”所迷惑。这并不是说 it (作为 GHC)不能推断出 (Num a) 约束。这是说(Num a) 约束不能从your 类型签名中推断出来,它知道使用+ 必须存在该类型签名。因此,您是在说这个函数的类型比编译器知道的更通用。编译器不希望你向全世界谎报你的函数!

在您给出的第一个示例中,没有类型签名,如果您在 ghci 中运行 :t add,您会看到编译器完全知道存在 (Num a) 约束。

至于 C++ 的模板,请记住它们是语法模板,并且仅在使用时在每个实例中进行完全类型检查。您的add 模板适用于任何类型,只要在每个使用它的地方都有一个合适的+ 运算符,也许还有转换,以使模板实例可行。在此之前无法对模板做出任何保证......这就是为什么模板的主体必须对使用它的每个模块“可见”的原因。

基本上,C++ 所能做的就是验证模板的语法,然后将其作为一种非常卫生的宏来保留。而 Haskell 为 add 生成了一个真正的函数(撇开它可能会选择生成特定于类型的特化进行优化)。

【讨论】:

    【解决方案4】:

    在某些情况下,编译器无法找出适合您的类型以及需要您帮助的地方。考虑

    f s = show $ read s
    

    编译器说:

    Ambiguous type variable `a' in the constraints:
    Read a' arising from a use of `read' at src\Main.hs:20:13-18
    `Show a' arising from a use of `show' at src\Main.hs:20:6-9
    Probable fix: add a type signature that fixes these type variable(s)
    

    (很奇怪,好像可以在ghci中定义这个函数,但是好像没办法实际使用)

    如果您希望 f "1" 这样的东西起作用,您需要指定类型:

    f s = show $ (read s :: Int) 
    

    【讨论】:

    • GHCi 使用比 Haskell 标准官方指定的更宽松的“默认”规则。在这种情况下,它将选择read :: String -&gt; (),因此您可以在字符串“()”上使用f。任何其他字符串都将无法解析。 (编辑:好吧,除了 read 的 () 实例接受的其他字符串,例如“()”)
    • 感谢您的澄清!
    • 由于 ghc 版本 8 可以使用TypeApplications 来注释read . show @Int 之类的函数,这样就可以在pointfree 之上编写函数f
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-26
    • 2011-01-15
    • 1970-01-01
    • 2013-05-28
    • 1970-01-01
    相关资源
    最近更新 更多