hvr*_*hvr 81 haskell imperative-programming
(我希望这个问题是关于主题的 - 我试图寻找答案,但没有找到明确的答案.如果这恰好是偏离主题或已经回答,请缓和/删除它.)
我记得听过/读过关于Haskell 几次是最好的命令式语言的半开玩笑的评论,这当然听起来很奇怪,因为Haskell通常以其功能特性而闻名.
所以我的问题是,Haskell的哪些特性/特性(如果有的话)有理由证明Haskell被认为是最好的命令式语言 - 或者它实际上更像是一个笑话?
luq*_*qui 88
我认为这是一个半真半假的事实.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 ()
Run Code Online (Sandbox Code Playgroud)
对于许多命令式语言来说,这种抽象级别很难.这可以在具有闭包的命令式语言中完成; 例如.Python和C#.
但是Haskell也具有(非常独特)使用Monad类来表征允许的副作用的能力.例如,如果我们有一个函数:
foo :: (MonadWriter [String] m) => m Int
Run Code Online (Sandbox Code Playgroud)
这可能是一个"命令"功能,但我们知道它只能做两件事:
它无法打印到控制台或建立网络连接等.结合抽象功能,您可以编写作用于"任何产生流的计算"等的函数.
它真的是关于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
Run Code Online (Sandbox Code Playgroud)
所有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
Run Code Online (Sandbox Code Playgroud)
相同数量的线,但每条线的噪音都相当少.
编辑2
对于它的价值,这就是Haskell 的纯粹替代品的样子:
lastElt = return . last
Run Code Online (Sandbox Code Playgroud)
而已.或者,如果你禁止我使用Prelude.last
:
lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs
Run Code Online (Sandbox Code Playgroud)
或者,如果您希望它可以处理任何Foldable
数据结构并认识到您实际上不需要 IO
处理错误:
import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))
lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)
Run Code Online (Sandbox Code Playgroud)
用Map
,例如:
?? let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
?? lastElt example
Just "eggs"
Run Code Online (Sandbox Code Playgroud)
的(.)
操作者是函数的组合物.
Nei*_*own 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"
Run Code Online (Sandbox Code Playgroud)易于传递代码.鉴于我已经编写了上述内容,如果我现在想要使用该printBoth
函数打印出所有字符串列表,那么通过将子例程传递给mapM_
函数可以轻松完成:
mapM_ printBoth ["Hello", "World!"]
Run Code Online (Sandbox Code Playgroud)
另一个例子虽然不是必须的,但却是排序.假设您只想按长度对字符串进行排序.你可以写:
sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
Run Code Online (Sandbox Code Playgroud)
哪个会给你["b","cc","aaaa"].(你也可以把它写得比这短,但现在也没关系.)
易于重用代码.该mapM_
函数经常使用,并替换其他语言中的每个循环.还有forever
一些行为像一段时间(真实),以及各种其他功能,可以传递代码并以不同的方式执行它.因此,其他语言中的循环被Haskell中的这些控制函数所取代(这些函数并不特殊 - 您可以非常轻松地自己定义它们).通常,这使得很难使循环条件错误,就像 - 每个循环比长手迭代器等价物(例如在Java中)或数组索引循环(例如在C中)更难弄错.
含有副作用.假设我想从stdin中读取一行,并在将一些函数应用到stdout后将其写入stdout(我们称之为foo).你可以写:
do line <- getLine
putStrLn (foo line)
Run Code Online (Sandbox Code Playgroud)
我立即知道foo
没有任何意想不到的副作用(如更新全局变量,或释放内存,或其他),因为它的类型必须是String - > String,这意味着它是一个纯函数; 无论我传递什么价值,每次都必须返回相同的结果,没有副作用.Haskell很好地将副作用代码与纯代码分开.在像C,甚至Java这样的东西中,这并不明显(getFoo()方法是否会改变状态?你希望不会,但它可能会......).
除此之外还有一些优点,但那些是我想到的.
tib*_*bbe 16
除了已经提到过的其他内容之外,将副作用行为视为一流有时也是有用的.这是一个显示这个想法的愚蠢的例子:
f = sequence_ (reverse [print 1, print 2, print 3])
Run Code Online (Sandbox Code Playgroud)
此示例显示如何print
在实际执行它们之前如何构建具有副作用的计算(在此示例中),然后放入数据结构或以其他方式操作它们.