我很好奇在 Haskell 中实现一个循环。我怎样才能在 Haskell 中做类似的事情(伪代码):
var i = 0
for (int i1 = 0; i1 < 10; i1++) {
println(i1)
i += 2
}
println(i)
我很好奇在 Haskell 中实现一个循环。我怎样才能在 Haskell 中做类似的事情(伪代码):
var i = 0
for (int i1 = 0; i1 < 10; i1++) {
println(i1)
i += 2
}
println(i)
在功能方面,您所做的是折叠整数列表,以便为每个整数打印元素并将累加器增加 2。由于我们正在打印某些内容(即执行 I/O),因此我们需要在 monad 中折叠,但是否则它只是你的标准left-fold。
foldM (\i i1 -> print i1 >> return (i + 2)) 0 [0..9] >>= print
我们使用与您的代码使用相同变量名称的 lambda 函数进行折叠。Iei1
是当前元素,i
是累加器。
i = 0
下一个参数是代码中对应的累加器的初始值。
最后一个参数是我们折叠的数字列表(包括两端)。
(>>=
绑定运算符)将折叠的结果(即 accumulator 的最终值i
)传递给 print 函数。
编辑:这是假设你打算写
var i = 0
for (int i1 = 0; i1 < 10; i1++) {
println(i1)
i += 2
}
println(i)
i
而不是只在 for 子句和循环体中递增。
遵循两个假设
(与商相同的假设)你想打印出来
0
1
2
3
4
5
6
7
8
9
20
我会在 Haskell 中写
do
let xs = [0..9]
mapM_ print xs
print (length xs * 2)
您会看到原始计算如何分成三个独立的(并且独立的!)计算。
i1
变成一个列表。我们知道边界是 0 和 9(包括 0 和 9),所以循环变量可以用 list 表示[0..9]
。i
”,然后将其打印出来。第三个计算特别有趣,因为它突出了传统命令式编程和 Haskell 之间的区别,后者在很大程度上是关于声明式编程的。
列表的每次迭代都向一个数字添加 2 与获取列表的长度并将其乘以 2 相同。在我看来,最大的不同是我可以i = length xs * 2
在眨眼之间阅读并理解它的含义。然而,计算i
循环的每次迭代需要一些思考才能理解它的真正含义。
所有三个子计算都是独立的这一事实意味着它们更容易测试——您可以一次测试一个,如果它们单独工作,它们也可以一起工作!
如果您在“外观相似的代码”的意义上指的是“相似”,请参阅任何STRef
/IORef
答案。
您将使用高阶函数,例如forM_
. 这个名字看起来有点奇怪,但实际上非常系统:该函数被调用for
,它在 monads ( M
) 上工作并且它不返回值 ( _
): forM_
。
你可以像这样使用它:
import Control.Monad
import Data.IORef
main = do i <- newIORef 0
forM_ [0..9] $ \ i' -> do
print i'
i `modifyIORef` (+ 2)
readIORef i >>= print
forM_
函数本身可以用递归来实现。这是一个简单的方法:
forM_ [] _ = return ()
forM_ (x:xs) f = f x >> forM_ xs f
当然,在 Haskell 中使用像这样的可变状态是丑陋的。不是因为它必须丑陋,而是因为它很少使用。你可以想象一个看起来更好的类 C 库;看看这个例子。
自然地,在 Haskell 中做到这一点的最好方法是采用更实用的方法并忘记可变变量。你可以写这样的东西:
main = do forM_ [0..9] print
print $ sum [i' * 2 | i' <- [0..9]]
我们还可以通过一些非常简单的函数定义来改进循环代码。只要有一个很好的操作符来读取、设置和修改IOVar
s 就大有帮助:
(!) :: (a -> IO b) -> IORef a -> IO b
f ! var = readIORef var >>= f
(+=) :: Num a => IORef a -> a -> IO ()
var += n = var `modifyIORef` (+ n)
ref :: a -> IO (IORef a)
ref = newIORef
这让我们可以这样编写循环:
import Data.IORef
main = do i <- ref 0
forM_ [0..9] $ \ i' -> do
print i'
i += 2
print !i
在这一点上,它看起来几乎和 OCaml 完全一样!仅对几个运算符定义进行了相当大的改进。
执行循环的一种简单而灵活的方法是在 let 或 where 子句中定义递归函数:
main = loop 0 10
where
loop i n | i < n = putStrLn (show i) >> loop (i+2) n
loop _ _ = return ()
这定义了loop
两个变量i
和的函数n
。第一个模式有一个守卫(在 之后|
),它检查条件(i < n
)。如果为真,则选择此分支并i
打印到控制台,然后loop
再次调用自身,现在绑定i
到i+2
. 否则选择默认分支,它只是returns ()
(=在 IO Monad 中什么也不做)。
使用递归函数实现这样的循环非常灵活。如果您想使用高阶函数(例如for
and map
)很容易,但是如果您不知道如何从命令式设置中转换某些循环,则递归函数通常可以解决问题。
import Data.IORef
import Control.Monad
main = do
i <- newIORef 0
let loop = do
iNow <- readIORef i
when (iNow < 10) $ do
print i
modifyIORef i (+1)
loop
loop
但显然,你应该避免这种情况。mapM_ print [0..9]
效果更好。
我看到你必须i
在那里以不同的增量变量。好吧,很明显如何在这里添加它。您可以通过循环保持手动递归。更好一点的是用一个IORef
简单的forM_
. 最好尽量不使用任何IORef
结构,而只使用功能结构。