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

发布于 2024-11-18 20:06:12 字数 289 浏览 3 评论 0原文

(我希望这个问题是切中主题的——我尝试寻找答案,但没有找到明确的答案。如果这个问题恰好偏离主题或已经得到回答,请审核/删除它。)

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

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

(I hope this question is on-topic -- I tried searching for an answer but didn't find a definitive answer. If this happens to be off-topic or already answered, please moderate/remove it.)

I remember having heard/read the half-joking comment about Haskell being the best imperative language a few times, which of course sounds weird as Haskell is usually best known for its functional features.

So my question is, what qualities/features (if any) of Haskell give reason to justify Haskell being deemed the best imperative language -- or is it actually more of a joke?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

随梦而飞# 2024-11-25 20:06:12

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

(.) 运算符是 功能组合

I consider it a half-truth. Haskell has an amazing ability to abstract, and that includes abstraction over imperative ideas. For example, Haskell has no built-in imperative while loop, but we can just write it and now it does:

while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
    c <- cond
    if c 
        then action >> while cond action
        else return ()

This level of abstraction is difficult for many imperative languages. This can be done in imperative languages that have closures; eg. Python and C#.

But Haskell also has the (highly unique) ability to characterize allowed side-effects, using the Monad classes. For example, if we have a function:

foo :: (MonadWriter [String] m) => m Int

This can be an "imperative" function, but we know that it can only do two things:

  • "Output" a stream of strings
  • return an Int

It can't print to the console or establish network connections, etc. Combined with the abstraction ability, you can write functions which act on "any computation that produces a stream", etc.

It's really all about Haskell's abstraction abilities that makes it a very fine imperative language.

However, the false half is the syntax. I find Haskell pretty verbose and awkward to use in an imperative style. Here is an example imperative-style computation using the above while loop, which finds the last element of a linked list:

lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
    lst <- newIORef xs
    ret <- newIORef (head xs)
    while (not . null <
gt; readIORef lst) $ do
        (x:xs) <- readIORef lst
        writeIORef lst xs
        writeIORef ret x
    readIORef ret

All that IORef garbage, the double read, having to bind the result of a read, fmapping (<$>) to operate on the result of an inline computation... it's all just very complicated looking. It makes a whole lot of sense from a functional perspective, but imperative languages tend to sweep most of these details under the rug to make them easier to use.

Admittedly, perhaps if we used a different while-style combinator it would be cleaner. But if you take that philosophy far enough (using a rich set of combinators to express yourself clearly), then you arrive at functional programming again. Imperative-style Haskell just doesn't "flow" like a well-designed imperative language, e.g. python.

In conclusion, with a syntactic face-lift, Haskell might well be the best imperative language. But, by the nature of face lifts, it would be replacing something internally beautiful and real with something externally beautiful and fake.

EDIT: Contrast lastElt with this python transliteration:

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

Same number of lines, but each line has quite a bit less noise.


EDIT 2

For what it's worth, this is how a pure replacement in Haskell looks like:

lastElt = return . last

That's it. Or, if you forbid me from using Prelude.last:

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs

Or, if you want it to work on any Foldable data structure and recognize that you don't actually need IO to handle errors:

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

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

with Map, for example:

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

The (.) operator is function composition.

玩世 2024-11-25 20:06:12

这不是玩笑,我相信。我会尽力让那些不懂 Haskell 的人也能理解它。 Haskell 使用 do 表示法(除其他外)允许您编写命令式代码(是的,它使用 monad,但不用担心)。以下是 Haskell 为您提供的一些优势:

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

    do let printBoth s = putStrLn s >>> hPutStrLn 标准错误
       打印两个“你好”
       -- 其他一些代码
       打印两个“再见”
    
  • 易于传递代码。鉴于我已经编写了上述内容,如果我现在想使用 printBoth 函数打印出所有字符串列表,只需将我的子例程传递给 mapM_ 即可轻松完成代码>功能:

    mapM_ printBoth [“你好”,“世界!”]
    

    另一个例子是排序,尽管不是必需的。假设您想仅按长度对字符串进行排序。你可以写:

    sortBy (\ab -> 比较 (长度 a) (长度 b)) ["aaaa", "b", "cc"]
    

    这会给你[“b”,“cc”,“aaaa”]。 (您也可以将其写得更短,但现在不用介意。)

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

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

    do line <- getLine
       putStrLn(foo 行)
    

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

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

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

It's not a joke, and I believe it. I'll try to keep this accessible for those who don't know any Haskell. Haskell uses do-notation (among other things) to allow you to write imperative code (yes, it uses monads, but don't worry about that). Here's some of the advantages that Haskell gives you:

  • Easy creation of subroutines. Let's say that I want a function to print a value to stdout and stderr. I can write the following, defining the subroutine with one short line:

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • Easy to pass code around. Given that I've written the above, if I now want to use the printBoth function to print out all of a list of strings, that's easily done by passing my subroutine to the mapM_ function:

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

    Another example, although not imperative, is sorting. Let's say you want to sort strings solely by length. You can write:

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

    Which will give you ["b", "cc", "aaaa"]. (You can write it shorter than that, too, but never mind for now.)

  • Easy to re-use code. That mapM_ function is used a lot, and replaces for-each loops in other languages. There's also forever which acts like a while (true), and various other functions that can be passed code and execute it in different ways. So loops in other languages are replaced by these control functions in Haskell (which are not special -- you can define them yourself very easily). In general this makes it hard to get the loop condition wrong, just like for-each loops are harder to get wrong than the long-hand iterator equivalents (e.g. in Java), or array-indexing loops (e.g. in C).

  • Binding not assignment. Basically, you can only assign to a variable once (rather like single static assignment). This removes a lot of confusion about the possible values of a variable at any given point (its value is only set on one line).
  • Contained side effects. Let's say that I want to read a line from stdin, and write it on stdout after applying some function to it (we'll call it foo). You can write:

    do line <- getLine
       putStrLn (foo line)
    

    I know immediately that foo doesn't have any unexpected side effects (like updating a global variable, or deallocating memory, or whatever), because it's type must be String -> String, which means it is a pure function; whatever value I pass, it must return the same result every time, without side effects. Haskell nicely separates the side-effecting code from the pure code. In something like C, or even Java, this is not obvious (does that getFoo() method change state? You'd hope not, but it might do...).

  • Garbage collection. A lot of languages are garbage collected these days, but worth mentioning: no hassles of allocating and deallocating memory.

There's probably a few more advantages besides, but those are the ones that come to mind.

找回味觉 2024-11-25 20:06:12

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

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

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

In addition to what other's have already mentioned, having side-effecting actions be first-class is sometimes useful. Here's a silly example to show the idea:

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

This example shows how you can build up computations with side-effects (in this example print) and then put the in data structures or manipulate them in other ways, before actually executing them.

满身野味 2024-11-25 20:06:12

使用与此答案中的@Chi相同的示例,您可以使用 State monad 来模拟带有递归的命令式循环:

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 以新状态递归调用循环,用递归模拟迭代

您可以添加仅调试打印 < code>put $traceShowId (new_i, new_s) 而不是 put (new_i, new_s),但您应该仅将其用于调试,因为它会欺骗类型系统。

因此,还有一些事情必须“手动”处理,但可以在 Haskell 中编写合理可读的命令式代码。

Using the same example as @Chi in this answer, you can use the State monad to simulate an imperative loop with recursion:

C code:

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

Haskell code:

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

As you can see this is quite similar to the C code, we just have 3 more lines:

  • (i, s) <- get to get the current state.
  • put (new_i, new_s) to update the current state with the new state
  • sum_loop to recursively call loop with new state, simulating iteration with recursion

You can add debug only printing with put $ traceShowId (new_i, new_s) instead of put (new_i, new_s), but you should only use this for debugging because it cheats the type system.

So a few things more things have to handled "manually" but it is possible to write reasonably readable imperative code in Haskell.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文