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

hvr*_*hvr 81 haskell imperative-programming

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

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

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

luq*_*qui 88

我认为这是一个半真半假的事实.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 ()
Run Code Online (Sandbox Code Playgroud)

对于许多命令式语言来说,这种抽象级别很难.这可以在具有闭包的命令式语言中完成; 例如.Python和C#.

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

foo :: (MonadWriter [String] m) => m Int
Run Code Online (Sandbox Code Playgroud)

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

  • "输出"一串字符串
  • 返回一个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
Run Code Online (Sandbox Code Playgroud)

所有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 
Run Code Online (Sandbox Code Playgroud)

相同数量的线,但每条线的噪音都相当少.


编辑2

对于它的价值,这就是Haskell 的纯粹替代品的样子:

lastElt = return . last
Run Code Online (Sandbox Code Playgroud)

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

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs
Run Code Online (Sandbox Code Playgroud)

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

import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)
Run Code Online (Sandbox Code Playgroud)

Map,例如:

?? let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
?? lastElt example
Just "eggs"
Run Code Online (Sandbox Code Playgroud)

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

  • 对话的一个脚注,为后人:[@augustss使用ad-hoc多态来使`IORef'隐含](http://augustss.blogspot.com/2011/07/impredicative-polymorphism-use-case-in. html),或者至少试图和被GHC的变化所阻挠.:[ (5认同)
  • 使用Python作为比较并不完全公平 - 正如你所说,它是精心设计的,是我熟悉的语法上最干净的命令式语言之一.同样的比较认为,大多数命令式语言在命令式风格中使用都很笨拙......但是,也许这正是你的意思.] (4认同)
  • 你可以通过更多的抽象来减少IORef噪音. (2认同)
  • @luqui使用ST将是表征允许的副作用的一个很好的例子.作为奖励,可以从ST跳回到纯粹的计算中. (2认同)

Nei*_*own 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"
    
    Run Code Online (Sandbox Code Playgroud)
  • 易于传递代码.鉴于我已经编写了上述内容,如果我现在想要使用该printBoth函数打印出所有字符串列表,那么通过将子例程传递给mapM_函数可以轻松完成:

    mapM_ printBoth ["Hello", "World!"]
    
    Run Code Online (Sandbox Code Playgroud)

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

    sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
    
    Run Code Online (Sandbox Code Playgroud)

    哪个会给你["b","cc","aaaa"].(你也可以把它写得比这短,但现在也没关系.)

  • 易于重用代码.该mapM_函数经常使用,并替换其他语言中的每个循环.还有forever一些行为像一段时间(真实),以及各种其他功能,可以传递代码并以不同的方式执行它.因此,其他语言中的循环被Haskell中的这些控制函数所取代(这些函数并不特殊 - 您可以非常轻松地自己定义它们).通常,这使得很难使循环条件错误,就像 - 每个循环比长手迭代器等价物(例如在Java中)或数组索引循环(例如在C中)更难弄错.

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

    do line <- getLine
       putStrLn (foo line)
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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

  • @Michael Snoyman:但总和类型在OOP中很容易!只需定义一个抽象类,它表示数据类型的Church编码,case的子类,可处理每个case的类的接口,然后将支持每个接口的对象传递给sum对象,使用子类型多态性来控制流(如你应该).不可能更简单.你为什么讨厌设计模式? (19认同)
  • 我会加入强大的类型安全性.Haskell允许编译器消除大量错误.在最近处理了一些Java代码之后,我被提醒了一下,如果没有和类型,有多么糟糕的空指针是多么缺乏OOP. (9认同)
  • @camccann我知道你在开玩笑,但这基本上就是我在项目中实现的内容. (9认同)
  • @Michael Snoyman:不错的选择吧!真正的笑话是,我用一种听起来像笑话的方式描述了几乎最好的编码.哈哈!一路笑到绞刑架...... (9认同)

tib*_*bbe 16

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

f = sequence_ (reverse [print 1, print 2, print 3])
Run Code Online (Sandbox Code Playgroud)

此示例显示如何print在实际执行它们之前如何构建具有副作用的计算(在此示例中),然后放入数据结构或以其他方式操作它们.