大多数Haskell教程都教授使用IO进行编号。
我也开始使用do-notation,但这使得我的代码看起来更像是一种命令式语言而不是FP语言。
本周我看到一个教程使用IO <$>
stringAnalyzer <$> readFile "testfile.txt"
而不是使用do
main = do
strFile <- readFile "testfile.txt"
let analysisResult = stringAnalyzer strFile
return analysisResult
日志分析工具在没有do
的情况下完成。
所以我的问题是“我们应该在任何情况下避免使用记号吗?”。
我知道也许do
会在某些情况下使代码变得更好。
此外,为什么大多数教程都会使用do
教授IO?
在我看来,<$>
和<*>
使代码比IO更加FP。
答案 0 :(得分:40)
在我看来,
<$>
和<*>
使代码比IO更加FP。
Haskell不是纯粹的功能语言,因为它“看起来更好”。有时确实如此,通常不会。保持功能的原因不是它的语法,而是它的语义。它为我们提供了参考透明度,这使得更容易证明不变量,允许非常高级的优化,使编写通用代码等变得容易。
这些都与语法无关。 Monadic计算仍然纯粹是功能性的 - 无论你是用do
表示还是用<$>
,<*>
和>>=
编写它们,所以我们得到Haskell的好处。
然而,尽管有上述FP优势,但从类似命令的角度考虑算法通常更直观 - 即使您已经习惯了如何通过monad实现这一点。在这些情况下,do
表示法可以让您快速了解“计算顺序”,“数据来源”,“修改点”,然而在您的脑海中手动将它去除>>=
是微不足道的。 }版本,以掌握功能上正在发生的事情。
在许多方面,应用风格肯定很棒,但它本质上是无点的。这通常是一件好事,但特别是在更复杂的问题中,将名称赋予“临时”变量会非常有帮助。当仅使用“FP”Haskell语法时,这需要lambdas或显式命名的函数。两者都有很好的用例,但前者在你的代码中间引入了相当多的噪音而后者却破坏了“流”,因为它需要where
或let
放置在其他地方在哪里使用它。另一方面,do
允许您在需要的地方引入命名变量,而不会引入任何噪音。
答案 1 :(得分:39)
do
符号非常简单。
do
x <- foo
e1
e2
...
变成
foo >>= \x ->
do
e1
e2
和
do
x
e1
e2
...
到
x >>
do
e1
e2
....
这意味着您可以使用>>=
和return
编写任何monadic计算。我们不这样做的唯一原因是因为它只是更痛苦的语法。 Monads对于模仿命令式代码很有用,do
符号使它看起来像它。
C-ish语法使初学者更容易理解它。你是对的,它看起来不那么实用,但要求有人在使用IO之前正确地修改monad是一个非常大的威慑力。
另一方面,我们使用>>=
和return
的原因是因为它的很多对于1-2个内衬来说更紧凑。然而,对于任何太大的东西,它确实会变得更难以理解。所以要直接回答你的问题,请不要在适当的时候避免做符号。
最后,您看到的两个运算符<$>
和<*>
实际上分别是fmap和applicative,而不是monadic。它们实际上不能用于表示符号所做的很多事情。它们确实更紧凑,但它们不会让您轻易命名中间值。就个人而言,我大约80%的时间使用它们,主要是因为我倾向于编写非常小的可组合函数,无论如何应用程序非常适合。
答案 2 :(得分:34)
我经常发现自己首先用do
符号写一个monadic动作,然后将它重构为一个简单的monadic(或functorial)表达式。这种情况主要发生在do
块比我预期的更短的情况下。有时我会反方向重构;这取决于有问题的代码。
我的一般规则是:如果do
块只有几行长,那么它通常比较短的表达式。较长的do
- 块可能更具可读性,除非您能找到一种方法将其分解为更小,更可组合的函数。
作为一个有效的例子,我们可以将您的详细代码片段转换为简单的代码片段。
main = do
strFile <- readFile "testfile.txt"
let analysisResult = stringAnalyzer strFile
return analysisResult
首先,请注意最后两行的格式为let x = y in return x
。这当然可以简化为return y
。
main = do
strFile <- readFile "testfile.txt"
return (stringAnalyzer strFile)
这是一个非常短的do
块:我们将readFile "testfile.txt"
绑定到一个名称,然后在下一行中对该名称执行某些操作。让我们像编译器一样尝试“解糖”:
main = readFile "testFile.txt" >>= \strFile -> return (stringAnalyser strFile)
查看>>=
右侧的lambda形式。它要求以无点样式重写:\x -> f $ g x
变为\x -> (f . g) x
,变为f . g
。
main = readFile "testFile.txt" >>= (return . stringAnalyser)
这已经比最初的do
块更整洁,但我们可以更进一步。
这是唯一需要一点思考的步骤(虽然一旦熟悉了monad和functor,它应该是显而易见的)。上述功能提示monad laws:(m >>= return) == m
之一。唯一的区别是>>=
右侧的函数不仅仅是return
- 我们对monad中的对象执行某些操作,然后将其重新包装在return
中。但是“在不影响其包装器的情况下对包装值执行操作”的模式正是Functor
的用途。所有monad都是仿函数,所以我们可以重构它,这样我们甚至不需要Monad
实例:
main = fmap stringAnalyser (readFile "testFile.txt")
最后,请注意<$>
只是撰写fmap
的另一种方式。
main = stringAnalyser <$> readFile "testFile.txt"
我认为这个版本比原始代码更清晰。它可以像句子一样阅读:“main
stringAnalyser
应用于阅读"testFile.txt"
的结果”。原版本在操作的程序细节中陷入困境。
附录:我的评论'所有monad都是仿函数'实际上可以通过m >>= (return . f)
(又名标准库的liftM
)与fmap f m
相同的观察来证明。如果您有Monad
的实例,则可以免费获得Functor
'的实例 - 只需定义fmap = liftM
!如果有人为其类型定义了Monad
个实例,但没有为Functor
和Applicative
定义实例,我会称之为错误。客户希望能够在Functor
的实例上使用Monad
方法而不会有太多麻烦。
答案 3 :(得分:9)
应鼓励应用风格,因为它构成(并且更漂亮)。在某些情况下,Monadic风格是必要的。有关详细说明,请参阅https://stackoverflow.com/a/7042674/1019205。
答案 4 :(得分:9)
在任何情况下我们都应该避免使用记号吗?
我会说肯定没有。对我来说,在这种情况下最重要的标准是使代码尽可能易读和易懂。引入do
- 符号是为了使monadic代码更容易理解,这才是最重要的。当然,在许多情况下,使用Applicative
无点符号非常很好,例如,而不是
do
f <- [(+1), (*7)]
i <- [1..5]
return $ f i
我们只写[(+1), (*7)] <*> [1..5]
。
但是有很多例子没有使用do
- 符号会使代码变得非常难以理解。考虑this example:
nameDo :: IO ()
nameDo = do putStr "What is your first name? "
first <- getLine
putStr "And your last name? "
last <- getLine
let full = first++" "++last
putStrLn ("Pleased to meet you, "++full++"!")
这里很清楚发生了什么以及IO
行动是如何排序的。 do
- 免费符号看起来像
name :: IO ()
name = putStr "What is your first name? " >>
getLine >>= f
where
f first = putStr "And your last name? " >>
getLine >>= g
where
g last = putStrLn ("Pleased to meet you, "++full++"!")
where
full = first++" "++last
或喜欢
nameLambda :: IO ()
nameLambda = putStr "What is your first name? " >>
getLine >>=
\first -> putStr "And your last name? " >>
getLine >>=
\last -> let full = first++" "++last
in putStrLn ("Pleased to meet you, "++full++"!")
两者都不太可读。当然,这里do
- 符号在这里更为可取。
如果您想避免使用do
,请尝试将代码结构化为许多小函数。无论如何,这是一个好习惯,你可以减少do
块只包含2-3行,然后>>=
,<$>,
&lt; *&gt;可以很好地替换它们。例如,上面的内容可以改写为
name = getName >>= welcome
where
ask :: String -> IO String
ask s = putStr s >> getLine
join :: [String] -> String
join = concat . intersperse " "
getName :: IO String
getName = join <$> traverse ask ["What is your first name? ",
"And your last name? "]
welcome :: String -> IO ()
welcome full = putStrLn ("Pleased to meet you, "++full++"!")
它有点长,也许对Haskell初学者来说可能不太容易理解(由于intersperse
,concat
和traverse
),但在很多情况下,这些新的小函数可能是在代码的其他地方重用,这将使其更具结构性和可组合性。
我说情况与是否使用point-free notation非常相似。在很多情况下(比如最顶层的例子[(+1), (*7)] <*> [1..5]
),无点符号很棒,但是如果你尝试转换一个复杂的表达式,你会得到像
f = ((ite . (<= 1)) `flip` 1) <*>
(((+) . (f . (subtract 1))) <*> (f . (subtract 2)))
where
ite e x y = if e then x else y
在不运行代码的情况下理解它需要很长时间。 [下面的剧透:]
f x = if (x <= 1) then 1 else f (x-1) + f (x-2)
另外,为什么大多数教程都教IO做?
因为IO
完全是为了模仿具有副作用的命令式计算而设计的,所以使用do
对其进行排序是非常自然的。
答案 5 :(得分:6)
do
符号只是一种语法糖。在所有情况下都可以避免 。但是,在某些情况下,将do
替换为>>=
和return
会降低代码的可读性。
所以,对于你的问题:
&#34;在任何情况下我们都应该避免使用Do语句吗?&#34;。
专注于使您的代码更清晰,更易读。在有帮助时使用do
,否则请避免使用。
我还有另外一个问题,为什么大多数教程会教IO做什么?
因为do
在许多情况下使IO代码更易读。
此外,大多数开始学习Haskell的人都有必要的编程经验。教程适合初学者。他们应该使用新手容易理解的风格。
答案 6 :(得分:1)
使用函数do
和(>>=)
以及(>>)
表达式将let
表示法扩展为表达式。所以它不是语言核心的一部分。
(>>=)
和(>>)
用于按顺序组合操作,当操作结果更改以下操作的结构时,它们是必不可少的。
在问题中给出的示例中,这并不明显,因为只有一个IO
动作,因此不需要排序。
考虑例如表达式
do x <- getLine
print (length x)
y <- getLine
return (x ++ y)
被翻译为
getLine >>= \x ->
print (length x) >>
getLine >>= \y ->
return (x ++ y)
在此示例中,do
操作的排序需要(>>=)
符号(或(>>)
和IO
函数)。
程序员很快就会需要它。