3

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数据类型和三个附带的函数mkRobotrobotNameresetName

  • mkRobot生成一个实例Robot
  • robotName生成并“返回”未命名的唯一名称Robot(即,robotName不覆盖预先存在的名称);如果 aRobot已经有名称,它只是“返回”现有名称
  • 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']

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

版本 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']
4

1 回答 1

8

让我们根据测试的要求从类型开始。mkRobot返回的东西IO

mkRobot :: IO r

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

robotName :: r -> IO String

最后,resetName获取返回的内容mkRobot并产生一个IO动作。这个动作的返回从未被使用过,所以我们将使用它的单位类型(),这对于IO在 Hasekll 中没有结果的动作是正常的。

resetName :: r -> IO ()

根据测试,任何r需要能够表现得像它一样的东西都被resetName. 对于在s、IOs 、s 和软件事务内存中表现得像可变的事物,我们有许多选项。我对简单问题的首选是. 我将采取与您略有不同的策略,并将a 与a分开。IORefSTRefMVarsIORefIORefRobot

newtype Robot = Robot {name :: String}

这留下Robot了一个非常纯粹的数据类型。然后我将使用测试接口中的IORef Robot内容r

IORefs 提供了五个非常有用的函数来使用它们,我们将使用其中的三个。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)

当我们制作一个新的时Robot,我们将IORef Robot使用newIORef.

mkRobot :: IO (IORef Robot)
mkRobot = mkRobotName >>= return . Robot >>= newIORef

当我们阅读名称时,我们会阅读Robotwith readIORef,然后returnRobot'sname

robotName :: IORef Robot -> IO String
robotName rr = readIORef rr >>= return . name

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

resetName :: IORef Robot -> IO ()
resetName rr = mkRobotName >>=
               \newName -> modifyIORef rr (\r -> r {name = newName})

该功能\r -> r {name = newName}与 相同const (Robot newName),只是它只会在name我们稍后决定向Robot数据类型添加其他字段时更改。

于 2014-11-07T21:50:38.817 回答