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

n53p2ov0  于 2023-03-19  发布在  其他
关注(0)|答案(4)|浏览(292)
  • (我希望此问题与主题相关--我尝试搜索答案,但未找到明确答案。如果此问题恰好与主题无关或已回答,请缓和/删除它。)*

我记得我听过/读过几次关于Haskell是“最好的命令式语言”的半开玩笑的评论,这当然听起来很奇怪,因为Haskell通常以其“功能性”特性而闻名。
所以我的问题是,Haskell的哪些品质/特性(如果有的话)给予了Haskell被认为是最好的命令式语言--或者它实际上更像是一个笑话?

5jvtdoz2

5jvtdoz21#

我认为这是半真半假的。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(<$>)操作内联计算的结果......看起来都非常复杂。从 * 函数 * Angular 来看,这非常有意义,但命令式语言倾向于掩盖这些细节,以使它们更易于使用。
诚然,如果我们使用一个不同的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"

(.)运算符是function composition

0tdrvxhp

0tdrvxhp2#

这不是玩笑,我相信这一点。我会尽量让那些不知道Haskell的人也能理解。Haskell使用do符号(还有其他一些东西)来让你编写命令式代码(是的,它使用单子,但不用担心)。下面是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(真),和各种其他函数,这些函数可以传递代码并以不同的方式执行代码。因此,其他语言中的循环被Haskell中的这些控制函数所取代(这并不特别--你可以自己定义它们)一般来说,这使得循环条件很难出错,就像for-each循环比长手迭代器(例如在Java中)或数组索引循环(例如在C中)更难出错。
  • 绑定而不是赋值。基本上,你只能给一个变量赋值一次(有点像单次静态赋值)。这消除了很多关于变量在任何给定点的可能值的困惑(它的值只在一行上设置)。
  • 包含副作用。假设我想从stdin读取一行,并在应用某个函数后将其写入stdout(我们称之为foo)。您可以编写:
do line <- getLine
   putStrLn (foo line)

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

  • 垃圾收集。现在很多语言都是垃圾收集的,但值得一提的是:没有分配和解除分配存储器的麻烦。

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

f0brbegy

f0brbegy3#

除了其他人已经提到过的,有时候把副作用放在第一位是有用的,下面是一个愚蠢的例子来说明这个想法:

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

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

eyh26e7m

eyh26e7m4#

使用与this answer中的@Chi相同的示例,可以使用State单子来模拟带有递归的命令式循环:

C代码:

// sum 0..100
i = s = 0;
while (i <= 100) {
   s = s+i;
   i++;
}
return s;

haskell 代码:

import Control.Monad.State
final_s :: Int
final_s = evalState sum_loop (0, 0)  -- evaluate loop with initial state (0, 0)
sum_loop :: State (Int, Int) Int
sum_loop = do
  (i, s) <- get           -- retrieve current state
  if i <= 100             -- check loop condition
    then do               -- if condition is true:
      let new_s = s + i
      let new_i = i + 1   
      put (new_i, new_s)  -- update state with new tuple
      sum_loop            -- recursively call loop with new state, simulate iteration with recursion
    else
      return s            -- if condition is false, return s as final result

main = print final_s

正如您所看到的,这与C代码非常相似,我们只多了3行代码:

  • (i, s)〈- get以获取当前状态。
  • put (new_i, new_s)以使用新状态更新当前状态
  • sum_loop使用新状态递归调用循环,使用递归模拟迭代

您可以使用put $ traceShowId (new_i, new_s)而不是put (new_i, new_s)添加debug only printing,但是您应该只将其用于调试,因为它欺骗了类型系统。
因此,有一些事情需要“手动”处理,但可以用Haskell编写可读性相当好的命令式代码。

相关问题