在任何情况下我们都应该避免 do-notation 吗?
我肯定会说不。对我来说,在这种情况下最重要的标准是使代码尽可能地可读和易于理解。引入-notationdo
是为了使一元代码更易于理解,这很重要。当然,在许多情况下,使用Applicative
无点表示法非常好,例如,而不是
do
f <- [(+1), (*7)]
i <- [1..5]
return $ f i
我们只写[(+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++"!")
这里很清楚发生了什么以及IO
动作是如何排序的。do
-free 符号看起来像
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 行,然后可以很好地替换为>>=
, <$>,
<*>` 等。例如,上面可以重写为
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
),但在许多情况下,这些新的小函数可以在代码的其他地方重用,这将使其更具结构化和可组合性。
我想说这种情况与是否使用无点表示法非常相似。在许多情况下(例如最上面的示例[(+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)
另外,为什么大多数教程都用 do 来教授 IO?
因为IO
被设计用来模仿带有副作用的命令式计算,所以使用它们的顺序do
是非常自然的。