TL;DR:
How do I ensure persistence of values generated by randomRIO
(from System.Random
) within a given do
statement?
How do I work with mutable structures in the IO Monad?
My initial question was (so very) wrong - I'm updating the title so future readers who want to understand use mutable structures in the IO monad can find this post.
Longer version:
A heads up:
This looks long but a lot of it is just me giving an overview of how exercism.io
works. (UPDATE: the last two code-blocks are older versions of my code which are included as reference, in case future readers would like to follow along with the iterations in the code based on the comments/answers.)
Overview of Exercise:
I'm working on the Robot Name
exercise from (the extremely instructive) exercism.io. The exercise involves creating a Robot
data type which is capable of storing a name, which is randomly generated (exercise Readme
is included below).
For those who aren't familiar with it, the exercism.io
learning model is based on automated testing of student-generated code. Each exercise consists of a series of tests (written by the test author) and the solution code must be able to pass all of them. Our code must pass all tests in a given exercise's test file, before we can move to the next exercise - an effective model, imo. (Robot Name
is exercise #20 or so.)
In this particular exercise, we're asked to create a Robot
data-type and three accompanying functions: mkRobot
, robotName
and resetName
.
mkRobot
generates an instance of a Robot
robotName
generates and "returns" a unique name for a unnamed Robot
(i.e., robotName
does not overwrite a pre-existing name); if a Robot
already has a name, it simply "returns" the existing nameresetName
overwrites a pre-existing name with a new one.In this particular exercise, there are 7 tests. The tests checks that:
robotName
generates names that conforms to the specified pattern (a
name is 5 characters long and is made up of two letters followed by
three digits, e.g., AB123, XQ915, etc.)robotName
is persistent (i.e., let's say we create robot A and assign him (or her) a name using robotName
; calling robotName
a second time (on robot A) shouldn't overwrite his name)robotName
generates unique names for different robots (i.e., it tests that we're actually randomizing the process)resetName
generates names that conform to the specified pattern (similar to test #0)resetName
is persistentresetName
assigns a different name (i.e., resetName
gives a robot a name that's different form it's current name)resetName
affects only one robot at a time (i.e., let's say we have robot A and robot B; resetting robot A's name shouldn't affect robot B's name) AND (ii) names that are generated by resetName
are persistentAs reference, here's the test itself: https://github.com/dchaudh/exercism-haskell-solutions/blob/master/robot-name/robot-name_test.hs
Where I'm stuck:
Version 1 (original post): At the moment, my code fails on three tests (#1, #4 and #6) all of which have to do with persistence of a robot's name..
Version 2: (interim) Now my code fails on one test (#5) only - test 5 has to do with changing the name of a robot that we've already created
(thanks to bheklikr for his helpful comments which helped me clean up version 1)
Version 3 (final): The code is now fixed (and passes all tests) thanks to Cirdec's thorough post below. For future reader's benefit, I'm including the final version of the code along with the two earlier versions (so they can follow along with the various comments/answers).
Version 3 (Final): Here's the final version based on Cirdec's answer below (which I'd highly recommend reading). It turns out that my original question (which asked how to create persistent variables using System.Random) was just totally wrong because my initial implementation was unsound. My question should instead have asked how to work with mutable structures in the IO monad (which Cirdec explains 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 which clean up the mkRobotName
function and which help start fixing the mkRobot function. This version of the code yielded an error on test #5 only - test #5 has to do with changing a robot's name, 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 retrospect, this is laughably bad. This version failed on tests #1, #4 and #6 all of which are related to persistence of a robot's 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's 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']
Let's start with the types, based on what is required by the tests. mkRobot
returns something in IO
mkRobot :: IO r
robotName
takes what is returned from mkRobot
and returns an IO String
.
robotName :: r -> IO String
Finally, resetName
takes what is returned from mkRobot
and produces an IO
action. The return of this action is never used, so we'll use the unit type ()
for it which is normal for IO
actions with no result in Hasekll.
resetName :: r -> IO ()
Based on the tests, whatever r
is needs to be able to behave like it is mutated by resetName
. We have a number of options for things that behave like they are mutable in IO
: IORef
s, STRef
s, MVars
s, and software transactional memory. My go-to preference for simple problems is the IORef
. I'm going to take a slightly different tack than you, and separate the IORef
from what a Robot
is.
newtype Robot = Robot {name :: String}
This leaves Robot
a very pure data type. Then I'll use IORef Robot
for what r
is in the interface to the tests.
IORef
s provide five extremely useful functions for working with them, which we will use three of. newIORef :: a -> IO (IORef a)
makes a new IORef
holding the provided value. readIORef :: IORef a -> IO a
reads the value stored in the IORef
. modifyIORef :: IORef a -> (a -> a) -> IO ()
applies the function to the value stored in the IORef
. There are two other extremely useful functions we won't use, writeIORef
which sets the value without looking at what's there, and atomicModifyIORef
which solves about half of the shared memory problems in writing multi-threaded programs. We'll import the three that we will use
import Data.IORef (IORef, newIORef, modifyIORef, readIORef)
When we make a new Robot
we'll be making a new IORef Robot
with newIORef
.
mkRobot :: IO (IORef Robot)
mkRobot = mkRobotName >>= return . Robot >>= newIORef
When we read the name, we'll read the Robot
with readIORef
, then return
the Robot
's name
robotName :: IORef Robot -> IO String
robotName rr = readIORef rr >>= return . name
Finally, resetName
will mutate the IORef
. We'll make a new name for the robot with mkRobotName
, then call modifyIORef
with a function that sets the robot's name to the new name`.
resetName :: IORef Robot -> IO ()
resetName rr = mkRobotName >>=
\newName -> modifyIORef rr (\r -> r {name = newName})
The function \r -> r {name = newName}
is the same as const (Robot newName)
, except that it will only change the name
if we later decide to add some other field to the Robot
data type.