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 instance Robot

  • robotName

    generates and "returns" a unique name for the unnamed one Robot

    (ie robotName

    does not overwrite a pre-existing name); if a Robot

    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 with robotName

    ; calling the second robotName

    (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 generated resetName

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

      

+3


source to share


1 answer


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

.

+8


source







All Articles