在下面的代码中,我可以在前面添加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表达式无法做到这一点,该表达式仅覆盖所包含的表达式。
我的问题:所以,可变数字不应该对最后一个印刷短语可见。我在这里想念一下吗?
答案 0 :(得分:109)
简短回答:在do-block的正文中使用let
而不使用in
,在列表理解中使用|
之后的部分。在其他任何地方,请使用let ... in ...
。
关键字let
在Haskell中以三种方式使用。
第一种形式是 let-expression 。
let variable = expression in expression
这可以在允许表达的任何地方使用,例如
> (let x = 2 in x*2) + 3
7
第二个是 let-statement 。此表单仅在do-notation中使用,不使用in
。
do statements
let variable = expression
statements
第三个类似于数字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
的形式。
答案 1 :(得分:17)
首先,为什么拥抱? 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
:做符号和列表推导(实际上,monad comprehensions)。这些便利案例的分配范围是预定义的。
do foo
let {assignments}
bar
baz
对于do
表示法,{assignments}
适用于以下任何语句,在本例中为bar
和baz
,但不包括foo
。就好像我们写了
do foo
let {assignments}
in do bar
baz
列出理解(或者实际上,任何monad理解)desugar into do notation,所以他们提供类似的设施。
[ baz | foo, let {assignments}, bar ]
{assignments}
适用于bar
和baz
这两个词,但不适用于foo
。
where
有些不同。如果我没有弄错的话,where
的范围与特定的函数定义对齐。所以
someFunc x y | guard1 = blah1
| guard2 = blah2
where {assignments}
此{assignments}
子句中的where
可以访问x
和y
。 guard1
,guard2
,blah1
和blah2
所有都可以访问此{assignments}
子句的where
。正如您链接的教程中所提到的,如果多个警卫重用相同的表达式,这可能会有所帮助。
答案 2 :(得分:7)
在do
表示法中,您确实可以使用let
使用in
。为了它是等价的(在你的情况下,我稍后将展示一个你需要添加第二个do
并因此更多缩进的例子),你需要在发现时缩进它(如果你使用的是布局) - 如果你使用显式括号和分号,它们就完全等价了。
要理解为什么它是等价的,你必须实际修改monad(至少在某种程度上)并查看do
符号的desugaring规则。特别是,像这样的代码:
do let x = ...
stmts -- the rest of the do block
已翻译为let x = ... in do { stmts }
。在您的情况下,stmts = print (problem_8 digits)
。评估整个desugared let
绑定会导致IO操作(来自print $ ...
)。在这里,你需要理解monad,直观地认为do
符号和描述导致monadic值的计算的“常规”语言元素之间没有区别。
至于两者为什么都可能:嗯,let ... in ...
有广泛的应用程序(其中大多数与monad无关),以及很长的启动历史。另一方面,let
没有in
do
符号val <- return $ ...
似乎只是一小段语法糖。优点是显而易见的:您可以将纯(不是monadic)计算的结果绑定到名称而不诉诸无意义do
并且不将do stuff
let val = ...
in do more
stuff $ using val
块拆分为两个:
do
您在let
后面不需要额外do e
块的原因是您只有一行。请注意,e
为digit
。
关于您的修改:do
在下一行中可见是重点。它也不例外。 let
表示法成为单个表达式,where
在单个表达式中工作正常。只有非表达式的东西才需要do
。
为了演示,我将展示您>>=
块的desugared版本。如果你还不熟悉monad(你应该尽快更改恕我直言),忽略let
运算符并专注于main = readFile "p8.log" >>= (\t ->
let digits = map digitToInt $ concat $ lines t
in print (problem_8 digits))
。另请注意,缩进不再重要。
{{1}}
答案 3 :(得分:1)
一些初学者注意到“跟随两个相同”。
例如,add1
是一个函数,它将数字加1:
add1 :: Int -> Int
add1 x =
let inc = 1
in x + inc
所以,就像add1 x = x + inc
一样,let
关键字的替换值为1。
当您尝试取消in
关键字
add1 :: Int -> Int
add1 x =
let inc = 1
x + inc
你有解析错误。
Within do-blocks or list comprehensions
let { d1 ; ... ; dn }
without `in` serves to introduce local bindings.
顺便说一下,nice explanation有许多关于where
和in
关键字实际执行的示例。