// sum 0..100
i = s = 0;
while (i <= 100) {
s = s+i;
i++;
}
return s;
haskell 代码:
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)添加debug only printing,但是您应该只将其用于调试,因为它欺骗了类型系统。 因此,有一些事情需要“手动”处理,但可以用Haskell编写可读性相当好的命令式代码。
4条答案
按热度按时间5jvtdoz21#
我认为这是半真半假的。Haskell有惊人的抽象能力,包括对命令式概念的抽象。例如,Haskell没有内置的命令式while循环,但我们可以编写它,现在它做到了:
这一抽象层次对于许多命令式语言来说是困难的,这可以在有闭包的命令式语言中实现;例如Python和C#。
但是Haskell也有(非常独特的)能力,可以使用Monad类来“描述允许的副作用”,例如,我们有一个函数:
这可以是一个“命令式”函数,但我们知道它只能做两件事:
它不能打印到控制台或建立网络连接,等等。结合抽象能力,你可以编写函数,作用于“任何产生流的计算”,等等。
Haskell的抽象能力使它成为一种非常优秀的命令式语言。
然而,错误的一半是语法。我发现Haskell在命令式风格中使用起来非常冗长和笨拙。下面是一个命令式风格计算的示例,使用上面的
while
循环,它查找链表的最后一个元素:所有IORef垃圾、双重读取、必须绑定读取的结果、fmapping(
<$>
)操作内联计算的结果......看起来都非常复杂。从 * 函数 * Angular 来看,这非常有意义,但命令式语言倾向于掩盖这些细节,以使它们更易于使用。诚然,如果我们使用一个不同的
while
风格的组合符,它可能会更简洁。但是如果你把这种哲学走得足够远(使用一组丰富的组合符来清晰地表达自己),那么你又回到了函数式编程。命令式风格的Haskell只是不像一个设计良好的命令式语言(如python)那样“流畅”。总而言之,通过句法上的修饰, haskell 可能是最好的命令式语言,但是,由于修饰的本质,它会用外表上美丽和虚假的东西来取代内在美丽和真实的的东西。
编辑:将
lastElt
与以下python音译进行对比:线数相同,但每一条线的噪声要少得多。
编辑2
不管怎么说,这是Haskell中的一个 * 纯粹 * 替代品的样子:
就是这样。或者,如果您禁止我使用
Prelude.last
:或者,如果您希望它可以处理任何
Foldable
数据结构,并且认识到实际上并不需要IO
来处理错误:以
Map
为例:(.)
运算符是function composition。0tdrvxhp2#
这不是玩笑,我相信这一点。我会尽量让那些不知道Haskell的人也能理解。Haskell使用do符号(还有其他一些东西)来让你编写命令式代码(是的,它使用单子,但不用担心)。下面是Haskell给你的一些好处:
printBoth
函数打印出一个字符串列表中的所有字符串,可以很容易地将我的子例程传递给mapM_
函数:另一个例子是排序,虽然不是强制性的,假设你只想按长度对字符串排序,你可以写:
这将给予你[“B”,“cc”,“aaaa”]。(你也可以写得比这更短,但现在不要紧。)
mapM_
函数被大量使用,它取代了其他语言中的for-each循环。还有forever
,它的作用类似于while(真),和各种其他函数,这些函数可以传递代码并以不同的方式执行代码。因此,其他语言中的循环被Haskell中的这些控制函数所取代(这并不特别--你可以自己定义它们)一般来说,这使得循环条件很难出错,就像for-each循环比长手迭代器(例如在Java中)或数组索引循环(例如在C中)更难出错。我立刻知道
foo
没有任何意想不到的副作用(比如更新全局变量,或者释放内存,或者其他什么),因为它的类型必须是String -〉String,这意味着它是一个纯函数;无论我传递什么值,它每次都必须返回相同的结果,没有副作用。Haskell很好地将副作用代码与纯代码区分开来。在C甚至Java中,这一点并不明显(getFoo()方法改变状态了吗?你可能希望没有,但它可能会改变...)。除此之外,可能还有其他一些优势,但这些都是我想到的。
f0brbegy3#
除了其他人已经提到过的,有时候把副作用放在第一位是有用的,下面是一个愚蠢的例子来说明这个想法:
这个例子展示了如何构建带有副作用的计算(在这个例子中是
print
),然后在实际执行之前,将放入数据结构中或以其他方式操作它们。eyh26e7m4#
使用与this answer中的@Chi相同的示例,可以使用State单子来模拟带有递归的命令式循环:
C代码:
haskell 代码:
正如您所看到的,这与C代码非常相似,我们只多了3行代码:
(i, s)
〈- get以获取当前状态。put (new_i, new_s)
以使用新状态更新当前状态sum_loop
使用新状态递归调用循环,使用递归模拟迭代您可以使用
put $ traceShowId (new_i, new_s)
而不是put (new_i, new_s)
添加debug only printing,但是您应该只将其用于调试,因为它欺骗了类型系统。因此,有一些事情需要“手动”处理,但可以用Haskell编写可读性相当好的命令式代码。