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


83

(我希望这个问题是话题性的-我尝试搜索答案,但没有找到明确的答案。如果这个话题不正确或已经回答了,请进行审核/删除。)

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

所以我的问题是,Haskell的哪些特性/特征(如果有的话)有理由证明Haskell被认为是最好的命令性语言,或者实际上是在开玩笑吗?



10
这句话可能起源于处理笨拙小队的简介:Haskell中的单子输入/输出,并发,异常和外语调用,其中说:“简而言之,Haskell是世界上最好的命令式编程语言。”
罗素·奥康纳

@Russel:感谢您指出这句话的最可能起源(看来是SPJ自己)!
hvr

您可以使用Haskell进行严格的命令性OO:ekmett / structs
Janus Troelsen

Answers:


90

我认为这是事实。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

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

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

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

实际上,这完全取决于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不会像精心设计的命令式语言(例如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"

(.)操作者是函数的组合物


2
您可以通过创建更多抽象来减少IORef噪音。
2011年

1
@augustss,嗯,我对此很好奇。您是指更多的领域级抽象,还是仅仅通过构建更丰富的命令式子语言。”对于前者,我同意-但我的想法是将命令式编程与低抽象联系在一起(我的工作假设是,随着抽象的增加,样式–后者,我很想知道您的意思,因为我
想不起来

2
@luqui使用ST将是表征允许的副作用的一个很好的例子。作为奖励,可以从ST跳回到纯计算。
2011年

4
使用Python进行比较并不完全公平-正如您所说的,它经过精心设计,是我所熟悉的语法上最简洁的命令性语言之一。相同的比较表明,大多数命令式语言很难以命令式方式使用...不过,也许这正是您的意思。;]
CA McCann

5
对话的脚注,以供后代使用IORef@augustss使用即席多态性使s隐式出现,或者至少尝试对GHC进行更改并受到其阻碍。:[
CA McCann

22

这不是在开玩笑,我相信。对于那些不了解Haskell的人,我将尝试使其保持可访问状态。Haskell使用do表示法(除其他外)来允许您编写命令性代码(是的,它使用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中)更容易出错。

  • 绑定不赋值。基本上,您只能分配一次变量(就像单次静态分配一样)。这消除了在任何给定点上变量可能的值的很多困惑(其值仅设置在一行上)。
  • 所包含的副作用。假设我想从stdin中读取一行,并在对其应用一些功能(我们将其称为foo)后将其写到stdout上。你可以写:

    do line <- getLine
       putStrLn (foo line)
    

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

  • 垃圾收集。如今,许多语言都被垃圾回收了,但是值得一提:分配和释放内存没有麻烦。

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


9
我会添加强类型安全性。Haskell允许编译器消除大量错误。最近处理了一些Java代码之后,我想起了空指针多么可怕,并且在没有求和类型的情况下缺少了多少OOP。
Michael Snoyman 2011年

1
谢谢您的精心安排!您提到的优势似乎可以归结为Haskell将“命令式”效果视为一流对象(因此可以组合)以及将这些效果“包含”在限定范围内的能力。这是足够的压缩摘要吗?
2011年

19
@Michael Snoyman:但是在OOP中求和类型很容易!只需定义一个代表您数据类型的Church编码的抽象类,用例的子类,可以处理各种用例的类的接口,然后使用子类型多态性(如控制流)将支持每个接口的对象传递给sum对象。你应该)。再简单不过了。您为什么讨厌设计模式?
CA McCann

9
@camccann我知道你在开玩笑,但这本质上就是我在项目中实现的。
Michael Snoyman 2011年

9
@迈克尔·斯诺曼:那就好选择!真正的笑话是我以听起来像个笑话来描述什么是最好的编码。哈哈!一直笑到绞刑架……
CA McCann

16

除了已经提到的内容外,将副作用动作列为一流也是有时有用的。这是一个愚蠢的例子来展示这个想法:

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

本示例说明了如何建立具有副作用的计算(在本示例中print),然后将其放入数据结构或以其他方式对其进行处理,然后再实际执行它们。


我认为与此相对应的javascript代码将是:call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse())。我看到的主要区别是我们需要一些额外的东西() =>
胡勒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.