4

我试图了解如何IORefs真正使用,并且在遵循我在https://www.seas.upenn.edu/~cis194/spring15/lectures/12-unsafe.html上找到的示例代码时遇到了麻烦

newCounter :: IO (IO Int)
newCounter = do
  r <- newIORef 0
  return $ do
    v <- readIORef r
    writeIORef r (v + 1)
    return v

printCounts :: IO ()
printCounts = do
  c <- newCounter
  print =<< c
  print =<< c
  print =<< c

printCounts执行“ ”时,为什么在“ ”块c <- newCounter中没有c得到执行工作的结果,这似乎应该在第一次调用时分配给常量“”然后永远不会改变?相反,似乎被分配了在该 " " 块中定义的函数,然后每次到达另一个 " " 时都会重新执行该函数。似乎答案在某种程度上在于具有双重嵌套的“ ”类型,但我无法理解为什么这会使函数在调用时重新执行,而不是只评估一次常量。newCounterreturn $ doIO 0creturn $ doprintCountsprint =<< cnewCounterIO (IO Int)c

4

1 回答 1

7

您可以将其IO视为一种程序。newCounter :: IO (IO Int)是输出程序的程序。更准确地说,newCounter分配一个新的计数器,并返回一个程序,该程序在运行时递增计数器并返回其旧值。newCounter不执行它返回的程序。如果你改为写:

newCounter :: IO (IO Int)
newCounter = do 
  r <- newIORef 0
  let p = do              -- name the counter program p
        v <- readIORef r
        writeIORef r (v + 1)
        return v
  p          -- run the counter program once
  return p   -- you can still return it to run again later

您还可以使用等式推理展开printCounts成一系列原语。以下所有版本printCounts都是等效程序:

-- original definition
printCounts :: IO ()
printCounts = do
  c <- newCounter
  print =<< c
  print =<< c
  print =<< c

-- by definition of newCounter...

printCounts = do
  c <- do
    r <- newIORef 0
    return $ do
      v <- readIORef r
      writeIORef r (v + 1)
      return v
  print =<< c
  print =<< c
  print =<< c

-- by the monad laws (quite hand-wavy for brevity)
-- do
--   c <- do
--     X
--     Y
--   .....
-- =
-- do
--   X
--   c <- 
--     Y
--   .....
--
-- (more formally,
--  ((m >>= \x -> k x) >>= h) = (m >>= (\x -> k x >>= h)))

printCounts = do
  r <- newIORef 0
  c <-
    return $ do
      v <- readIORef r
      writeIORef r (v + 1)
      return v
  print =<< c
  print =<< c
  print =<< c

-- c <- return X
-- =
-- let c = X
--
-- (more formally, ((return X) >>= (\c -> k c)) = (k X)

printCounts = do
  r <- newIORef 0
  let c = do
        v <- readIORef r
        writeIORef r (v + 1)
        return v
  print =<< c
  print =<< c
  print =<< c

-- let-substitution

printCounts = do
  r <- newIORef 0
  print =<< do
        v <- readIORef r
        writeIORef r (v + 1)
        return v
  print =<< do
        v <- readIORef r
        writeIORef r (v + 1)
        return v
  print =<< do
        v <- readIORef r
        writeIORef r (v + 1)
        return v

-- after many more applications of monad laws and a bit of renaming to avoid shadowing
-- (in particular, one important step is ((return v >>= print) = (print v)))

printCounts = do
  r <- newIORef 0
  v1 <- readIORef r
  writeIORef r (v1 + 1)
  print v1
  v2 <- readIORef r
  writeIORef r (v2 + 1)
  print v2
  v3 <- readIORef r
  writeIORef r (v3 + 1)
  print v3

在最终版本中,您可以看到它printCounts确实分配了一个计数器并将其递增 3 次,打印每个中间值。

一个关键步骤是 let-substitution 步骤,计数器程序在此重复,这就是它运行 3 次的原因。let x = p; ...不同于x <- p; ..., 它运行p并绑定x到结果而不是程序p本身。

于 2018-11-26T20:05:52.090 回答