我很难理解这一点。用do notation编写时,以下两行有何不同?
1. let x = expression
2. x <- expression
我看不到它。有时一个工作,有时一个工作。但两者都很少。 “了解一个哈斯克尔”说<-
将右侧与左侧的符号绑定。但是,与仅使用x
定义let
?
答案 0 :(得分:25)
<-
语句将从monad中提取值,而let
语句则不会。
import Data.Typeable
readInt :: String -> IO Int
readInt s = do
putStrLn $ "Enter value for " ++ s ++ ": "
readLn
main = do
x <- readInt "x"
let y = readInt "y"
putStrLn $ "x :: " ++ show (typeOf x)
putStrLn $ "y :: " ++ show (typeOf y)
运行时,程序将询问x的值,因为readInt "x"
语句执行monadic操作<-
。它不会要求y的值,因为readInt "y"
被计算但是没有执行生成的monadic动作。
Enter value for x: 123 x :: Int y :: IO Int
自x :: Int
以来,你可以用它做正常的Int
事。
putStrLn $ "x = " ++ show x
putStrLn $ "x * 2 = " ++ show (x * 2)
自y :: IO Int
以来,您无法假装它是常规Int
。
putStrLn $ "y = " ++ show y -- ERROR
putStrLn $ "y * 2 = " ++ show (y * 2) -- ERROR
答案 1 :(得分:13)
在let
绑定中,表达式可以包含任何类型,而您所做的只是为其命名(或在其内部结构上匹配模式)。
在<-
版本中,表达式的类型必须为m a
,其中m
是do
块所在的monad。所以在IO
中例如,monad,此表单的绑定必须在右侧具有某种类型IO a
的值。 a
部分(monadic值内)是左侧模式的约束。这使您可以在do
块的有限范围内提取monad的“内容”。
正如您可能已经读过的那样,do
符号只是monadic绑定运算符(>>=
和>>
)的语法糖。 x <- expression
将expression >>= \x ->
和expression
(本身,没有<-
)去糖去expression >>
去糖。这只是为定义monadic计算的长链提供了一种更方便的语法,否则它往往会构建一个相当令人印象深刻的嵌套lambda。
let
绑定根本不会脱糖,真的。 let
块中的do
与let
块之外的do
之间的唯一区别是do
版本不需要in
}关键字跟随它;它绑定的名称隐含在do
块的其余部分的范围内。
答案 2 :(得分:9)
在let
表单中,expression
是非monadic值,而<-
的右侧是monadic表达式。例如,您只能在第二种绑定中进行I / O操作(类型为IO t
)。详细地说,这两种形式可以大致翻译为(其中==>
显示翻译):
do {let x = expression; rest} ==> let x = expression in do {rest}
和
do {x <- operation; rest} ==> operation >>= (\ x -> do {rest})
答案 3 :(得分:3)
let
只为任意值指定名称或模式匹配。
对于<-
,让我们先离开(不是真的)神秘的IO
monad,但要考虑具有“容器”概念的monad,如列表或{{1} }。然后Maybe
只能“解包”该容器的元素。 “放回去”的相反操作是<-
。请考虑以下代码:
return
它“解包”两个容器的元素,将值一起添加,并将它再次包装在同一个monad中。它适用于列表,采用所有可能的元素组合:
add m1 m2 = do
v1 <- m1
v2 <- m2
return (v1 + v2)
事实上,如果是列表,你也可以写main = print $ add [1, 2, 3] [40, 50]
--[41,51,42,52,43,53]
。但我们的版本也适用于add m1 m2 = [v1 + v2 | v1 <- m1, v2 <- m2]
s:
Maybe
现在main = print $ add (Just 3) (Just 12)
--Just 15
main = print $ add (Just 3) Nothing
--Nothing
并没有那么不同。它是单个值的容器,但它是像病毒一样的“危险”不纯的值,我们不能直接触摸它。 IO
- 块在这里是我们的玻璃收容所,do
是内置的“手套”来操纵里面的东西。使用<-
,当我们准备好时,我们会提供完整,完整的容器(而不仅仅是危险内容)。顺便说一下,return
函数也可以使用add
值(我们从文件或命令行或随机生成器获得...)。
答案 4 :(得分:2)
Haskell通过使用形式IO a
的类型表示命令行为来协调副作用命令式编程与纯函数式编程:产生a
类型结果的命令行为的类型。
其中一个后果是将变量绑定到表达式的值并将其绑定到执行操作的结果是两回事:
x <- action -- execute action and bind x to the result; may cause effect
let x = expression -- bind x to the value of the expression; no side effects
所以getLine :: IO String
是一个动作,这意味着它必须像这样使用:
do line <- getLine -- side effect: read from stdin
-- ...do stuff with line
line1 ++ line2 :: String
是纯表达式,必须与let
一起使用:
do line1 <- getLine -- executes an action
line2 <- getLine -- executes an action
let joined = line1 ++ line2 -- pure calculation; no action is executed
return joined
答案 5 :(得分:2)
这是一个显示差异的简单示例。 考虑以下两个简单表达式:
letExpression = 2
bindExpression = Just 2
您要检索的信息是数字2
。
这是你如何做到的:
let x = letExpression
x <- bindExpression
let
直接将值2
放入x
。
<-
从2
中提取值Just
并将其放入x
。
您可以看到这个例子,为什么这两个符号不可互换:
let x = bindExpression
会直接将值Just 2
放入x
。
x <- letExpression
无法提取和放入x
。