(我希望这个问题是关于主题的 - 我试图寻找答案,但没有找到明确的答案。如果这恰好是偏离主题或已经回答,请缓和/删除它。 )
我记得曾经多次听过/读过关于Haskell成为最佳命令式语言的半开玩笑的评论,这当然听起来很奇怪,因为Haskell通常因其功能<而闻名/ em>功能。
所以我的问题是,Haskell的哪些特性/特性(如果有的话)有理由证明Haskell被认为是最好的命令式语言 - 或者它实际上更像是一个笑话?
答案 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
这可能是一个“命令式”功能,但我们知道它只能做两件事:
无法打印到控制台或建立网络连接等。结合抽象功能,您可以编写作用于“任何产生流的计算”等的函数。
真正关于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
),然后在实际执行之前将数据结构放入或以其他方式对其进行操作。