【问题标题】:imperative loop in HaskellHaskell 中的命令式循环
【发布时间】:2015-09-10 14:15:03
【问题描述】:

我正在尝试了解 Haskell 中的 monad 系统。我以前大约 80% 的编程经验是用 C 语言编写的,但具有讽刺意味的是,Haskell 的命令式部分是最难理解的。列表操作和惰性评估更加清晰。无论如何,我想让 ghc 接受此代码。我知道代码根本没有意义。最明显的是,我传递了一个Bool,其中预计IO Bool。但这不是唯一的问题。我知道这是一个愚蠢的问题,但请帮助我进一步了解 Haskell 语言。

import Control.Monad

while :: Monad m => m Bool -> m () -> m ()
while cond action = do
  c <- cond
  when c $ do
    action
    while cond action

main :: IO ()
main = do
  i <- 0
  while (i < 10) $ do
    i <- i + 1
    print i

这就是我最终做到的方式。我知道allocaArray 不是必需的,但使用起来非常有趣。 Haskell 真的没有限制,非常强大。

import Control.Monad
import Data.IORef
import Foreign.Ptr
import Foreign.Storable
import Foreign.Marshal.Array

while :: Monad m => m Bool -> m () -> m ()
while cond action = do
  c <- cond
  if c then do
    action
    while cond action
  else return ()

main :: IO ()
main = do
  let n = 10
  allocaArray n $ \p -> do
    i <- newIORef 0
    while (liftM (< n) (readIORef i)) $ do
      i2 <- readIORef i
      poke (advancePtr p i2) i2
      modifyIORef i (+ 1)
    writeIORef i 0
    while (liftM (< n) (readIORef i)) $ do
      i2 <- readIORef i
      (peek $ advancePtr p i2) >>= print
      modifyIORef i (+ 1)

【问题讨论】:

  • 让我们从显而易见的开始。 i 不是一个可变变量,并且不会仅仅因为你有一个 monad 而成为一个变量。 i &lt;- i + 1 指的是两个不同的is。
  • while 构造在 Haskell 中很少使用,我认为正是因为在 Haskell 中,它实际上并不允许您以自然方式使用命令式语言的人使用“变量”。您可以更尴尬地做同样的事情,但您必须使用可变引用,例如Data.IORefControl.Concurrent.MVar 中的引用。除非您确实需要可变更新,否则通常最好用函数式表达它。
  • 请注意,以这种方式使用IORef 会导致循环计数器被“装箱”,因此每次迭代都会分配一个新的Int 框,并且访问计数器涉及指针间接。当您处理更多功能风格的计数器时,GHC 通常可以将其拆箱,从而获得更快的代码。
  • 另外,你为什么要使用Foreign 的所有东西?你真的需要处理外国代码吗?你有理由让垃圾收集器把数组当作外来的吗?如果没有,您应该考虑使用未装箱的可变向量(来自vector 包)。
  • 还有一些库提供未装箱的可变变量;我不知道他们的表现如何。基本思想是使用可变字节数组来存储“变量”。

标签: haskell monads


【解决方案1】:

这种方法的问题在于i 不是可变变量。您可以使用IORef,但是更实用的方法是通过每次迭代传递当前状态。您可以重写您的 whileM 正文和条件以获取当前值:

whileM :: Monad m => (a -> Bool) -> (a -> m a) -> a -> m ()
whileM test act init =
   when (test init) $ (act init) >>= whileM test act

那你就可以了

whileM (< 10) (\i -> print i >> return (i + 1)) 0

【讨论】:

    【解决方案2】:

    与全局状态(IORef 和朋友)相反,具有本地状态(状态和相关的 monad 转换器)的解决方案:

    import Control.Monad
    import Control.Monad.State
    
    while :: Monad m => m Bool -> m () -> m ()
    while cond action = do
      c <- cond
      when c $ do
        action
        while cond action
    
    main :: IO ()
    main = do 
      runStateT (while cond body) 1 
      return ()
    
    body :: StateT Integer IO ()
    body = do
        x <- get
        liftIO $ print x
        put (x + 1)
        return ()
    
    cond :: StateT Integer IO Bool
    cond = do
        x <- get
        return (x < 10)
    

    循环体和循环条件是明确的,并且为了清楚起见而命名;可以写例如while (liftM (&lt; 10) get) body.

    【讨论】:

      【解决方案3】:

      有两件事会阻止您的代码进行类型检查:

      1. 您的while 函数需要IO Bool,但您给它i &lt; 10,它是Bool 类型的表达式。要将Bool 转换为IO Bool,只需使用return

      2. 当您编写i &lt;- 0 时,您尝试使用文字零作为一元值,但事实并非如此。请记住

        main = do
            i <- 0
            ...
        

        等价于

        main = 0 >>= \i -> do ...
        

      要解决此问题,您还可以通过return 推广0

      因此,你最终得到

      main :: IO ()
      main = do
          i <- return 0
          while (return (i < 10)) $ do
              i <- return (i + 1)
              print i
      

      但是,这仍然不会按照您的意图进行:原因是i &lt;- return (i + 1) 中的第一个(最左侧)i 与@987654341 中的i 不同 @。您正在隐藏该变量,创建一个具有相同名称的新变量,然后打印该变量。所以你实际上根本不会碰到任何计数器。

      我不想破坏乐趣,但如果你真的卡住了:有一个 monad-loops 包,它公开了几个有用的一元循环函数,包括一个 whileM 函数。

      【讨论】:

      • OP 中的while 几乎等同于该包的whileM_(稍微不那么通用,但并不重要);我不认为这就是问题所在。
      • 周围还有一些其他的循环库,例如 loopscontrol-monad-loop,它们提供的设施更符合 OP 的需求。 monad-loops 有一些很酷的并发工具,但除此之外它似乎不太通用。
      猜你喜欢
      • 2015-06-01
      • 2021-07-03
      • 2023-03-20
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      • 1970-01-01
      • 2018-01-31
      • 2013-05-14
      相关资源
      最近更新 更多