【问题标题】:In Haskell, when do we use in with let?在 Haskell 中,我们什么时候使用 in with let?
【发布时间】:2012-01-06 15:52:03
【问题描述】:

在下面的代码中,最后一个短语我可以在前面放一个in。它会改变什么吗?

另一个问题:如果我决定将in 放在最后一个短语的前面,我需要缩进吗?

我试过不缩进和拥抱抱怨

do {...} 中的最后一个生成器必须是表达式

import Data.Char
groupsOf _ [] = []
groupsOf n xs = 
    take n xs : groupsOf n ( tail xs )

problem_8 x = maximum . map product . groupsOf 5 $ x
main = do t <- readFile "p8.log" 
          let digits = map digitToInt $concat $ lines t
          print $ problem_8 digits

编辑

好的,所以人们似乎不明白我在说什么。让我改写一下: 考虑到上面的上下文,以下两个是否相同?

1.

let digits = map digitToInt $concat $ lines t
print $ problem_8 digits

2.

let digits = map digitToInt $concat $ lines t
in print $ problem_8 digits

关于let 中声明的绑定范围的另一个问题:我读到here

where 子句。

有时在几个受保护的方程式上进行范围绑定很方便,这需要一个 where 子句:

f x y  |  y>z           =  ...
       |  y==z          =  ...
       |  y<z           =  ...
     where z = x*x

请注意,这不能使用 let 表达式来完成,它仅作用于 它所包含的表达式

我的问题:所以,最后一个打印短语不应该看到可变数字。我错过了什么吗?

【问题讨论】:

  • 您知道do 表示法的真正含义还是对您来说是一个黑匣子?知道它的胆量,你提到的一切对我来说似乎都是非常直观的。当然,在不了解先决条件的情况下,直觉只能到此为止……
  • OK 现在我得到了缩进部分。基本上,如果我不缩进,“do”会将其视为一项独立任务,与 let 相同级别。另一个问题:我必须使用 let 来声明变量吗?
  • @osager:Haskell 并没有真正的“变量”。您可以将“值”绑定到给定名称。如果您真的想在内存中有一个单元格,您可以在程序执行期间使用不同的值进行修改,您需要使用IORef 或类似名称。但一般鼓励新手学习函数式的做事方式,这会极力避免修改引用的值。
  • 谢谢丹。我也开始意识到(令我惊讶的是)haskell 并没有真正使用变量的概念!但是,我发现 Haskell 的语法比函数式编程的思维方式更难掌握!

标签: haskell scope whitespace where-clause let


【解决方案1】:

简答:在 do-block 的主体中使用 let 而不使用 in,在列表推导中使用 | 之后的部分。在其他任何地方,请使用let ... in ...


关键字let在Haskell中以三种方式使用。

  1. 第一种形式是let-expression

    let variable = expression in expression
    

    这可以在任何允许使用表达式的地方使用,例如

    > (let x = 2 in x*2) + 3
    7
    
  2. 第二个是let-statement。这种形式只在do-notation内部使用,不使用in

    do statements
       let variable = expression
       statements
    
  3. 第三个类似于数字 2,并在列表推导中使用。同样,没有in

    > [(x, y) | x <- [1..3], let y = 2*x]
    [(1,2),(2,4),(3,6)]
    

    这种形式绑定了一个变量,该变量在后续生成器的范围内以及|之前的表达式中。


您在这里感到困惑的原因是表达式(类型正确)可以用作 do-block 中的语句,而 let .. in .. 只是一个表达式。

由于haskell的缩进规则,比上一行缩进更远的行意味着它是上一行的延续,所以这个

do let x = 42 in
     foo

被解析为

do (let x = 42 in foo)

没有缩进,你会得到一个解析错误:

do (let x = 42 in)
   foo

总之,永远不要在列表解析或执行块中使用in。这是不必要且令人困惑的,因为这些构造已经有自己的let 形式。

【讨论】:

  • 表格2和表格3有什么区别吗?在将[RET | SRC1, SRC2, ...] 扩展为do SRC1 ⏎ SRC2 ⏎ ... ⏎ return RET 之后,我认为它们实际上是相同的。
  • @leftaroundabout:他们曾经在旧版本的 Haskell 中。然后列表推导仅限于列表,而 do-notation 适用于任何 monad。但是,GHC 7.2.1 再次将此功能带回 MonadComprehensions 扩展。
  • 非常次要:let-binding 中的 LHS 是模式匹配,而不仅仅是变量绑定。
  • 这个答案只需要一个关于 GHCi 有点像现在处于永久执行块中的小评论。
  • 我意识到你不能写像let x = x + 1这样的东西。我假设它就像let x = 1 ; let x = x +1 之类的变量赋值会将 x 增加到 2。但由于 let 是递归的,x 只是变成了一个无限表达式。除非您执行 x &lt;- return (x + 1) 之类的操作,否则您无法重新分配变量。有没有办法让 let 不递归?
【解决方案2】:

首先,为什么要拥抱? Haskell Platform 通常是 GHC 附带的新手推荐使用方式。

现在,转到let关键字。此关键字的最简单形式是始终in 一起使用。

let {assignments} in {expression}

例如,

let two = 2; three = 3 in two * three

