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

时间:2013-05-24 02:06:46

标签: haskell

大多数Haskell教程都教授使用IO进行编号。

我也开始使用do-notation,但这使得我的代码看起来更像是一种命令式语言而不是FP语言。

本周我看到一个教程使用IO <$>

stringAnalyzer <$> readFile "testfile.txt"

而不是使用do

main = do
    strFile <- readFile "testfile.txt"
    let analysisResult = stringAnalyzer strFile
    return analysisResult

日志分析工具在没有do的情况下完成。

所以我的问题是“我们应该在任何情况下避免使用记号吗?”。

我知道也许do会在某些情况下使代码变得更好。

此外,为什么大多数教程都会使用do教授IO?

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

7 个答案:

答案 0 :(得分:40)

  

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

Haskell不是纯粹的功能语言,因为它“看起来更好”。有时确实如此,通常不会。保持功能的原因不是它的语法,而是它的语义。它为我们提供了参考透明度,这使得更容易证明不变量,允许非常高级的优化,使编写通用代码等变得容易。

这些都与语法无关。 Monadic计算仍然纯粹是功能性的 - 无论你是用do表示还是用<$><*>>>=编写它们,所以我们得到Haskell的好处。

然而,尽管有上述FP优势,但从类似命令的角度考虑算法通常更直观 - 即使您已经习惯了如何通过monad实现这一点。在这些情况下,do表示法可以让您快速了解“计算顺序”,“数据来源”,“修改点”,然而在您的脑海中手动将它去除>>=是微不足道的。 }版本,以掌握功能上正在发生的事情。

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

答案 1 :(得分:39)

Haskell desugars中的

do符号非常简单。

do
  x <- foo
  e1 
  e2
  ...

变成

 foo >>= \x ->
 do
   e1
   e2

do
  x
  e1
  e2
  ...

x >>
do 
  e1
  e2
  ....

这意味着您可以使用>>=return编写任何monadic计算。我们不这样做的唯一原因是因为它只是更痛苦的语法。 Monads对于模仿命令式代码很有用,do符号使它看起来像它。

C-ish语法使初学者更容易理解它。你是对的,它看起来不那么实用,但要求有人在使用IO之前正确地修改monad是一个非常大的威慑力。

另一方面,我们使用>>=return的原因是因为它的很多对于1-2个内衬来说更紧凑。然而,对于任何太大的东西,它确实会变得更难以理解。所以要直接回答你的问题,请不要在适当的时候避免做符号。

最后,您看到的两个运算符<$><*>实际上分别是fmap和applicative,而不是monadic。它们实际上不能用于表示符号所做的很多事情。它们确实更紧凑,但它们不会让您轻易命名中间值。就个人而言,我大约80%的时间使用它们,主要是因为我倾向于编写非常小的可组合函数,无论如何应用程序非常适合。

答案 2 :(得分:34)

我经常发现自己首先用do符号写一个monadic动作,然后将它重构为一个简单的monadic(或functorial)表达式。这种情况主要发生在do块比我预期的更短的情况下。有时我会反方向重构;这取决于有问题的代码。

我的一般规则是:如果do块只有几行长,那么它通常比较短的表达式。较长的do - 块可能更具可读性,除非您能找到一种方法将其分解为更小,更可组合的函数。


作为一个有效的例子,我们可以将您的详细代码片段转换为简单的代码片段。

main = do
    strFile <- readFile "testfile.txt"
    let analysisResult = stringAnalyzer strFile
    return analysisResult

首先,请注意最后两行的格式为let x = y in return x。这当然可以简化为return y

main = do
    strFile <- readFile "testfile.txt"
    return (stringAnalyzer strFile)

这是一个非常短的do块:我们将readFile "testfile.txt"绑定到一个名称,然后在下一行中对该名称执行某些操作。让我们像编译器一样尝试“解糖”:

main = readFile "testFile.txt" >>= \strFile -> return (stringAnalyser strFile)

