(我希望这个问题是话题性的-我尝试搜索答案,但没有找到明确的答案。如果这个话题不正确或已经回答了,请进行审核/删除。)
我记得几次听到/读过关于Haskell是最好的命令式语言的开玩笑的评论,这听起来当然很奇怪,因为Haskell通常以其功能而闻名。
所以我的问题是,Haskell的哪些特性/特征(如果有的话)有理由证明Haskell被认为是最好的命令性语言,或者实际上是在开玩笑吗?
(我希望这个问题是话题性的-我尝试搜索答案,但没有找到明确的答案。如果这个话题不正确或已经回答了,请进行审核/删除。)
我记得几次听到/读过关于Haskell是最好的命令式语言的开玩笑的评论,这听起来当然很奇怪,因为Haskell通常以其功能而闻名。
所以我的问题是,Haskell的哪些特性/特征(如果有的话)有理由证明Haskell被认为是最好的命令性语言,或者实际上是在开玩笑吗?
Answers:
我认为这是事实。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"
的(.)
操作者是函数的组合物。
这不是在开玩笑,我相信。对于那些不了解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()方法是否更改状态?您希望不会,但是它可能会...)。
除了可能还有其他一些优点,但这些都是我想到的。
除了已经提到的内容外,将副作用动作列为一流也是有时有用的。这是一个愚蠢的例子来展示这个想法:
f = sequence_ (reverse [print 1, print 2, print 3])
本示例说明了如何建立具有副作用的计算(在本示例中print
),然后将其放入数据结构或以其他方式对其进行处理,然后再实际执行它们。
call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse())
。我看到的主要区别是我们需要一些额外的东西() =>
。