我很好奇有经验的 Haskell 程序员在实践中真正使用类型推断的频率。我经常看到它被称赞为优于某些其他语言所需的始终显式声明的优势,但出于某种原因(也许只是因为我是新手),几乎一直“感觉”编写类型签名是正确的。 .而且我敢肯定在某些情况下它确实是必需的。
一些有经验的 Haskellers(Haskellites?Haskellizers?)可以提供一些输入吗?
我很好奇有经验的 Haskell 程序员在实践中真正使用类型推断的频率。我经常看到它被称赞为优于某些其他语言所需的始终显式声明的优势,但出于某种原因(也许只是因为我是新手),几乎一直“感觉”编写类型签名是正确的。 .而且我敢肯定在某些情况下它确实是必需的。
一些有经验的 Haskellers(Haskellites?Haskellizers?)可以提供一些输入吗?
即使您编写类型签名,这仍然是一个优势,因为编译器会在您的函数中捕获类型错误。我通常也编写类型签名,但在您实际定义新符号但不觉得需要指定类型签名的地方,如where
or子句中省略它们。let
愚蠢的例子,用一种奇怪的方式计算数字的平方:
squares :: [Int]
squares = sums 0 odds
where
odds = filter odd [1..]
sums s (a:as) = s : sums (s+a) as
square :: Int -> Int
square n = squares !! n
odds
如果编译器不会自动推断它们,并且sums
是需要类型签名的函数。
此外,如果您像往常一样使用泛型函数,则类型推断可以确保您真正以有效的方式将所有这些泛型函数组合在一起。如果你在上面的例子中说
squares :: [a]
squares = ...
编译器可以推断出这种方式无效,因为使用的函数之一(odd
标准库中的函数)需要a
在类型 classIntegral
中。在其他语言中,您通常只能在以后才认识到这一点。
如果在 C++ 中将其编写为模板,则在非整数类型上使用该函数时会出现编译器错误,但在定义模板时不会。这可能会让人非常困惑,因为您并不能立即清楚您哪里出错了,您可能需要查看一长串错误消息才能找到问题的真正根源。在 python 之类的东西中,您会在运行时在某个意外点收到错误,因为某些东西没有预期的成员函数。在更松散类型的语言中,您可能不会收到任何错误,而只会收到意想不到的结果。
在 Haskell 中,编译器可以确保可以使用其签名中指定的所有类型调用该函数,即使它是对满足某些约束的所有类型(也称为类型类)有效的通用函数。这使得以通用方式编程和使用通用库变得容易,而在其他语言中很难做到这一点。即使您指定了泛型类型签名,编译器中仍然会进行大量类型推断,以找出每次调用中使用的特定类型以及该类型是否满足函数的所有要求。
我总是为顶级函数和值编写类型签名,但不为“where”、“let”或“do”子句中的内容编写类型签名。
首先,顶级函数一般都是导出的,而 Haddock 需要一个类型声明来生成文档。
其次,当你犯了错误时,如果编译器有可用的类型信息,那么编译器错误就更容易解码。事实上,有时在一个复杂的“where”子句中我会得到一个难以理解的类型错误,所以我添加了临时类型声明来查找问题,有点像 printf 调试的类型级等价物。
所以为了回答最初的问题,我经常使用类型推断,但不是 100% 的时间。
你有很好的直觉。因为它们由编译器检查,所以顶级值的类型签名提供了宝贵的文档。
像其他人一样,我几乎总是为顶级函数添加类型签名,而几乎从不为任何其他声明添加类型签名。
另一个非常重要的地方类型推断是在交互式循环中(例如,使用 GHCi)。当我设计和调试一些花哨的新高阶函数或类似函数时,这种技术最有用。
当您遇到类型检查错误时,尽管 Haskell 编译器确实提供了有关错误的信息,但这些信息可能很难解码。为了更容易,您可以注释掉函数的类型签名,然后查看编译器对该类型的推断,并查看它与您的预期类型有何不同。
另一个用途是当您在顶级函数中构建“内部函数”但您不确定如何构建内部函数甚至它的类型应该是什么时。您可以做的是将内部函数作为参数传递给顶级函数,然后向 ghci 询问类型级函数的类型。这将包括内部函数的类型。然后,您可以使用像 Hoogle 这样的工具来查看该函数是否已存在于库中。