查看>>=右侧的lambda形式。它要求以无点样式重写:\x -> f $ g x变为\x -> (f . g) x,变为f . g

main = readFile "testFile.txt" >>= (return . stringAnalyser)

这已经比最初的do块更整洁,但我们可以更进一步。

这是唯一需要一点思考的步骤(虽然一旦熟悉了monad和functor,它应该是显而易见的)。上述功能提示monad laws(m >>= return) == m之一。唯一的区别是>>=右侧的函数不仅仅是return - 我们对monad中的对象执行某些操作,然后将其重新包装在return中。但是“在不影响其包装器的情况下对包装值执行操作”的模式正是Functor的用途。所有monad都是仿函数,所以我们可以重构它,这样我们甚至不需要Monad实例:

main = fmap stringAnalyser (readFile "testFile.txt")

最后,请注意<$>只是撰写fmap的另一种方式。

main = stringAnalyser <$> readFile "testFile.txt"

我认为这个版本比原始代码更清晰。它可以像句子一样阅读:“main stringAnalyser应用于阅读"testFile.txt"的结果”。原版本在操作的程序细节中陷入困境。


附录:我的评论'所有monad都是仿函数'实际上可以通过m >>= (return . f)(又名标准库的liftM)与fmap f m相同的观察来证明。如果您有Monad的实例,则可以免费获得Functor'的实例 - 只需定义fmap = liftM!如果有人为其类型定义了Monad个实例,但没有为FunctorApplicative定义实例,我会称之为错误。客户希望能够在Functor的实例上使用Monad方法而不会有太多麻烦。

答案 3 :(得分:9)

应鼓励应用风格,因为它构成(并且更漂亮)。在某些情况下,Monadic风格是必要的。有关详细说明,请参阅https://stackoverflow.com/a/7042674/1019205

答案 4 :(得分:9)

  

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

我会说肯定没有。对我来说,在这种情况下最重要的标准是使代码尽可能易读和易懂。引入do - 符号是为了使monadic代码更容易理解,这才是最重要的。当然,在许多情况下,使用Applicative无点符号非常很好,例如,而不是

do
    f <- [(+1), (*7)]
    i <- [1..5]
    return $ f i

我们只写[(+1), (*7)] <*> [1..5]

但是有很多例子没有使用do - 符号会使代码变得非常难以理解。考虑this example

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 - 免费符号看起来像

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行,然后>>=<$>,&lt; *&gt;可以很好地替换它们。例如,上面的内容可以改写为

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),但在很多情况下,这些新的小函数可能是在代码的其他地方重用,这将使其更具结构性和可组合性。


我说情况与是否使用point-free notation非常相似。在很多情况下(比如最顶层的例子[(+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)


  

另外,为什么大多数教程都教IO做?

因为IO完全是为了模仿具有副作用的命令式计算而设计的,所以使用do对其进行排序是非常自然的。

答案 5 :(得分:6)

do符号只是一种语法糖。在所有情况下都可以避免 。但是,在某些情况下,将do替换为>>=return会降低代码的可读性。

所以,对于你的问题:

  

&#34;在任何情况下我们都应该避免使用Do语句吗?&#34;。

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

  

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

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

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

答案 6 :(得分:1)

使用函数do(>>=)以及(>>)表达式将let表示法扩展为表达式。所以它不是语言核心的一部分。

(>>=)(>>)用于按顺序组合操作,当操作结果更改以下操作的结构时,它们是必不可少的。

在问题中给出的示例中,这并不明显,因为只有一个IO动作,因此不需要排序。

考虑例如表达式

do x <- getLine
   print (length x)
   y <- getLine
   return (x ++ y)

被翻译为

getLine >>= \x ->
print (length x) >>
getLine >>= \y ->
return (x ++ y)

在此示例中,do操作的排序需要(>>=)符号(或(>>)IO函数)。

程序员很快就会需要它。