How to deal with mutable structures in the IO monad
TL; DR: How to ensure that the values generated randomRIO
(from System.Random
) are stored in a given expression do
?
How can I work with mutable structures in IO Monad?
My original question was (so very) wrong - I am updating the title, so future readers who want to understand the use of mutable structs in the IO monad may find this post.
Longer version:
And head up: It looks like a long time, but many of them just give me an overview of how it works exercism.io
. (UPDATE: The last two blocks of code are older versions of my code, which are included as a reference, in case future readers would like to follow along with iterations in the code based on the comments and answers.)
Exercise overview:
I am working on an exercise Robot Name
(extremely instructive). The exercise involves creating a data type Robot
that can store a randomly generated name (an exercise is Readme
included below).
For those unfamiliar with it, the learning model is exercism.io
based on automated testing of student-generated code. Each exercise consists of a series of tests (written by the author of the test), and the solution code must be able to pass all of them. Our code must pass all the tests in a given exercise test file before we can move on to the next exercise - an efficient model, imo. ( Robot Name
- Exercise # 20 or so.)
In this particular exercise, we are invited to create a data type Robot
, and three related functions: mkRobot
, robotName
and resetName
.
-
mkRobot
generates an instanceRobot
-
robotName
generates and "returns" a unique name for the unnamed oneRobot
(ierobotName
does not overwrite a pre-existing name); if aRobot
already has a name, it just "returns" the existing name -
resetName
overwrites the pre-existing name.
There are 7 tests in this particular exercise. The test verifies that:
- 0)
robotName
generates names that match the specified pattern (a name is 5 characters long and consists of two letters followed by three numbers, for example, AB123, XQ915, etc.). - 1) the name assigned
robotName
is constant (i.e., suppose we create robot A and assign it (or her) a name withrobotName
; calling the secondrobotName
(on robot A) shouldn't overwrite its name) - 2)
robotName
generates unique names for different robots (i.e. verifies that we are actually doing process randomization) - 3)
resetName
generates names matching the specified pattern (similar to test # 0) - 4) the name assigned
resetName
is permanent - 5)
resetName
assigns a different name (i.e.resetName
gives the robot a name that is different from the current name) - 6)
resetName
only affects one robot at a time (ie, suppose we have robot A and robot B; reset robot. The name should not affect the name of robot B) AND (ii) the names that are generatedresetName
are constant.
As a link, here's the test itself: https://github.com/dchaudh/exercism-haskell-solutions/blob/master/robot-name/robot-name_test.hs
Where I am stuck:
Version 1 (original post): At the moment my code is failing on three tests (# 1, # 4 and # 6), all of which involve storing the robot name. ...
Version 2: (intermediate) Now my code only fails on one test (# 5) - test 5 is associated with changing the name of an already created robot
(thanks to bheklikr for his helpful comments that helped me clean up version 1)
Version 3 (final): The code is now fixed (and passes all tests) thanks to the detailed Cirdec post below. For the future benefit of the reader, I am including the final version of the code along with the two earlier versions (so that they can be followed along with various comments / answers).
Version 3 (Final): Here's the final version based on Cirdec's answer below (which I highly recommend reading). It turns out my original question (which was asking about creating constant variables using System.Random) was just completely wrong because my original implementation was unreasonable. Instead, my question was to ask how to deal with mutable structures in the IO monad (which Cirdec talks about below).
{-# LANGUAGE NoMonomorphismRestriction #-}
module Robot (robotName, mkRobot, resetName) where
import Data.Map (fromList, findWithDefault)
import System.Random (Random, randomRIO)
import Control.Monad (replicateM)
import Data.IORef (IORef, newIORef, modifyIORef, readIORef)
newtype Robot = Robot { name :: String }
mkRobot :: IO (IORef Robot)
mkRobot = mkRobotName >>= return . Robot >>= newIORef
robotName :: IORef Robot -> IO String
robotName rr = readIORef rr >>= return . name
resetName :: IORef Robot -> IO ()
resetName rr = mkRobotName >>=
\newName -> modifyIORef rr (\r -> r {name = newName})
mkRobotName :: IO String
mkRobotName = replicateM 2 getRandLetter >>=
\l -> replicateM 3 getRandNumber >>=
\n -> return $ l ++ n
getRandNumber :: IO Char
getRandNumber = fmap getNumber $ randomRIO (1, 10)
getRandLetter :: IO Char
getRandLetter = fmap getLetter $ randomRIO (1, 26)
getNumber :: Int -> Char
getNumber i = findWithDefault ' ' i alphabet
where alphabet = fromList $ zip [1..] ['0'..'9']
getLetter :: Int -> Char
getLetter i = findWithDefault ' ' i alphabet
where alphabet = fromList $ zip [1..] ['A'..'Z']
Version 2 (interim): Based on bheklikr's comments that clean up the function mkRobotName
and help start fixing the mkRobot function. This version of the code gave an error only on test # 5 - test # 5 is associated with a change in the name of the robot, which motivates the need for mutable structures ...
{-# LANGUAGE NoMonomorphismRestriction #-}
module Robot (robotName, mkRobot, resetName) where
import Data.Map (fromList, findWithDefault)
import System.Random (Random, randomRIO)
import Control.Monad (replicateM)
data Robot = Robot (IO String)
resetName :: Robot -> IO String
resetName (Robot _) = mkRobotName >>= \name -> return name
mkRobot :: IO Robot
mkRobot = mkRobotName >>= \name -> return (Robot (return name))
robotName :: Robot -> IO String
robotName (Robot name) = name
-------------------------------------------------------------------------
--Supporting functions:
mkRobotName :: IO String
mkRobotName = replicateM 2 getRandLetter >>=
\l -> replicateM 3 getRandNumber >>=
\n -> return $ l ++ n
getRandNumber :: IO Char
getRandNumber = fmap getNumber $ randomRIO (1, 10)
getRandLetter :: IO Char
getRandLetter = fmap getLetter $ randomRIO (1, 26)
getNumber :: Int -> Char
getNumber i = findWithDefault ' ' i alphabet
where alphabet = fromList $ zip [1..] ['0'..'9']
getLetter :: Int -> Char
getLetter i = findWithDefault ' ' i alphabet
where alphabet = fromList $ zip [1..] ['A'..'Z']
Version 1 (Original) : In hindsight, this is ridiculously bad. This version failed on testing # 1, # 4, and # 6, all of which involve keeping the robot name.
{-# LANGUAGE NoMonomorphismRestriction #-}
module Robot (robotName, mkRobot, resetName) where
import Data.Map (fromList, findWithDefault)
import System.Random (Random, randomRIO)
data Robot = Robot (IO String)
resetName :: Robot -> IO Robot
resetName (Robot _) = return $ (Robot mkRobotName)
mkRobot :: IO Robot
mkRobot = return (Robot mkRobotName)
robotName :: Robot -> IO String
robotName (Robot name) = name
--the mass of code below is used to randomly generate names; it probably
--possible to do it in way fewer lines. but the crux of the main problem lies
--with the three functions above
mkRobotName :: IO String
mkRobotName = getRandLetter >>=
\l1 -> getRandLetter >>=
\l2 -> getRandNumber >>=
\n1 -> getRandNumber >>=
\n2 -> getRandNumber >>=
\n3 -> return (l1:l2:n1:n2:n3:[])
getRandNumber :: IO Char
getRandNumber = randomRIO (1,10) >>= \i -> return $ getNumber i
getNumber :: Int -> Char
getNumber i = findWithDefault ' ' i alphabet
where alphabet = fromList $ zip [1..] ['0'..'9']
getRandLetter :: IO Char
getRandLetter = randomRIO (1,26) >>= \i -> return $ getLetter i
getLetter :: Int -> Char
getLetter i = findWithDefault ' ' i alphabet
where alphabet = fromList $ zip [1..] ['A'..'Z']
source to share
Start with types based on what the tests require. mkRobot
returns something inIO
mkRobot :: IO r
robotName
takes what is returned from mkRobot
and returns IO String
.
robotName :: r -> IO String
Finally, it resetName
takes what is returned from mkRobot
and performs an action IO
. The return of this action is never used, so we'll use the block type ()
for it, which is normal for IO
no-result actions in Hasekll.
resetName :: r -> IO ()
Based on tests, anyone r
should behave as if they are mutated with resetName
. We have a number of options for things that behave as if they change in IO
: IORef
s, STRef
s, MVars
s and software transactional memory. My preference for simple problems is this IORef
. I will take a slightly different personality than you and separate IORef
from what is Robot
.
newtype Robot = Robot {name :: String}
This leaves a Robot
very clean data type. Then I will use IORef Robot
what r
is in the interface for tests.
IORef
provide five extremely useful functions for working with them, and we will use three of them. newIORef :: a -> IO (IORef a)
makes a new one IORef
containing the supplied value. readIORef :: IORef a -> IO a
reads the value stored in IORef
. modifyIORef :: IORef a -> (a -> a) -> IO ()
applies a function to the value stored in IORef
. There are two other extremely useful functions that we won't be using, writeIORef
one that sets a value without looking at what's in there, and atomicModifyIORef
one that solves about half of the shared memory problems when writing multithreaded programs. We import three which we will use
import Data.IORef (IORef, newIORef, modifyIORef, readIORef)
When we create a new one Robot
, we will create a new one IORef Robot
with newIORef
.
mkRobot :: IO (IORef Robot) mkRobot = mkRobotName >>= return . Robot >>= newIORef
When we read the name, we read Robot
with readIORef
, thenreturn
Robot
name
robotName :: IORef Robot -> IO String
robotName rr = readIORef rr >>= return . name
Finally, it resetName
will mutate IORef
. We will create a new name for the robot with mkRobotName
, then call modifyIORef
with a function that sets the name of the robot to the new name.
resetName :: IORef Robot -> IO ()
resetName rr = mkRobotName >>=
\newName -> modifyIORef rr (\r -> r {name = newName})
The function is the \r -> r {name = newName}
same as const (Robot newName)
, except that it will change the value name
if we later decide to add another field to the data type Robot
.
source to share