88

(我希望这个问题是关于主题的——我尝试寻找答案,但没有找到明确的答案。如果这恰好是题外话或已经回答,请适度/删除它。)

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

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

4

3 回答 3

93

我认为这是半真半假。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

这可能是一个“命令式”功能,但我们知道它只能做两件事:

  • “输出”一个字符串流
  • 返回一个 Int

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

实际上,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 ( <$>) 以对内联计算的结果进行操作……这一切看起来都非常复杂。从功能的角度来看,这很有意义,但是命令式语言倾向于将这些细节中的大部分隐藏起来,以使它们更易于使用。

诚然,如果我们使用不同的while-style 组合器,它可能会更干净。但是,如果您将这种理念走得足够远(使用一组丰富的组合器来清楚地表达自己),那么您就会再次到达函数式编程。命令式的 Haskell 只是不像精心设计的命令式语言(例如 python)那样“流动”。

总之,通过语法上的改进,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 

行数相同,但每行的噪音要少得多。


编辑 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) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

Map例如:

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

运算符(.)函数组合

于 2011-07-08T09:59:51.770 回答
23

这不是玩笑,我相信。我会尽量让那些不了解 Haskell 的人可以使用它。Haskell 使用 do-notation(除其他外)允许您编写命令式代码(是的,它使用 monad,但不要担心)。以下是 Haskell 为您提供的一些优势:

  • 轻松创建子程序。假设我想要一个函数将值打印到 stdout 和 stderr。我可以编写以下内容,用一条短线定义子例程:

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

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

    另一个例子,虽然不是必须的,是排序。假设您只想按长度对字符串进行排序。你可以写:

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

    这会给你[“b”,“cc”,“aaaa”]。(你也可以把它写得更短,但暂时不要介意。)

  • 易于重用的代码。该mapM_函数被大量使用,并替换了其他语言中的 for-each 循环。还有forever一些类似于 while (true) 的行为,以及可以传递代码并以不同方式执行的各种其他函数。因此,其他语言中的循环被 Haskell 中的这些控制函数所取代(这并不特殊——您可以很容易地自己定义它们)。一般来说,这使得循环条件很难出错,就像 for-each 循环比长手迭代器等价物(例如在 Java 中)或数组索引循环(例如在 C 中)更难出错一样。

  • 绑定而不是赋值。基本上,你只能给一个变量赋值一次(就像单个静态赋值一样)。这消除了对变量在任何给定点的可能值的很多混淆(它的值只设置在一行上)。
  • 含有副作用。假设我想从标准输入读取一行,并在对其应用一些函数后将其写入标准输出(我们称之为 foo)。你可以写:

    do line <- getLine
       putStrLn (foo line)
    

    我马上就知道foo它没有任何意外的副作用(比如更新全局变量,或者释放内存等等),因为它的类型必须是 String -> String,这意味着它是一个纯函数;无论我传递什么值,它每次都必须返回相同的结果,没有副作用。Haskell 很好地将副作用代码与纯代码区分开来。在 C 甚至 Java 之类的东西中,这并不明显(getFoo() 方法会改变状态吗?你希望不会,但它可能会……)。

  • 垃圾收集。现在很多语言都被垃圾收集了,但值得一提的是:没有分配和释放内存的麻烦。

除此之外,可能还有其他一些优点,但这些都是我想到的。

于 2011-07-08T10:01:58.120 回答
17

除了其他人已经提到的之外,将副作用操作设置为一流有时很有用。这是一个愚蠢的例子来展示这个想法:

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

这个例子展示了如何建立带有副作用的计算(在这个例子中print),然后在实际执行它们之前将它们放入数据结构或以其他方式操作它们。

于 2011-07-08T11:20:28.380 回答