2

我很好奇在 Haskell 中实现一个循环。我怎样才能在 Haskell 中做类似的事情(伪代码):

var i = 0
for (int i1 = 0; i1 < 10; i1++) {
  println(i1)
  i += 2
}

println(i)
4

5 回答 5

8

在功能方面,您所做的是折叠整数列表,以便为每个整数打印元素并将累加器增加 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 子句和循环体中递增。

于 2013-11-04T11:04:33.480 回答
7

遵循两个假设

  1. “相似”是指行为相似,并且
  2. (与商相同的假设)你想打印出来

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    20
    

我会在 Haskell 中写

do
  let xs = [0..9]
  mapM_ print xs
  print (length xs * 2)

您会看到原始计算如何分成三个独立的(并且独立的!)计算。

  1. 我们将循环变量i1变成一个列表。我们知道边界是 0 和 9(包括 0 和 9),所以循环变量可以用 list 表示[0..9]
  2. 我们打印列表的内容,这与每次迭代打印循环变量相同。
  3. 我们根据我们对列表的了解计算“ i”,然后将其打印出来。

第三个计算特别有趣,因为它突出了传统命令式编程和 Haskell 之间的区别,后者在很大程度上是关于声明式编程的。

列表的每次迭代都向一个数字添加 2 与获取列表的长度并将其乘以 2 相同。在我看来,最大的不同是我可以i = length xs * 2在眨眼之间阅读并理解它的含义。然而,计算i循环的每次迭代需要一些思考才能理解它的真正含义。

所有三个子计算都是独立的这一事实意味着它们更容易测试——您可以一次测试一个,如果它们单独工作,它们也可以一起工作!

如果您在“外观相似的代码”的意义上指的是“相似”,请参阅任何STRef/IORef答案。

于 2013-11-04T11:06:46.143 回答
3

您将使用高阶函数,例如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]]

我们还可以通过一些非常简单的函数定义来改进循环代码。只要有一个很好的操作符来读取、设置和修改IOVars 就大有帮助:

(!) :: (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 完全一样!仅对几个运算符定义进行了相当大的改进。

于 2013-11-04T10:56:25.860 回答
1

执行循环的一种简单而灵活的方法是在 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再次调用自身,现在绑定ii+2. 否则选择默认分支,它只是returns ()(=在 IO Monad 中什么也不做)。

使用递归函数实现这样的循环非常灵活。如果您想使用高阶函数(例如forand map)很容易,但是如果您不知道如何从命令式设置中转换某些循环,则递归函数通常可以解决问题。

于 2013-11-04T11:07:49.777 回答
1
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结构,而只使用功能结构。

于 2013-11-04T10:55:32.723 回答