{assignments}在相应的 {expression} 范围内。 应用常规布局规则,这意味着 in 必须至少缩进它对应的let,以及与let 表达式相关的任何子表达式也必须同样缩进。这实际上不是 100% 正确的,但它是一个很好的经验法则; Haskell 布局规则是您在阅读和编写 Haskell 代码时会逐渐习惯的东西。请记住,缩进量是指示哪些代码与哪些表达式相关的主要方式。

Haskell 提供了两种方便的情况,您不必 编写in:做表示法和列表推导(实际上是单子推导)。这些便利案例的分配范围是预定义的。

do foo
   let {assignments}
   bar
   baz

对于do 表示法,{assignments} 在后面的任何语句的范围内,在这种情况下,barbaz,但不是foo。好像我们写过

do foo
   let {assignments}
   in do bar
         baz

列表推导式(或者实际上,任何单子推导式)脱糖为 do 表示法,因此它们提供了类似的工具。

[ baz | foo, let {assignments}, bar ]

{assignments} 在表达式 barbaz 的范围内,但不在 foo 的范围内。


where 有点不同。如果我没记错的话,where 的范围与特定的函数定义一致。所以

someFunc x y | guard1 = blah1
             | guard2 = blah2
  where {assignments}

where 子句中的{assignments} 可以访问xyguard1guard2blah1blah2 所有都可以访问此 where 子句的 {assignments}。正如您链接的教程中所述,如果多个警卫重用相同的表达式,这可能会有所帮助。

【讨论】:

  • 我也选择了Hugs,因为它提供了比winghci更多的功能,例如代码浏览。我确实想知道这两者是否兼容,尤其是对于第三方库。前几天我安装了一个库,winghci 找到了它,但没有找到 Hugs。
  • 谢谢。这也是我找到分号的唯一地方,例如let a = 1; b = 2 in ...
【解决方案3】:

do 表示法中,您确实可以使用letin。为了使其等效(在您的情况下,稍后我将展示一个示例,您需要添加第二个 do 并因此增加缩进),您需要按照您发现的方式缩进(如果您使用的是布局 - 如果你使用显式的大括号和分号,它们是完全等价的)。

要理解 为什么 它是等价的,您必须真正了解 monad(至少在某种程度上)并查看 do 表示法的脱糖规则。特别是这样的代码:

do let x = ...
   stmts -- the rest of the do block

被翻译成let x = ... in do { stmts }。在您的情况下,stmts = print (problem_8 digits)。评估整个脱糖 let 绑定会导致一个 IO 操作(来自 print $ ...)。在这里,您需要了解 monad 才能直观地同意 do 符号和描述计算产生 monadic 值的“常规”语言元素之间没有区别。

至于为什么两者都是可能的:嗯,let ... in ... 具有广泛的应用程序(其中大多数与 monads 无关),并且启动历史悠久。另一方面,let 没有 indo 符号似乎只是一小块语法糖。优点很明显:您可以将纯(如,非单子)计算的结果绑定到一个名称,而无需诉诸毫无意义的val &lt;- return $ ...,也无需将do 块一分为二:

do stuff
   let val = ...
    in do more
          stuff $ using val

let 后面的内容不需要额外的do 块的原因是你只有一行。请记住,do ee

关于您的编辑:digit 在下一行可见是重点。它也不例外。 do 表示法成为一个表达式,let 在单个表达式中工作得很好。 where 仅用于非表达式的事物。

为了演示,我将展示您的 do 块的脱糖版本。如果您还不太熟悉 monad(恕我直言,您应该尽快更改),请忽略 &gt;&gt;= 运算符并专注于 let。另请注意,缩进不再重要。

main = readFile "p8.log" >>= (\t ->
  let digits = map digitToInt $ concat $ lines t
  in print (problem_8 digits))

【讨论】:

  • 感谢您的解释。我可能会在到达单子章节之前上吊。
  • @osager Bind 和 return 是使 monad 有用的两个函数,但是当要了解 monad 实际上是什么时,最好从 fmap 和 join 的角度来考虑它们。跨度>
  • @McBearHolden 我的用户图片中的图表对我帮助很大。我的个人资料中也有链接。 monad 只是一个类型 M 具有一种特殊的通用应用程序/组合 (a -&gt; M b) -&gt; (b -&gt; M c) -&gt; (a -&gt; M c) (它满足一些“明显的”法则,例如关联性和同一性的存在 a -&gt; M a wrt 组合),就像常规应用程序组合是(a -&gt; b) -&gt; (b -&gt; c) -&gt; (a -&gt; c)(其中(.) 是关联的,id :: a -&gt; a 存在于任何a)。还有do-notation SO 标签及其info page
【解决方案4】:

一些关于“两个相同”的初学者笔记。

例如,add1 是一个将数字加 1 的函数:

add1 :: Int -> Int
add1 x =
    let inc = 1
    in x + inc

所以,这就像 add1 x = x + inclet 关键字中的 1 替换 inc。

当你试图抑制 in 关键字时

add1 :: Int -> Int
add1 x =
    let inc = 1
    x + inc

你有解析错误。

来自documentation

Within do-blocks or list comprehensions 
let { d1 ; ... ; dn } 
without `in` serves to introduce local bindings. 

顺便说一句,nice explanation 有很多关于 wherein 关键字实际作用的示例。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-01-08
    • 2012-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多