在Haskell中应该避免使用表示法吗?

Jul*_*ang 38 haskell

大多数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%的时间使用它们,主要是因为我倾向于编写非常小的可组合功能,无论如何应用程序非常适合.

  • 你描述的desugaring确实很简单,但天真.Do-notation还涉及与Monad失败函数相关的错误处理,但您没有提及它.不过,这个答案非常有用,我已多次引用它.来自我的+1. (2认同)

lef*_*out 40

在我看来<$>,<*>使代码更多FP比IO.

Haskell不是一种纯函数式语言,因为它"看起来更好".有时确实如此,通常不会.保持功能的原因不是它的语法,而是它的语义.它装备我们引用透明,这使得它更容易证明不变,允许非常高层次的优化,可以很容易地编写通用代码等.

这些都与语法无关.一元计算仍然是纯粹的功能-无论你写他们用do符号或<$>,<*>>>=,所以我们得到Haskell的好处无论哪种方式.

然而,尽管有前面提到的FP优势,但从类似命令的角度考虑算法通常更直观 - 即使你已经习惯了如何通过monad实现它.在这些情况下,do符号为您提供了"计算顺序","数据来源","修改点"的快速洞察,然而在您的脑海中手动去除它与>>=版本,以掌握功能上发生的事情是微不足道的.

应用风格在很多方面肯定很棒,但它本质上是无点的.这通常是一件好事,但特别是在更复杂的问题中,将名称赋予"临时"变量会非常有帮助.当仅使用"FP"Haskell语法时,这需要lambdas或显式命名的函数.两者都有很好的用例,但前者在代码中间引入了相当多的噪音,而后者则会破坏"流",因为它需要一个wherelet放在你使用它的地方的其他地方.do另一方面,允许您在需要的地方引入命名变量,而不会引入任何噪声.

  • 我赞同这个答案.避免"do"符号只是因为它"看起来势在必行"会适得其反(它通常会使代码的可读性降低).对作业等使用正确的<del>工具</ del>语法 (16认同)

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)

我认为这个版本比原始代码更清晰.它可以读取类似的句子:" mainstringAnalyser应用到阅读的结果"testFile.txt"".原始版本在其操作的程序细节中陷入困境.


附录:我的评论说,"所有的单子都是仿函数"实际上可以被观察到是合理的m >>= (return . f)(又名标准库liftM)是一样的fmap f m.如果你有一个实例Monad,你得到一个Functor'免费' 的实例- 只需定义fmap = liftM!如果有人Monad为他们的类型定义了一个实例而没有为Functor和实例定义Applicative,我会称之为bug.客户希望能够FunctorMonad没有太多麻烦的情况下使用方法.

  • 值得庆幸的是,基础4.8使`Applicative`(因此`Functor`)成为`Monad`的超类,这是一个没有人抱怨的重大变化.很快,就没有必要*想*你的'Monad`是否是一个'Functor`! (3认同)

che*_*eeo 9

应该鼓励应用风格,因为它组成(并且它更漂亮).在某些情况下,Monadic风格是必要的.有关详细说明,请参阅/sf/answers/492987211/.


Pet*_*lák 9

在任何情况下我们都应该避免使用记号吗?

绝对不会说.对我来说,在这种情况下最重要的标准是使代码尽可能易读和易懂.该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,concattraverse),但在许多情况下,这些新的小功能,可以在代码中的其他地方,这将使其更结构化和组合的重复使用.


我会说情况与是否使用无点符号非常相似.在很多情况下(比如在最顶层的例子中[(+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非常自然.


Ser*_*gov 6

do符号只是一种语法糖.它可以在所有情况下是可以避免的.然而,在某些情况下,取代do使用>>=return使代码的可读性.

所以,对于你的问题:

"在任何情况下,我们都应该避免使用Do声明吗?"

专注于使您的代码清晰,更易读.有用do时使用,否则请避免使用.

我有另一个问题,为什么大多数教程都会教IO?

因为do在许多情况下使IO代码更易读.

此外,大多数开始学习Haskell的人都有必要的编程经验.教程适合初学者.他们应该使用新手容易理解的风格.