为什么Haskell(有时)被称为“最佳命令式语言”?

(我希望这个问题是关于主题的 – 我试着寻找一个答案,但没有find一个明确的答案。如果这是恰当的话题或已经回答,请温和/删除它。)

我记得听过/读过关于Haskell几次被认为是最好的命令式语言的半开玩笑的评论,这听起来很奇怪,因为Haskell通常以其function特性而闻名。

所以我的问题是,Haskell有什么品质/特性(如果有的话)有理由certificateHaskell被认为是最好的命令式语言 – 或者实际上更像是一个笑话?

我认为这是一个半真相。 Haskell具有惊人的抽象能力,包括对命令性思想的抽象。 例如,Haskell在while循环中没有内置的命令,但我们可以直接编写它,现在它可以:

while :: (Monad m) => m Bool -> m () -> m () while cond action = do c <- cond if c then action >> while cond action else return () 

这种抽象级别对于许多命令式语言来说是困难的。 这可以通过命令式语言来完成, 例如。 Python和C#。

但Haskell也具有(非常独特的)使用Monad类来表征允许的副作用的能力。 例如,如果我们有一个函数:

 foo :: (MonadWriter [String] m) => m Int 

这可能是一个“必要的”function,但是我们知道它只能做两件事:

  • “输出”一串string
  • 返回一个Int

它不能打印到控制台或build立networking连接等。结合抽象能力,您可以编写作用于“任何产生stream的计算”的函数等。

这实际上是关于Haskell的抽象能力,这使得它成为一个非常好的命令语言。

然而,错误的一半是语法。 我发现Haskell在命令式风格中使用起来非常冗长和尴尬。 下面是使用上面的while循环进行命令式计算的示例,它查找链表的最后一个元素:

 lastElt :: [a] -> IO a lastElt [] = fail "Empty list!!" lastElt xs = do lst <- newIORef xs ret <- newIORef (head xs) while (not . null <$> readIORef lst) $ do (x:xs) <- readIORef lst writeIORef lst xs writeIORef ret x readIORef ret 

所有IORef垃圾,双重读取,必须绑定读取的结果,fmapping( <$> )对内联计算的结果进行操作……看起来都非常复杂。 从function的angular度来看,它有很多意义,但是命令式语言倾向于将这些细节中的大部分都清理干净,以使它们更易于使用。

不可否认,也许如果我们使用了不同的while型组合器,它会更干净。 但是,如果你把这个哲学放到了足够的地步(用一组丰富的组合器来清晰地expression自己的意思),那么你就可以再次进入函数式编程。 命令式的Haskell并不像一个精心devise的命令式语言,例如python,“stream动”。

总而言之,在句法上,Haskell可能是最好的命令式语言。 但是,由于脸部表情的提升,将会用外在美丽和假冒的东西来取代内在的美丽和真实。

编辑 :对比lastElt这个python音译:

 def last_elt(xs): assert xs, "Empty list!!" lst = xs ret = xs.head while lst: ret = lst.head lst = lst.tail return ret 

相同的线数,但每一行都有相当less的噪音。


编辑2

对于什么是值得的,这是一个在Haskell 纯粹的替代看起来像:

 lastElt = return . last 

而已。 或者,如果你禁止使用Prelude.last

 lastElt [] = fail "Unsafe lastElt called on empty list" lastElt [x] = return x lastElt (_:xs) = lastElt xs 

或者,如果您希望它在任何Foldable数据结构上工作,并且认识到您实际上不需要 IO来处理错误:

 import Data.Foldable (Foldable, foldMap) import Data.Monoid (Monoid(..), Last(..)) lastElt :: (Foldable t) => ta -> Maybe a lastElt = getLast . foldMap (Last . Just) 

Map ,例如:

 λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String λ➔ lastElt example Just "eggs" 

(.)运算符是函数组合 。

这不是一个笑话,我相信。 我会尽力让那些不认识Haskell的人可以使用它。 Haskell使用do-notation(等等)来让你编写命令式的代码(是的,它使用monad,但是不用担心)。 以下是Haskell为您提供的一些优势:

  • 轻松创build子例程。 假设我想要一个函数将值输出到stdout和stderr。 我可以写下面的内容,用一行简短的代码定义子例程:

     do let printBoth s = putStrLn s >> hPutStrLn stderr s printBoth "Hello" -- Some other code printBoth "Goodbye" 
  • 易于传递代码。 鉴于我已经写了上面的内容,如果我现在要使用printBoth函数打印出所有的string列表,可以通过将我的子程序传递给mapM_函数来轻松完成:

     mapM_ printBoth ["Hello", "World!"] 

    另一个例子,虽然不是必要的,但是正在sorting。 假设你想按stringsortingstring。 你可以写:

     sortBy (\ab -> compare (length a) (length b)) ["aaaa", "b", "cc"] 

    哪个会给你[“b”,“cc”,“aaaa”]。 (你可以写得比这更短,但是现在不要紧。)

  • 易于重复使用的代码。 mapM_函数被使用了很多,并且replace了其他语言的每个循环。 也有forever这样的行为(真),和其他各种function,可以通过代码和执行不同的方式。 所以在其他语言中的循环被Haskell中的这些控制函数取代(这不是特别的 – 你可以很容易地自己定义它们)。 一般情况下,这使得很难得到错误的循环条件,就像每个循环比长时间的迭代器(例如在Java中)或数组索引循环(例如在C中)更难以得到错误。

  • 绑定不分配。 基本上,你只能分配一个variables(而不是一个静态分配)。 这消除了在任何给定点(它的值只设置在一行上)的variables的可能值的许多混淆。
  • 含有副作用。 假设我想从stdin中读取一行,并在应用某个函数后将它写在stdout上(我们将它称为foo)。 你可以写:

     do line <- getLine putStrLn (foo line) 

    我立即知道foo没有任何意想不到的副作用(比如更新全局variables,或者释放内存,或者其他),因为它的types必须是String – > String,这意味着它是一个纯函数。 无论我通过什么样的价值,它都必须每次都返回相同的结果,而没有副作用。 Haskell很好地将纯代码中的副作用代码分开。 在C,甚至Java这样的东西,这不是很明显(是否getFoo()方法改变状态?你不希望,但它可能会…)。

  • 垃圾收集。 很多语言是垃圾收集这些天,但值得一提:没有分配和释放内存的麻烦。

除此之外还可能有更多的优点,但是这些是我们想到的。

除了其他人已经提到的,有副作用的行为是一stream的有时是有用的。 这是一个愚蠢的例子来展示这个想法:

 f = sequence_ (reverse [print 1, print 2, print 3]) 

这个例子展示了如何用副作用build立计算(在这个例子中是print ),然后把数据结构放到数据结构中,或者在实际执行之前用其他方法来处理它们。