在Haskell中应该避免使用符号吗?
大多数Haskell教程都教导了如何使用IO标记。
我也从注释开始,但是这使得我的代码更像是一种命令式语言,而不是一种FP语言。
本周,我看到一个教程使用IO与<$>
stringAnalyzer <$> readFile "testfile.txt"
而不是使用do
main = do strFile <- readFile "testfile.txt" let analysisResult = stringAnalyzer strFile return analysisResult
日志分析工具没有do
。
所以我的问题是“ 我们是否应该避免使用符号? ”。
我知道也许会在某些情况下使代码更好。
另外, 为什么大多数教程都用IO来教IO呢?
在我看来, <$>
和<*>
使得代码比IO更多。
在我看来,
<$>
和<*>
使得代码比IO更多。
Haskell不是一个纯粹的函数式语言,因为“看起来更好”。 有时候确实如此,但事实并非如此。 保持function的原因不在于它的语法,而在于它的语义 。 它为我们提供了参考透明度,这使得certificate不variables更容易,允许非常高级的优化,使得编写通用代码变得容易。
这与语法没有太大的关系。 一元计算仍然是纯粹的function – 无论你是用符号还是用<$>
, <*>
和>>=
编写它们,所以我们都能获得Haskell的好处。
然而,尽pipe有前面提到的FP优点,即使您习惯了如何通过monad来实现这个function,从一个必要的angular度来考虑algorithm,也往往会更直观。 在这些情况下,符号给你这个“计算顺序”,“数据的起源”,“修改点”的快速见解,但是在你的头部手工解开它,去掌握什么是微不足道的在function上进行。
在许多方面,应用风格确实很棒,但它本质上是无点的。 这通常是一件好事,但特别是在更复杂的问题中,给“临时”variables命名是非常有帮助的。 当仅使用“FP”Haskell语法时,这需要lambdas或明确命名的函数。 两者都有很好的用例,但是前者在代码中间引入了相当多的噪声,而后者会扰乱“stream”,因为它需要一个where
或者let
放在你使用它的地方。 另一方面,可以让你在需要的地方引入一个名字variables,而不会引入任何噪音。
在一个非常简单的方式在Haskell desugars中表示。
do x <- foo e1 e2 ...
变成
foo >>= \x -> do e1 e2
和
do x e1 e2 ...
成
x >> do e1 e2 ....
这意味着你可以真正用>>=
来写任何单子计算并return
。 我们不这样做的唯一原因是因为它只是更痛苦的语法。 Monad对于模仿命令式的代码很有用,符号使它看起来像它。
C-ish语法使初学者更容易理解它。 你说得对,它看起来没有什么function,但是要求有人在使用IO之前正确使用monad,这是一个很大的威慑。
我们之所以使用>>=
和return
另一方面是因为它更紧凑的1-2单位。 但是,对于任何太大的事情,它往往会变得难以理解。 所以要直接回答你的问题,不要在适当的时候避免标记。
最后,你看到的两个运算符<$>
和<*>
实际上分别是fmap和applicative,而不是monadic。 它们实际上不能用来表示许多符号的function。 它们比较紧凑,但是不能让你轻易地命名中间值。 就我个人而言,大约80%的时间使用它们,主要是因为我倾向于编写非常小的可组合的函数,而应用程序非常适合。
我经常发现自己先写一个monadic动作,然后把它重构成一个简单的monadic(或者functorial)expression式。 发生这种情况的主要原因是, do
块的数量比我预期的要短。 有时候我会在相反的方向重构。 这取决于有问题的代码。
我的一般规则是:如果do
块只有几行,那么它通常是一个简短的expression式。 一个很长的块可能是更可读的,除非你能find一种方法把它分解成更小,更可组合的函数。
作为一个成功的例子,下面是我们如何将您的详细代码片段转换为简单的代码片段。
main = do strFile <- readFile "testfile.txt" let analysisResult = stringAnalyzer strFile return analysisResult
首先,注意最后两行的forms是let x = y in return x
。 这当然可以转化为简单的return y
。
main = do strFile <- readFile "testfile.txt" return (stringAnalyzer strFile)
这是一个非常短的块:我们将readFile "testfile.txt"
绑定到一个名称,然后在下一行中对该名称进行一些操作。 让我们尝试像编译器一样“脱糖”:
main = readFile "testFile.txt" >>= \strFile -> return (stringAnalyser strFile)
查看>>=
右侧的lambda表单。 这是乞求以无点式重写: \x -> f $ gx
变成\x -> (f . g) x
变成f . g
f . g
。
main = readFile "testFile.txt" >>= (return . stringAnalyser)
这已经比原来的do
块好很多,但我们可以走得更远。
这是唯一需要一点思考的步骤(尽pipe一旦你熟悉单子和函子,它应该是显而易见的)。 上面的函数暗示了monad定律之一 : (m >>= return) == m
。 唯一的区别是>>=
右边的函数不仅仅是return
– 我们在monad中的对象做了一些事情,然后把它包装回去。 但是“在不影响包装的情况下对包装值进行操作”的模式正是Functor
所要做的。 所有单子都是仿函数,所以我们可以重构这个,所以我们甚至不需要Monad
实例:
main = fmap stringAnalyser (readFile "testFile.txt")
最后,请注意<$>
是写fmap
另一种方法。
main = stringAnalyser <$> readFile "testFile.txt"
我认为这个版本比原来的代码更清晰。 它可以像一个句子一样读取:“ main
是stringAnalyser
应用于读取"testFile.txt"
”的结果。 原始版本在操作的过程细节中让你陷入沉寂。
附录:我认为“所有单子都是仿函数”实际上可以通过m >>= (return . f)
fmap fm
m >>= (return . f)
(又名标准图书馆的liftM
)与fmap fm
相同的观察来certificate。 如果你有一个Monad
的实例,你可以免费获得一个Functor
的实例 – 只需定义fmap = liftM
! 如果有人为他们的types定义了一个Monad
实例,但是没有为Functor
和Applicative
定义实例,那么我会给这个错误打个电话。 客户希望能够在Monad
实例上使用Functor
方法,而不会有太多的麻烦。
应该鼓励应用风格,因为它构成(而且更漂亮)。 在某些情况下,Monadic风格是必要的。 请参阅https://stackoverflow.com/a/7042674/1019205进行深入的解释。;
我们应该避免在任何情况下做记号吗?
我会说绝对不是 。 对我来说,在这种情况下最重要的标准是使代码尽可能多的可读性和可理解性 。 引入了do
-notation来使monadic代码更容易理解,这是重要的。 当然,在许多情况下,使用Applicative
免提记法是非常好的,例如,而不是
do f <- [(+1), (*7)] i <- [1..5] return $ fi
我们只会写[(+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
操作如何sorting。 一个免费的符号看起来像
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
,可以尝试将你的代码构造成许多小函数。 无论如何,这是一个很好的习惯,你可以减less你的do
块,只包含2-3行,然后可以用>>=
, <$>,
<*>等来很好地replace。例如,上面可以是改写为
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]
),无点记法是很好的,但是如果您尝试将复杂的expression式,你会得到类似的结果
f = ((ite . (<= 1)) `flip` 1) <*> (((+) . (f . (subtract 1))) <*> (f . (subtract 2))) where ite exy = if e then x else y
如果不运行代码,理解它会花费相当长的时间。 [剧透下方:]
fx = if (x <= 1) then 1 else f (x-1) + f (x-2)
另外,为什么大多数教程都用IO来教IO呢?
因为IO
的devise完全是为了模仿带有副作用的命令式计算,所以使用do
来进行sorting是非常自然的。
记号只是一个句法糖。 在所有情况下都可以避免。 但是,在某些情况下用>>=
replace,而return
使得代码不易读。
所以,对于你的问题:
“在任何情况下,我们应该避免Do的声明?”。
专注于使您的代码更清晰易读。 使用时请帮助,否则就避免使用。
而我还有一个问题,为什么大多数教程都会教IO呢?
因为在很多情况下,IO代码的可读性更好。
而且,大部分开始学习Haskell的人都有必要的编程经验。 教程是为初学者。 他们应该使用新手容易理解的风格。
该符号被扩展为使用函数(>>=)
和(>>)
以及let
expression式的expression式。 所以这不是语言核心的一部分。
(>>=)
和(>>)
用于按顺序组合动作,当动作的结果改变以下动作的结构时,它们是必不可less的。
在问题中给出的例子中,这并不明显,因为只有一个IO
动作,因此不需要sorting。
考虑例如expression式
do x <- getLine print (length x) y <- getLine return (x ++ y)
这被翻译成
getLine >>= \x -> print (length x) >> getLine >>= \y -> return (x ++ y)
在这个例子中,符号(或者(>>=)
和(>>)
函数)被用于IO
操作的sorting。
所以程序员不久就会需要它。