为什么Haskell(有时)被称为“最佳命令式语言”?
(我希望这个问题是关于主题的 – 我试着寻找一个答案,但没有find一个明确的答案。如果这是恰当的话题或已经回答,请温和/删除它。)
我记得听过/读过关于Haskell几次被认为是最好的命令式语言的半开玩笑的评论,这听起来很奇怪,因为Haskell通常以其function特性而闻名。
所以我的问题是,Haskell有什么品质/特性(如果有的话)有理由certificateHaskell被认为是最好的命令式语言 – 或者实际上更像是一个笑话?
我认为这是一个半真相。 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
这可能是一个“必要的”function,但是我们知道它只能做两件事:
- “输出”一串string
- 返回一个Int
它不能打印到控制台或build立networking连接等。结合抽象能力,您可以编写作用于“任何产生stream的计算”的函数等。
这实际上是关于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( <$>
)对内联计算的结果进行操作……看起来都非常复杂。 从function的angular度来看,它有很多意义,但是命令式语言倾向于将这些细节中的大部分都清理干净,以使它们更易于使用。
不可否认,也许如果我们使用了不同的while
型组合器,它会更干净。 但是,如果你把这个哲学放到了足够的地步(用一组丰富的组合器来清晰地expression自己的意思),那么你就可以再次进入函数式编程。 命令式的Haskell并不像一个精心devise的命令式语言,例如python,“stream动”。
总而言之,在句法上,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
相同的线数,但每一行都有相当less的噪音。
编辑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) => ta -> 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-notation(等等)来让你编写命令式的代码(是的,它使用monad,但是不用担心)。 以下是Haskell为您提供的一些优势:
-
轻松创build子例程。 假设我想要一个函数将值输出到stdout和stderr。 我可以写下面的内容,用一行简短的代码定义子例程:
do let printBoth s = putStrLn s >> hPutStrLn stderr s printBoth "Hello" -- Some other code printBoth "Goodbye"
-
易于传递代码。 鉴于我已经写了上面的内容,如果我现在要使用
printBoth
函数打印出所有的string列表,可以通过将我的子程序传递给mapM_
函数来轻松完成:mapM_ printBoth ["Hello", "World!"]
另一个例子,虽然不是必要的,但是正在sorting。 假设你想按stringsortingstring。 你可以写:
sortBy (\ab -> compare (length a) (length b)) ["aaaa", "b", "cc"]
哪个会给你[“b”,“cc”,“aaaa”]。 (你可以写得比这更短,但是现在不要紧。)
-
易于重复使用的代码。
mapM_
函数被使用了很多,并且replace了其他语言的每个循环。 也有forever
这样的行为(真),和其他各种function,可以通过代码和执行不同的方式。 所以在其他语言中的循环被Haskell中的这些控制函数取代(这不是特别的 – 你可以很容易地自己定义它们)。 一般情况下,这使得很难得到错误的循环条件,就像每个循环比长时间的迭代器(例如在Java中)或数组索引循环(例如在C中)更难以得到错误。 - 绑定不分配。 基本上,你只能分配一个variables(而不是一个静态分配)。 这消除了在任何给定点(它的值只设置在一行上)的variables的可能值的许多混淆。
-
含有副作用。 假设我想从stdin中读取一行,并在应用某个函数后将它写在stdout上(我们将它称为foo)。 你可以写:
do line <- getLine putStrLn (foo line)
我立即知道
foo
没有任何意想不到的副作用(比如更新全局variables,或者释放内存,或者其他),因为它的types必须是String – > String,这意味着它是一个纯函数。 无论我通过什么样的价值,它都必须每次都返回相同的结果,而没有副作用。 Haskell很好地将纯代码中的副作用代码分开。 在C,甚至Java这样的东西,这不是很明显(是否getFoo()方法改变状态?你不希望,但它可能会…)。 - 垃圾收集。 很多语言是垃圾收集这些天,但值得一提:没有分配和释放内存的麻烦。
除此之外还可能有更多的优点,但是这些是我们想到的。
除了其他人已经提到的,有副作用的行为是一stream的有时是有用的。 这是一个愚蠢的例子来展示这个想法:
f = sequence_ (reverse [print 1, print 2, print 3])
这个例子展示了如何用副作用build立计算(在这个例子中是print
),然后把数据结构放到数据结构中,或者在实际执行之前用其他方法来处理它们。