Haskell 和其他函数式语言使用let 有重要原因。我将尝试逐步描述它们:
类型变量的量化
Haskell 和其他函数式语言中使用的Damas-Hindley-Milner type system 允许多态类型,但类型量词只允许在给定类型表达式的前面。例如,如果我们写
const :: a -> b -> a
const x y = x
那么const的类型是多态的,它被隐式地普遍量化为
∀a.∀b. a -> b -> a
和const 可以专门化为我们通过用两个类型表达式替换a 和b 获得的任何类型。
但是,类型系统不允许在类型表达式中使用量词,例如
(∀a. a -> a) -> (∀b. b -> b)
这种类型在System F 中是允许的,但是类型检查和类型推断是不可判定的,这意味着编译器将无法为我们推断类型,我们必须用类型显式注释表达式。
(很长一段时间以来,系统 F 中类型检查的可判定性问题一直是开放的,有时它被称为“一个令人尴尬的开放性问题”,因为除此之外的许多其他系统都证明了不可判定性,直到 1994 年被乔·威尔斯证明。)
(GHC 允许您使用 RankNTypes 扩展启用此类显式内部量词,但如前所述,无法自动推断类型。)
lambda 抽象的类型
考虑表达式λx.M,或者用Haskell表示法\x -> M,
其中M 是包含x 的某个术语。如果x的类型是a,M的类型是b,那么整个表达式的类型就是λx.M : a → b。由于上述限制,a 不能包含 ∀,因此 x 的类型不能包含类型量词,它不能是多态的(或者换句话说它必须是 单态)。
为什么 lambda 抽象还不够
考虑这个简单的 Haskell 程序:
i :: a -> a
i x = x
foo :: a -> a
foo = i i
让我们暂时忽略foo 不是很有用。重点是foo定义中的id是用两种不同的类型实例化的。第一个
i :: (a -> a) -> (a -> a)
第二个
i :: a -> a
现在,如果我们尝试将此程序转换为没有let 的纯 lambda 演算语法,我们最终会得到
(λi.i i)(λx.x)
其中第一部分是foo 的定义,第二部分是i 的定义。但是这个词不会进行类型检查。问题是i 必须具有单态类型(如上所述),但我们需要它是多态的,以便我们可以将i 实例化为两种不同的类型。
确实,如果您尝试在 Haskell 中键入 i -> i i,它将失败。没有可以分配给i 的单态类型,以便i i 进行类型检查。
let 解决问题
如果我们写let i x = x in i i,情况就不同了。与上一段不同,这里没有 lambda,没有像 λi.i i 这样的自包含表达式,我们需要抽象变量 i 的多态类型。因此let 可以允许i 具有多态类型,在这种情况下∀a.a → a 和i i 类型检查。
如果没有let,如果我们编译一个 Haskell 程序并将其转换为单个 lambda 项,则必须为每个函数分配一个单态类型!这将毫无用处。
所以let 是一种基本结构,它允许基于 Damas-Hindley-Milner 类型系统的语言中的多态。