我有一个函数readConfigFromEnv
,它从环境变量中读取一些环境变量。
readConfigFromEnv :: IO (Int, String)
readConfigFromEnv = do
portStr <- getEnv "PORT"
let port = read portStr
secret <- getEnv "SECRET"
return (port, secret)
即使Haskell具有出色的类型推断系统,我仍然想显式指定所需的类型,并让编译器告诉我是否有任何不合理的地方:
readConfigFromEnv :: IO (Int, String)
readConfigFromEnv = do
portStr :: String <- getEnv "PORT"
let port :: Int = read portStr
secret :: String <- getEnv "SECRET"
return (port, secret)
但是此代码无法编译。但是,编译器提示我应该添加语言扩展名ScopedTypeVariables
。我试过了。
所以我的问题是:
是否添加了ScopedTypeVariables
来正确地指定中间值类型的正确方法?
明确指定类型是一种好习惯吗?
使用ScopedTypeVariables
有副作用吗,我应该将其添加到默认语言扩展名列表中吗?
谢谢!
答案 0 :(得分:5)
如果没有ScopedTypeVariables
,您可以 为声明块中引入的变量添加类型签名,例如do块中的let
语句,let ... in ...
表达式和{ {1}}子句。不过,您在示例中猜测的语法不太正确,您需要添加单独的类型声明,而不是在变量的定义之外(与在变量的定义的单独一行上写where
的方式完全相同,而不是同时写成readConfigFromEnv :: IO (Int, String)
)。因此,您可以例如编写:
readConfigFromEnv :: IO (Int, String) = do
readConfigFromEnv :: IO (Int, String)
readConfigFromEnv = do
portStr <- getEnv "PORT"
let port :: Int
port = read portStr
secret <- getEnv "SECRET"
return (port, secret)
允许您在其他情况下无法添加的类型中添加类型注释,这有助于您声明以“声明块”以外的其他方式引入的变量的类型,例如ScopedTypeVariables
绑定在do块中,lambda绑定变量和以<-
模式引入的变量。
然而,case
的主要目的是允许类型变量具有多个类型注释的作用域;通常,即使两个不同类型注释中的变量具有相同的名称,也假定它们是不同的变量,因此,如果没有它,则有很多类型是无法编写的。基本上,在显式ScopedTypeVariables
引入的任何变量上使用ScopedTypeVariables
都会成为变量的定义,因此,在其范围内使用该类型变量名实际上是对该外部作用域变量的引用,而不是新变量。例如,考虑以下代码:
forall
在这里,类型表达式{-# LANGUAGE ScopedTypeVariables #-}
readTwoThings :: forall a. Read a => IO (a, a)
readTwoThings = do
sx <- readLn
sy <- readLn
let x, y :: a
x = read sx
y = read sy
pure (x, y)
表示x, y :: a
和x
是 类型y
的类型,出现在{ {1}}。
比较删除a
和/或将readTwoThings
换成forall a.
时发生的情况。在这种情况下,您会收到错误消息,因为ScopedTypeVariables
和ExplicitForAll
类型的a
与x
类型的y
是不同的变量
在实践中,我发现a
行为通常是您真正想要发生的事情,但是因为您可以找到类似的示例,其中相同的代码取决于是否启用了readTwoThings
,因此意味着两种不同的情况(您甚至可以构造示例,其中代码可以以任何一种方式进行编译并执行不同的操作!),您不能只在不了解代码如何影响类型签名的情况下盲目地将其打开。
现在,在具备所有背景知识之后,以下是我对您的问题列表的明确回答:
是的,如果要在声明的地方声明每个局部变量的类型,则需要ScopedTypeVariables
。
明确指定每个顶级定义的类型是一个非常普遍的建议,我想这在Haskell社区中几乎是共识。我还发现,如果将类型声明添加到ScopedTypeVariables
和ScopedTypeVariables
绑定变量中非常复杂,则会有所帮助,但是这种做法不太普遍。在let
之类的类型中添加类型根本不常见,但这确实是一个品味问题。我敢肯定,我会付出更多的努力来编写代码,而将代码修改变得比收益更令人讨厌。
我可以看到您为什么要在非常大的函数定义(特别是很长的do块)中执行此操作,但是我通常会尝试将它们分成较小的部分(顶级或使用where
子句),并且这些片段将具有类型声明。
所以我的投票是不,这不是好习惯,但这是主观意见,不是客观事实。如果对您有帮助,请不要停止尝试。
请参阅上面的所有讨论。 portStr <- getEnv "PORT"
是一个很好的扩展;默认情况下,很多人会很舒服地打开它,甚至很多人甚至希望有一天它成为基础语言的一部分。