Путаница по поводу IORefs, чтобы сделать счетчик
Я нашел пример кода и немного изменил его
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 каждый раз, когда я запускаю его.
Почему это происходит, и что я могу сделать, чтобы это исправить?
3 ответа
В твоей второй версии newNode
это простое значение, а не функция. Так что haskell оценивает его ровно один раз, а затем выдает результат этой оценки всякий раз, когда вы получаете доступ newNode
,
Слово предупреждения: Использование unsafePerformIO
на чем-либо, кроме действия ввода-вывода, которое, как вы знаете, является ссылочно-прозрачным, опасно. Он может плохо взаимодействовать с некоторыми оптимизациями и вообще не вести себя так, как вы ожидаете. Есть причина, по которой в его названии есть слово "небезопасно".
Как способ поиграть с unsafePerformIO
с вашим кодом все в порядке, но если вы когда-нибудь захотите использовать что-то подобное в реальном коде, я настоятельно рекомендую вам пересмотреть его.
Просто для пояснения: для приложения, подобного тому, которое вы, похоже, создаете, создание IORef с использованием unsafePerformIO
вполне нормально, и допускает самое близкое приближение Хаскелла к глобальным переменным в процедурных языках. То, что, вероятно, не разумно, использует это в своих действиях. Например, newNode, вероятно, лучше всего записать как:
newNode = do i <- readIORef counter
writeIORef counter (i+1)
return i
И тогда в любое место, которое вы намереваетесь назвать newNode
должно быть само действие IO.
Еще одна маленькая деталь: counter
будет иметь тип IORef Integer
по умолчанию, если вы не измените это с помощью default
, Не пытайтесь дать ему общий тип, как Num a => IORef a
: так лежит опасность.
Вам не следует использовать такие конструкции в обычном программировании, так как компилятор может применять различные оптимизации, которые убивают желаемый эффект или делают его непредсказуемым. Если возможно, используйте что-то вроде State
вместо монады. Он чище и всегда будет вести себя так, как вы хотите. Ну вот:
-- 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 за ответ на ваш вопрос. То, что вы объясняете, особенно полезно, если у вас есть что-то глобальное (например, конфиги), которое вряд ли изменится, что должно быть доступно практически везде. Используйте это с умом, так как это противоречит основному принципу чистоты. Если это возможно, используйте монады вместо этого (как я объяснил).