Haskell 对IOREF的混淆,以制作计数器

Haskell 对IOREF的混淆,以制作计数器,haskell,monads,ioref,unsafe-perform-io,Haskell,Monads,Ioref,Unsafe Perform Io,我找到了一些示例代码,并对其进行了一些更改 counter = unsafePerform $ newIORef 0 newNode _ = unsafePerformIO $ do i <- readIORef counter writeIORef counter (i+1) return i 每次运行时返回1、2、3、3等 但是当我把它改成 newNode =

我找到了一些示例代码,并对其进行了一些更改

counter = unsafePerform $ newIORef 0

newNode _ = unsafePerformIO $
              do
                i <- readIORef counter
                writeIORef counter (i+1)
                return i
每次运行时返回1、2、3、3等

但是当我把它改成

newNode = unsafePerformIO $
              do
                i <- readIORef counter
                writeIORef counter (i+1)
                return i
然后我每次运行它都得到0

为什么会发生这种情况,我可以做些什么来修复它?

在第二个版本中,newNode是一个简单的值,而不是一个函数。所以haskell只对它求值一次,然后在您访问newNode时给出求值结果

警告:在IO操作之外的任何操作上使用unsafePerformIO都是危险的,因为您知道该操作是引用透明的。它可能会与某些优化交互不好,并且通常不会像您期望的那样运行。它的名字中有“不安全”这个词是有原因的


作为一种处理unsafePerformIO的方法,您的代码很好,但如果您想在实际代码中使用类似的东西,我强烈建议您重新考虑。

您不应该在正常编程中使用此类结构,因为编译器可能会应用各种优化,从而破坏预期效果或使其无法预测。如果可能的话,使用类似State monad的东西。它更干净,而且总是像你想要的那样。给你:

-- This function lets you escape from the monad, it wraps the computation into a monad to make it, well, countable
counter :: (State Int x) -> x
counter = flip evalState 0

-- Increments the counter and returns the new counter
newNode :: State Int Int
newNode = modify (+1) >>= get

请看sepp2k对你的问题的回答感到悲伤。如果您有像configs这样的全局可用的、不太可能更改的东西,那么您所解释的内容尤其有用,因为它几乎可以从任何地方获得。明智地使用它,因为它违背了纯度的基本原则。如果可能的话,可以像我解释的那样使用monad。

作为澄清:对于像您正在构建的应用程序这样的应用程序,使用unsafePerformIO创建IORef是很正常的,并且允许Haskell在过程语言中最接近全局变量。在你的行动中使用它可能是不明智的。例如,newNode最好写为:

newNode = do i <- readIORef counter
             writeIORef counter (i+1)
             return i
然后,您打算调用newNode的任何位置本身都应该是一个IO操作


另一个小细节:计数器的默认类型为IORef Integer,除非您使用默认值更改此类型。不要试图给它一个像Num a=>IORef a这样的泛型类型:这样危险就存在了。

为什么不把它当作非实数函数处理?@MrBones:因为这种行为几乎永远不会是人们想要的。如果我写x=veryExpensiveFunction foobar,然后再写y=x*x+x,我希望veryExpensiveFunction foobar被计算一次,而不是三次。这正是Haskell所做的,除非x的类型是多态的。在本例中,您希望它像空函数一样运行的唯一原因是,对表达式求值会产生副作用,如果没有不安全的操作,这是不可能发生的。haskell中没有非空函数。函数都取一个值(可能是元组)并返回一个值(可能是另一个函数)。函数不被调用-它们被应用于值,以产生值。你真的想让foo=1+2成为一个函数,告诉处理器每次使用foo!这个词时加上1和2吗!?不管一个值是否可以被视为一个0参数函数,在我看来,它可以松散地被视为一种二进制函数类型,就像“x->y->z”可以松散地被视为一种二进制函数一样,纯度意味着这样一个函数必须始终返回相同的值。它总是使用相同的参数集none调用,因此必须始终返回相同的值,或者根据定义,它不是纯的,使用“unsafePerformIO”断言它是不正确的。目前,GHC 7.6.3 newNode肯定与优化交互不好:当使用-O编译时,它只运行一次!例如,newNode+newNode的计算结果为0。要在此处标出答案,最好的解决方法是永远不要使用unsafePerformIO,除非你真的知道你在做什么以及为什么需要它。即使这样,也可能不使用它。只是出于好奇,您提到使用Num a=>IORef a有什么危险?因为类型系统被它搞砸了。我在某个地方读到过这篇文章,但是再也找不到资源了。可能在的源代码中unsafePerformIO@Grazer:它可能违反类型安全;看见虽然他们的例子现在打印*对我来说,而不是转储核心,这仍然是不可能的。您还可以生成一个值,该值在第二次求值时会发生更改。