如何使用IO monad中的可变结构

ice*_*man 3 haskell mutable io-monad

TL; DR:
如何确保给定语句中randomRIO(from System.Random)生成的值的持久性do
如何使用IO Monad中的可变结构?

我最初的问题是(非常)错误 - 我正在更新标题,以便将来想要理解在IO monad中使用可变结构的读者可以找到这篇文章.

更长的版本:

抬头:这看起来很长,但很多只是我概述了如何exercism.io运作.(更新:最后两个代码块是我的代码的旧版本,作为参考,以防未来的读者希望根据评论/答案跟随代码中的迭代.)

练习概述:

我正在Robot Name从(非常有教育意义的)exercism.io开始练习.练习涉及创建一个Robot能够存储名称的数据类型,该名称是随机生成的(练习Readme包括在下面).

对于那些不熟悉它的人,exercism.io学习模型基于学生生成的代码的自动测试.每个练习都包含一系列测试(由测试作者编写),解决方案代码必须能够通过所有测试.我们的代码必须通过给定练习的测试文件中的所有测试,然后我们才能进入下一个练习 - 一个有效的模型,imo.(Robot Name运动#20左右.)

在这个特殊的练习中,我们会要求创建一个Robot数据类型和三个附带功能:mkRobot,robotNameresetName.

  • mkRobot 生成一个实例 Robot
  • robotName生成并"返回"未命名的唯一名称Robot(即,robotName不覆盖预先存在的名称); 如果Robot已有名称,则只返回现有名称
  • resetName 用新的名称覆盖预先存在的名称.

在这个特定的练习中,有7个测试.测试检查:

  • 0)robotName生成符合指定模式的名称(名称长度为5个字符,由两个字母后跟三个数字组成,例如AB123,XQ915等)
  • 1)分配的名称robotName是持久性的(即,假设我们创建机器人A并使用它来为他(或她)分配名称robotName; robotName第二次调用(在机器人A上)不应该覆盖他的名字)
  • 2)robotName为不同的机器人生成唯一的名称(即,它测试我们实际上是随机化过程)
  • 3)resetName生成符合指定模式的名称(类似于测试#0)
  • 4)分配的名称resetName是持久的
  • 5)resetName指定一个不同的名称(即,resetName给机器人一个与其当前名称不同的名称)
  • 6)一次resetName仅影响一个机器人(即,假设我们有机器人A和机器人B;重置机器人A的名称不应该影响机器人B的名称)和(ii)由resetName持久生成的名称是持久的

作为参考,这是测试本身:https://github.com/dchaudh/exercism-haskell-solutions/blob/master/robot-name/robot-name_test.hs


我被困在哪里:

版本1(原始帖子): 目前,我的代码在三个测试(#1,#4和#6)上失败,所有测试都与机器人名称的持久性有关..

版本2 :(临时) 现在我的代码仅在一个测试(#5)上失败 - 测试5与更改我们已经创建的机器人的名称有关 (感谢bheklikr帮助我清理他的有用评论版本1)

版本3(最终):由于Cirdec在下面的详细文章,代码现已修复(并通过了所有测试).为了将来读者的利益,我将包括最终版本的代码以及两个早期版本(因此他们可以跟随各种评论/答案).


版本3(最终版): 这是基于Cirdec下面答案的最终版本(我强烈推荐阅读).事实证明,我的原始问题(询问如何使用System.Random创建持久变量)完全错误,因为我的初始实现是不健全的.我的问题应该是如何使用IO monad中的可变结构(Cirdec在下面解释).

{-# 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']
Run Code Online (Sandbox Code Playgroud)

版本2(临时): 基于bheklikr的注释,它清理mkRobotName功能并帮助开始修复mkRobot功能.这个版本的代码只在测试#5上产生了错误 - 测试#5与更改机器人的名称有关,这促使了对可变结构的需求......

{-# 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']
Run Code Online (Sandbox Code Playgroud)

版本1(原文):回想起来,这是可笑的坏事.这个版本在测试#1,#4和#6上失败,所有测试都与机器人名称的持久性有关.

{-# 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']
Run Code Online (Sandbox Code Playgroud)

Cir*_*dec 8

让我们从类型开始,基于测试所需的内容.mkRobot返回一些东西IO

mkRobot :: IO r
Run Code Online (Sandbox Code Playgroud)

robotName获取返回的内容mkRobot并返回一个IO String.

robotName :: r -> IO String
Run Code Online (Sandbox Code Playgroud)

最后,resetName获取返回的内容mkRobot并生成一个IO动作.从不使用此操作的返回,因此我们将使用单位类型(),这对于IOHasekll中没有结果的操作是正常的.

resetName :: r -> IO ()
Run Code Online (Sandbox Code Playgroud)

根据测试,任何r需要能够像它一样的行为变异resetName.我们有许多选项可以表现为可变的IO: IORefs,STRefs,MVarss和软件事务内存.我对简单问题的偏好是IORef.我将采取与你略有不同的方法,并将其IORef与现实分开Robot.

newtype Robot = Robot {name :: String}
Run Code Online (Sandbox Code Playgroud)

这留下Robot了非常纯粹的数据类型.然后我将IORef Robot用于r测试界面中的内容.

IORef提供五个非常有用的功能来处理它们,我们将使用它们中的三个.newIORef :: a -> IO (IORef a)使新的IORef持有所提供的价值.readIORef :: IORef a -> IO a读取存储在中的值IORef.modifyIORef :: IORef a -> (a -> a) -> IO ()将函数应用于存储在的值IORef.还有另外两个我们将不会使用的非常有用的函数,writeIORef它们设置值而不查看atomicModifyIORef其中的内容,并且在编写多线程程序时解决了大约一半的共享内存问题.我们将导入我们将使用的三个

import Data.IORef (IORef, newIORef, modifyIORef, readIORef)
Run Code Online (Sandbox Code Playgroud)

当我们创造一个新的时候,Robot我们将会创造一个新IORef RobotnewIORef.

mkRobot :: IO (IORef Robot)
mkRobot = mkRobotName >>= return . Robot >>= newIORef
Run Code Online (Sandbox Code Playgroud)

当我们读到这个名字的时候,我们会读到Robotwith readIORef,然后returnRobot'sname

robotName :: IORef Robot -> IO String
robotName rr = readIORef rr >>= return . name
Run Code Online (Sandbox Code Playgroud)

最后,resetName将改变IORef.我们将为机器人创建一个新名称mkRobotName,然后modifyIORef使用将机器人名称设置为新名称的函数调用.

resetName :: IORef Robot -> IO ()
resetName rr = mkRobotName >>=
               \newName -> modifyIORef rr (\r -> r {name = newName})
Run Code Online (Sandbox Code Playgroud)

该函数\r -> r {name = newName}是相同的const (Robot newName),除了它只会改变name我们以后决定将其他字段添加到Robot数据类型.