How to work with mutable structures in the IO monad

iceman

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 name
  • resetName overwrites a pre-existing name with a new one.

In this particular exercise, there are 7 tests. The tests checks that:

  • 0) 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.)
  • 1) a name assigned by 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)
  • 2) robotName generates unique names for different robots (i.e., it tests that we're actually randomizing the process)
  • 3) resetName generates names that conform to the specified pattern (similar to test #0)
  • 4) a name assigned by resetName is persistent
  • 5) resetName assigns a different name (i.e., resetName gives a robot a name that's different form it's current name)
  • 6) 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 persistent

As 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']
Cirdec

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: IORefs, STRefs, MVarss, 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.

IORefs 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.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

From Dev

How does IO monad work in System.Random

From Dev

How to put mutable Vector into State Monad

From Dev

How to put mutable Vector into State Monad

From Dev

Mutable data structures in Erlang

From Dev

How to get ReaderT to work with another monad transformer?

From Dev

How to get ReaderT to work with another monad transformer?

From Dev

How does this weird monad bind work?

From Dev

SWIG ignores %mutable in class; how to work around?

From Dev

How to implement a short-circuit with IO monad in Scala

From Dev

How do I perform IO inside of Parsec's monad?

From Dev

How to preserve the state of the monad stack in the IO exception handler?

From Dev

How to properly force evaluation of pure value in IO monad?

From Dev

How to asign a value from the IO monad to a RankNType qualified constructor

From Dev

How do I perform IO inside of Parsec's monad?

From Dev

haskell how to print when function doesn't return IO monad?

From Dev

Behaviour of a Future in an IO Monad

From Dev

Delimiting the IO monad

From Dev

Logical AND strictness with IO monad

From Dev

Is IO a Free Monad?

From Dev

Monad transformers: IO and state

From Dev

How to work with nested data structures in Elixir

From Dev

How to implement qsort() to work for an array of structures?

From Dev

How does the state monad work? (without code explanation)

From Dev

How does the Reader monad's "ask" function work?

From Dev

How does scope work in Io?

From Dev

Why IO is a monad instead of a comonad?

From Dev

Simplify Maybe monad in IO block

From Dev

flatMap and For-Comprehension with IO Monad

From Dev

Getting Result of IO Monad in ghci

Related Related

HotTag

Archive