我应该首先提一下,我对Haskell很新。是否有特殊原因要将let
表达式保留在Haskell中?
我知道Haskell摆脱了对应于rec
语句的Y-combinator部分的let
关键字,表明它是递归的。他们为什么不完全摆脱let
陈述?
如果他们这样做,陈述在某种程度上似乎会更加迭代。例如,像:
let y = 1+2
z = 4+6
in y+z
就是:
y = 1+2
z = 4+6
y+z
对于熟悉函数式编程的人来说,哪个更易读,更容易阅读。我能想到保持它的唯一原因是这样的:
aaa = let y = 1+2
z = 4+6
in y+z
在没有let
的情况下会看到这个,我认为最终会出现含糊不清的语法:
aaa =
y = 1+2
z = 4+6
y+z
但是如果Haskell没有忽略空格,并且代码块/范围与Python类似,那么它是否能够删除let
?
是否有更强的理由来保持let
?
很抱歉,如果这个问题看起来很愚蠢,我只是想了解更多关于它为什么存在的问题。
答案 0 :(得分:9)
从语法上讲,您可以轻松想象没有let
的语言。如果我们想要的话,我们可以立即依靠where
在Haskell中生成这个。除此之外还有许多可能的语法。
从语义上讲,你可能会认为let可以转化为类似的东西
let x = e in g ==> (\x -> g) e
实际上,在运行时这两个表达式是相同的(模递归绑定,但可以用fix
实现)。但是,传统上,let
具有特殊的输入语义(以及where
和顶级名称定义......所有这些都是let
)的语法糖。
特别是在构成Haskell基础的Hindley-Milner型系统中,存在let
- 概括的概念。直观地说,它将我们将函数升级到最多态的形式。特别是,如果我们有一个函数出现在某个类型为
a -> b -> c
这些变量a
,b
和c
可能已经或可能没有该表达式中的含义。特别是,他们被认为是固定的未知类型。将其与类型
forall a b c. a -> b -> c
其中包含多态性的概念,立即声明,即使碰巧有类型变量a
,b
和c
,环境,这些引用新鲜。
这是HM推理算法中非常重要的一步,因为它是生成的多态性,允许HM达到更一般的类型。不幸的是,只要我们愿意,就不可能做到这一步 - 它必须在受控点完成。
这就是let
- 泛化的作用:它表示当类型let
绑定到特定名称时,类型应该被推广到多态类型。当它们仅仅作为参数传递给函数时,就不会发生这种泛化。
因此,最终,您需要一种“let”形式才能运行HM推理算法。此外,它不仅仅是功能应用的语法糖,尽管它们具有相同的运行时特性。
从语法上讲,这个“let”概念可能被称为let
或where
,或者通过顶级名称绑定的约定(这三个都在Haskell中可用)。只要它存在并且是生成绑定名称的主要方法,人们期望多态性,那么它将具有正确的行为。
答案 1 :(得分:8)
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年由Joe Wells证明。)
(GHC允许您使用RankNTypes
扩展名启用此类显式内部量词,但如上所述,无法自动推断类型。)
考虑表达式λx.M
,或者用Haskell表示法\x -> M
,
其中M
是包含x
的术语。如果x
的类型为a
且M
的类型为b
,则整个表达式的类型将为λx.M : a → b
。由于上述限制,a
不得包含∀,因此 x
的类型不能包含类型量词,它不能是多态的(或换句话说)它必须是单态)。
考虑一下这个简单的Haskell程序:
i :: a -> a
i x = x
foo :: a -> a
foo = i i
我们现在忽视foo
不是很有用。重点是id
定义中的foo
实例化了两种不同的类型。第一个
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类型系统的语言中的多态性。
答案 2 :(得分:6)
History of Haskell说明Haskell早已接受复杂的表面语法这一事实。
我们在这里花了一些时间来确定风格选择,但是一旦我们这样做了,我们就哪种风格“更好”进行激烈辩论。一个潜在的假设是,如果可能的话应该“只是”做某事的一种方法,“例如,让let和where都是多余和混乱的。
最后,我们放弃了潜在的假设,并为这两种风格提供了完整的语法支持。这似乎是一个典型的委员会决定,但是现在的作者认为这是一个很好的选择,我们现在认为这是一种语言的力量。不同的结构具有不同的细微差别,真正的程序员在实践中同时使用let和where,guards和conditionals,模式匹配定义和case表达式 - 不仅在同一个程序中,有时在同一个函数定义中。毫无疑问,额外的句法糖会使语言看起来更复杂,但它是一种肤浅的复杂性,很容易用纯语法转换来解释。
答案 3 :(得分:4)
这不是一个愚蠢的问题。这是完全合理的。
首先,let / in绑定在语法上是明确的,可以用简单的机械方式重写为lambdas。
其次,因此,let ... in ...
是一个表达式:也就是说,它可以写在允许表达式的任何地方。相比之下,您建议的语法更类似于where
,它绑定到周围的句法结构,就像函数定义的模式匹配行一样。
有人可能会提出一个论点,即你的建议语法在风格上过于命令,但这肯定是主观的。
您可能更喜欢将where
用于let
。许多Haskell开发人员都这样做。这是一个合理的选择。
答案 4 :(得分:3)
有let
存在的充分理由:
let
可以在do
表示法中使用。您可以使用以下示例替代let
:
y = 1+2
z = 4+6
y+z
以上示例不会进行类型检查,y
和z
也会导致全局命名空间的污染,使用let
可以避免这种情况。
答案 5 :(得分:3)
Haskell的let
看起来像它的部分原因也是它管理缩进灵敏度的一致方式。每个缩进敏感的构造都以相同的方式工作:首先是引入关键字(let
,where
,do
,of
);然后下一个标记的位置确定该块的缩进级别是什么;以及从同一级别开始的后续行被认为是块中的新元素。这就是你可以拥有
let a = 1
b = 2
in a + b
或
let
a = 1
b = 2
in a + b
但不是
let a = 1
b = 2
in a + b
我认为实际上可以使用无关键字的基于缩进的绑定,而不会使语法在技术上模糊不清。但我认为目前的一致性是有价值的,至少对于最不惊讶的原则是这样。一旦你看到一个缩进敏感的结构是如何工作的,它们都是一样的。作为奖励,它们都具有相同的缩进不等的等价物。此
keyword <element 1>
<element 2>
<element 3>
总是等同于
keyword { <element 1>; <element 2>; <element 3> }
事实上,作为一个主要是F#的开发人员,这是我从Haskell羡慕的事情:F#的缩进规则更复杂,并不总是一致。