(我希望这个问题是符合主题的——我试着搜索答案,但没有找到明确的答案。如果这个问题与主题无关或已经有了答案,请进行适当的调整/删除。)
我记得曾经听说过或读到过几次Haskell被称为最好的命令式语言的半玩笑评论,这当然听起来很奇怪,因为Haskell通常以其函数特性而闻名。
所以我的问题是,Haskell有哪些特点/功能(如果有的话)可以证明Haskell被认为是最好的命令式语言的理由,还是它实际上更像是一个玩笑?
(我希望这个问题是符合主题的——我试着搜索答案,但没有找到明确的答案。如果这个问题与主题无关或已经有了答案,请进行适当的调整/删除。)
我记得曾经听说过或读到过几次Haskell被称为最好的命令式语言的半玩笑评论,这当然听起来很奇怪,因为Haskell通常以其函数特性而闻名。
所以我的问题是,Haskell有哪些特点/功能(如果有的话)可以证明Haskell被认为是最好的命令式语言的理由,还是它实际上更像是一个玩笑?
我认为这只是半个真相。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 ()
foo :: (MonadWriter [String] m) => m Int
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"
< p > < code > (.) 运算符是 函数合成。 < /p >
IORef
隐式化,至少尝试过,但受到GHC更改的阻挠。:[ - C. A. McCann创建子程序非常容易。假设我想要一个函数来将值打印到标准输出和标准错误输出。我可以编写以下代码,使用一行短小的代码定义子程序:
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())
。
我看到的主要区别是我们需要一些额外的() =>
。 - Hjulle// sum 0..100
i = s = 0;
while (i <= 100) {
s = s+i;
i++;
}
return s;
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
用于以递归方式调用带有新状态的循环,模拟迭代你可以使用put $ traceShowId (new_i, new_s)
来添加仅用于调试的打印,而不是put (new_i, new_s)
,但你应该只在调试时使用它,因为它会欺骗类型系统。
因此,还有一些事情需要“手动”处理,但在Haskell中编写相当易读的命令式代码是可能的。