haskellshared-stateioref

How to share IORef state between two function invocations in Haskell when using IO?


I'm trying to learn Haskell and I'm playing around with IORef to which I try to save and find records. My code looks something like this (note that I've chosen "String" as IORef type in this example only for convienence and briefty, in my actual code I'm using a record. And also ignore that I'm using a Set instead of a Map, I will change that):

module MyTest where

import           Data.IORef
import           Data.Set
import           Data.Foldable          (find)

type State = (Set String)
type IORefState = IORef State

saveStringToState :: IO IORefState -> String -> IO String
saveStringToState stateIO string = do
  state <- stateIO
  atomicModifyIORef
    state
    (\oldStrings ->
       let updatedStrings = insert string oldStrings
       in (updatedStrings, updatedStrings))
  stringsState <- readIORef state :: IO State
  putStrLn ("### saved: " ++ show stringsState)
  return string

findStringInState :: IO IORefState -> String -> IO (Maybe String)
findStringInState stateIO soughtString = do
  state <- stateIO :: IO IORefState
  strings <- readIORef state :: IO State
  putStrLn ("Looking for " ++ soughtString ++ " in: " ++ show strings)
  return $ find (== soughtString) strings

doStuff =
  let stateIO = newIORef empty
  in do saveStringToState stateIO "string1"
        findStringInState stateIO "string1"

What I want to achieve is to share the state (Set) between the two function calls so that findStringInState can return the String that I just inserted into the Set. But when I run the doStuff function I get this:

*MyTest> doStuff
### saved: fromList ["string1"]
Looking for string1 in: fromList []
Nothing

I've probably misunderstood something since I thought the IORef should indeed be the container for my state.

  1. Why is this not working?
  2. What can I do to make it work?

Solution

  • Seems that you confused IO IORefState with IORefState (without IO), more generally, IO a with a.

    In your case, the value of IO IORefState is the action newIORef empty, which represents "an action that creates a fresh new IORef from scratch".
    By contrast, IORefState (without IO) is the right, raw object that you should share among the functions that use it (saveStringToState, and findStringInState).
    Then, both saveStringToState and findStringInState separately call newIORef empty, i.e. each of them create a different IORefState object, which cannot be affected by the other.

    To fix, you must call newIORef empty (as an IO action using <-) in doStuff function and share the IORefState created by newIORef empty instead of IO IORefState:

      saveStringToState :: IORefState -> String -> IO String
      ...
    
      findStringInState :: IORefState -> String -> IO (Maybe String)
      ...
    
      let stateIO = newIORef empty
      in do ioRef <- stateIO
            saveStringToState ioRef "string1"
            findStringInState ioRef "string1"
      -- Or, more simply:
      do ioRef <- newIORef empty
         saveStringToState ioRef "string1"
         findStringInState ioRef "string1"
    

    In my opinion, the difference between IO a with a is similar to the difference between "a function object that returns a value typed as a (with some side effects)" and "just a raw value typed as a" in the other programming languages.