大多数Haskell教程都教授IO使用do-notation.
我也开始使用do-notation,但这使得我的代码看起来更像是一种命令式语言而不是FP语言.
本周我看到一个教程使用IO <$>
stringAnalyzer <$> readFile "testfile.txt"
Run Code Online (Sandbox Code Playgroud)
而不是使用 do
main = do
strFile <- readFile "testfile.txt"
let analysisResult = stringAnalyzer strFile
return analysisResult
Run Code Online (Sandbox Code Playgroud)
并且日志分析工具没有完成do.
所以我的问题是" 在任何情况下我们都应该避免使用记号吗? ".
我知道do在某些情况下可能会使代码变得更好.
另外,为什么大多数教程都教IO do?
在我看来<$>,<*>使代码更多FP比IO.
Dan*_*zer 40
do Haskell desugars中的符号以一种非常简单的方式表达.
do
x <- foo
e1
e2
...
Run Code Online (Sandbox Code Playgroud)
变成
foo >>= \x ->
do
e1
e2
Run Code Online (Sandbox Code Playgroud)
和
do
x
e1
e2
...
Run Code Online (Sandbox Code Playgroud)
成
x >>
do
e1
e2
....
Run Code Online (Sandbox Code Playgroud)
这意味着您可以使用>>=和编写任何monadic计算return.我们不这样做的唯一原因是因为它只是更痛苦的语法.Monads对于模仿命令式代码很有用,do符号使它看起来像它.
C-ish语法使初学者更容易理解它.你是对的,它看起来不那么实用,但要求有人在使用IO之前正确地调整monad是一个非常大的威慑力.
我们之所以使用>>=和return另一方面是因为它对于1-2个衬垫来说更紧凑.然而,对于任何太大的事情,它确实会变得更难以理解.所以要直接回答你的问题,请不要在适当的时候避免做符号.
最后你看到的两家运营商,<$>并且<*>,实际上是FMAP和应用性分别,没有单子.它们实际上不能用于表示符号所做的很多事情.它们确实更紧凑,但它们不会让您轻松命名中间值.就个人而言,我大约80%的时间使用它们,主要是因为我倾向于编写非常小的可组合功能,无论如何应用程序非常适合.
lef*_*out 40
在我看来
<$>,<*>使代码更多FP比IO.
Haskell不是一种纯函数式语言,因为它"看起来更好".有时确实如此,通常不会.保持功能的原因不是它的语法,而是它的语义.它装备我们引用透明,这使得它更容易证明不变,允许非常高层次的优化,可以很容易地编写通用代码等.
这些都与语法无关.一元计算仍然是纯粹的功能-无论你写他们用do符号或<$>,<*>和>>=,所以我们得到Haskell的好处无论哪种方式.
然而,尽管有前面提到的FP优势,但从类似命令的角度考虑算法通常更直观 - 即使你已经习惯了如何通过monad实现它.在这些情况下,do符号为您提供了"计算顺序","数据来源","修改点"的快速洞察,然而在您的脑海中手动去除它与>>=版本,以掌握功能上发生的事情是微不足道的.
应用风格在很多方面肯定很棒,但它本质上是无点的.这通常是一件好事,但特别是在更复杂的问题中,将名称赋予"临时"变量会非常有帮助.当仅使用"FP"Haskell语法时,这需要lambdas或显式命名的函数.两者都有很好的用例,但前者在代码中间引入了相当多的噪音,而后者则会破坏"流",因为它需要一个where或let放在你使用它的地方的其他地方.do另一方面,允许您在需要的地方引入命名变量,而不会引入任何噪声.
Ben*_*son 34
我经常发现自己首先用do符号写出一个monadic动作,然后将它重构为一个简单的monadic(或functorial)表达式.这种情况大多发生在do块比我预期的要短的时候.有时我会反方向重构; 这取决于有问题的代码.
我的一般规则是:如果do块只有几行长,它通常作为一个短表达式整齐.do除非你能找到一种方法将它分解为更小,更易组合的函数,所以long- block可能更具可读性.
作为一个有效的例子,我们可以将您的详细代码片段转换为简单的代码片段.
main = do
strFile <- readFile "testfile.txt"
let analysisResult = stringAnalyzer strFile
return analysisResult
Run Code Online (Sandbox Code Playgroud)
首先,请注意最后两行是否具有该形式let x = y in return x.这当然可以简单地转化为return y.
main = do
strFile <- readFile "testfile.txt"
return (stringAnalyzer strFile)
Run Code Online (Sandbox Code Playgroud)
这是一个非常短的do块:我们绑定readFile "testfile.txt"一个名称,然后在下一行中对该名称执行某些操作.让我们尝试'去糖',就像编译器会:
main = readFile "testFile.txt" >>= \strFile -> return (stringAnalyser strFile)
Run Code Online (Sandbox Code Playgroud)
看看右边的lambda形式>>=.它乞求以无点的方式重写:\x -> f $ g x成为\x -> (f . g) x变成的f . g.
main = readFile "testFile.txt" >>= (return . stringAnalyser)
Run Code Online (Sandbox Code Playgroud)
这已经比原来的do块更整洁了,但我们可以走得更远.
这是唯一需要一点思考的步骤(尽管一旦熟悉了monad和functor,它应该是显而易见的).上述功能暗示了monad法则之一:(m >>= return) == m.唯一的区别是右侧的功能>>=不仅仅是return- 我们对monad中的对象执行某些操作,然后将其重新包装回来return.但是"在不影响其包装的情况下对包装值做某事"的模式正是如此Functor.所有monad都是仿函数,所以我们可以重构它,这样我们甚至不需要Monad实例:
main = fmap stringAnalyser (readFile "testFile.txt")
Run Code Online (Sandbox Code Playgroud)
最后,请注意这<$>只是另一种写作方式fmap.
main = stringAnalyser <$> readFile "testFile.txt"
Run Code Online (Sandbox Code Playgroud)
我认为这个版本比原始代码更清晰.它可以读取类似的句子:" main被stringAnalyser应用到阅读的结果"testFile.txt"".原始版本在其操作的程序细节中陷入困境.
附录:我的评论说,"所有的单子都是仿函数"实际上可以被观察到是合理的m >>= (return . f)(又名标准库liftM)是一样的fmap f m.如果你有一个实例Monad,你得到一个Functor'免费' 的实例- 只需定义fmap = liftM!如果有人Monad为他们的类型定义了一个实例而没有为Functor和实例定义Applicative,我会称之为bug.客户希望能够Functor在Monad没有太多麻烦的情况下使用方法.
在任何情况下我们都应该避免使用记号吗?
我绝对不会说.对我来说,在这种情况下最重要的标准是使代码尽可能易读和易懂.该do-notation引入,使一元代码更容易理解,这是最重要的.当然,在许多情况下,使用无Applicative点符号非常好,例如,而不是
do
f <- [(+1), (*7)]
i <- [1..5]
return $ f i
Run Code Online (Sandbox Code Playgroud)
我们写的只是[(+1), (*7)] <*> [1..5].
但是有很多例子没有使用do-notation会使代码变得非常难以理解.考虑这个例子:
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++"!")
Run Code Online (Sandbox Code Playgroud)
这里很清楚发生了什么以及如何对IO行动进行排序.A- dofree符号看起来像
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
Run Code Online (Sandbox Code Playgroud)
或者喜欢
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++"!")
Run Code Online (Sandbox Code Playgroud)
哪些都不太可读.当然,这里的do注释更为可取.
如果您想避免使用do,请尝试将代码结构化为许多小函数.无论如何这是一个好习惯,你可以减少你的do块只包含2-3行,然后可以很好地替换>>=,<$>,<*>`等.例如,上面的内容可以重写为
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++"!")
Run Code Online (Sandbox Code Playgroud)
这是一个长一点,也许少了几分理解哈斯克尔初学者(由于intersperse,concat和traverse),但在许多情况下,这些新的小功能,可以在代码中的其他地方,这将使其更结构化和组合的重复使用.
我会说情况与是否使用无点符号非常相似.在很多情况下(比如在最顶层的例子中[(+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
Run Code Online (Sandbox Code Playgroud)
在不运行代码的情况下理解它需要花费很长时间.[下面的剧透:]
f x = if (x <= 1) then 1 else f (x-1) + f (x-2)
另外,为什么大多数教程都教IO?
因为IO它的设计完全是为了模仿具有副作用的命令式计算,所以对它们进行排序do非常自然.
do符号只是一种语法糖.它可以在所有情况下是可以避免的.然而,在某些情况下,取代do使用>>=和return使代码的可读性.
所以,对于你的问题:
"在任何情况下,我们都应该避免使用Do声明吗?"
专注于使您的代码清晰,更易读.有用do时使用,否则请避免使用.
我有另一个问题,为什么大多数教程都会教IO?
因为do在许多情况下使IO代码更易读.
此外,大多数开始学习Haskell的人都有必要的编程经验.教程适合初学者.他们应该使用新手容易理解的风格.
| 归档时间: |
|
| 查看次数: |
10358 次 |
| 最近记录: |