剥离Haskell

时间:2014-05-04 03:39:11

标签: haskell let

我应该首先提一下,我对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

很抱歉,如果这个问题看起来很愚蠢,我只是想了解更多关于它为什么存在的问题。

6 个答案:

答案 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

这些变量abc可能已经或可能没有该表达式中的含义。特别是,他们被认为是固定的未知类型。将其与类型

进行比较
forall a b c. a -> b -> c

其中包含多态性的概念,立即声明,即使碰巧有类型变量abc,环境,这些引用新鲜

这是HM推理算法中非常重要的一步,因为它是生成的多态性,允许HM达到更一般的类型。不幸的是,只要我们愿意,就不可能做到这一步 - 它必须在受控点完成。

这就是let - 泛化的作用:它表示当类型let绑定到特定名称时,类型应该被推广到多态类型。当它们仅仅作为参数传递给函数时,就不会发生这种泛化。


因此,最终,您需要一种“let”形式才能运行HM推理算法。此外,它不仅仅是功能应用的语法糖,尽管它们具有相同的运行时特性。

从语法上讲,这个“let”概念可能被称为letwhere,或者通过顶级名称绑定的约定(这三个都在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可以专门用于我们通过将两个类型表达式替换为ab而获得的任何类型。

但是,类型系统不允许在类型表达式中使用量词,例如

(∀a. a -> a) -> (∀b. b -> b)

System F中允许使用这些类型,但是类型检查和类型推断是不可判定的,这意味着编译器无法为我们推断类型,我们必须使用类型显式地注释表达式。

(很长一段时间以来,系统F中类型检查的可判定性问题已经公开,而且它有时被称为“一个令人尴尬的开放性问题”,因为已经证明了许多其他系统的不可判定性,但是这个系统,直到1994年由Joe Wells证明。)

(GHC允许您使用RankNTypes扩展名启用此类显式内部量词,但如上所述,无法自动推断类型。)

lambda抽象的类型

考虑表达式λx.M,或者用Haskell表示法\x -> M, 其中M是包含x的术语。如果x的类型为aM的类型为b,则整个表达式的类型将为λx.M : a → b。由于上述限制,a不得包含∀,因此 x的类型不能包含类型量词,它不能是多态的(或换句话说)它必须是单态)。

为什么lambda抽象不够

考虑一下这个简单的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表示法中使用。
  • 它可以在列表理解中使用。
  • 可以方便地在函数定义中使用here

您可以使用以下示例替代let

y = 1+2
z = 4+6
y+z

以上示例不会进行类型检查,yz也会导致全局命名空间的污染,使用let可以避免这种情况。

答案 5 :(得分:3)

Haskell的let看起来像它的部分原因也是它管理缩进灵敏度的一致方式。每个缩进敏感的构造都以相同的方式工作:首先是引入关键字(letwheredoof);然后下一个标记的位置确定该块的缩进级别是什么;以及从同一级别开始的后续行被认为是块中的新元素。这就是你可以拥有

的原因
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#的缩进规则更复杂,并不总是一致。