在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" 

我认为这个版本比原来的代码更清晰。 它可以像一个句子一样读取:“ mainstringAnalyser应用于读取"testFile.txt" ”的结果。 原始版本在操作的过程细节中让你陷入沉寂。


附录:我认为“所有单子都是仿函数”实际上可以通过m >>= (return . f) fmap fm m >>= (return . f) (又名标准图书馆的liftM )与fmap fm相同的观察来certificate。 如果你有一个Monad的实例,你可以免费获得一个Functor的实例 – 只需定义fmap = liftM ! 如果有人为他们的types定义了一个Monad实例,但是没有为FunctorApplicative定义实例,那么我会给这个错误打个电话。 客户希望能够在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初学者可能不太理解(由于intersperseconcattraverse ),但是在许多情况下,这些新的小函数可以在代码的其他地方重用,这将使它更加结构化和可组合。


我想说的是,这种情况与是否使用无点记号非常相似。 在许多情况下(如最顶级的例子[(+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的人都有必要的编程经验。 教程是为初学者。 他们应该使用新手容易理解的风格。

该符号被扩展为使用函数(>>=)(>>)以及letexpression式的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。

所以程序员不久就会需要它。