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

时间:2011-07-08 09:31:12

标签: haskell imperative-programming

(我希望这个问题是关于主题的 - 我试图寻找答案,但没有找到明确的答案。如果这恰好是偏离主题或已经回答,请缓和/删除它。 )

我记得曾经多次听过/读过关于Haskell成为最佳命令式语言的半开玩笑的评论,这当然听起来很奇怪,因为Haskell通常因其功能<而闻名/ em>功能。

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

3 个答案:

答案 0 :(得分:87)

我认为这是一个半真半假的事实。 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式组合器,它会更清洁。但是如果你采用这种哲学足够远(使用丰富的组合器来清楚地表达自己),那么你再次进入函数式编程。势在必行的Haskell并不像设计良好的命令式语言那样“流动”,例如,蟒。

总之,通过语法改头换面,Haskell可能是最好的命令式语言。但是,通过面部提升的性质,它将取代内部美丽和真实的东西,外观美观和假。

编辑:使用此python音译对比lastElt

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"

(.)运算符为function composition

答案 1 :(得分:22)

这不是开玩笑,我相信它。我会尝试让那些不了解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_函数使用了很多,并替换了其他语言中的每个循环。还有forever就像一段时间(真实),以及可以传递代码并以不同方式执行它的各种其他函数。因此,其他语言中的循环被Haskell中的这些控制函数所取代(这些函数并不特殊 - 您可以非常轻松地自己定义它们)。一般来说,这使得很难让循环条件出错,就像for-each循环比长手迭代器等价物(例如在Java中)或数组索引循环(例如在C中)更难弄错。

  • 绑定不分配。基本上,您只能分配一次变量(而不是单个静态赋值)。这消除了在任何给定点上变量的可能值的大量混淆(其值仅在一行上设置)。
  • 包含副作用。假设我想从stdin中读取一行,并在将一些函数应用到stdout后将其写入stdout(我们称之为foo)。你可以写:

    do line <- getLine
       putStrLn (foo line)
    

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

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

除此之外,还有一些优点,但那些是我想到的。

答案 2 :(得分:15)

除了已经提到过的其他内容之外,将副作用行为视为一流有时也是有用的。这是一个显示这个想法的愚蠢的例子:

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

此示例显示了如何构建带副作用的计算(在此示例中为print),然后在实际执行之前将数据结构放入或以其他方式对其进行操作。