【问题标题】:How to work with mutable structures in the IO monad如何在 IO monad 中使用可变结构
【发布时间】:2014-11-07 20:35:43
【问题描述】:

TL;DR:
我如何确保randomRIO(来自System.Random)生成的值在给定的@987654324 中的持久性@ 声明?
如何在 IO Monad 中使用可变结构?

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

加长版:

提醒: 这看起来很长,但其中很多只是我概述了exercism.io 的工作原理。 (更新:最后两个代码块是我的代码的旧版本,作为参考包含在内,以防将来的读者希望根据 cmets/answers 跟随代码中的迭代。)

练习概述:

我正在研究来自(极具指导意义的)exercism.io 的Robot Name 练习。该练习涉及创建一个Robot 数据类型,该数据类型能够存储随机生成的名称(练习Readme 包含在下面)。

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

在这个特定的练习中,我们被要求创建一个 Robot 数据类型和三个附带的函数:mkRobotrobotNameresetName

  • mkRobot 生成 Robot 的实例
  • robotName 为未命名的 Robot 生成并“返回”一个唯一名称(即,robotName 不会覆盖预先存在的名称);如果 Robot 已经有了名字,它只会“返回”现有的名字
  • resetName 用新名称覆盖现有名称。

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

  • 0) robotName 生成符合指定模式的名称(a 名称长度为 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) 由 @ 生成的名称987654352@是持久的

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


我被困在哪里:

版本 1(原帖): 目前,我的代码在三个测试(#1、#4 和 #6)上都失败了,所有这些都与机器人的名字。.

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

第 3 版(最终版):感谢 Cirdec 在下面的详尽帖子,代码现已修复(并通过了所有测试)。为了将来读者的利益,我将代码的最终版本与两个早期版本一起包括在内(以便它们可以跟随各种 cmets/answers)。


第 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 的 cmets 清理 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']

【问题讨论】:

  • 你不应该有data Robot = Robot StringmkRobot = mkRobotName >>= Robot吗?您现在正在做的是在 Robot 构造函数中存储一个 IO 操作,我想您想要做的是返回一个带有随机 StringRobot,而不是返回随机字符串的操作.
  • 还有mkRobotName = do { l <- replicateM 2 getRandLetter; n <- replicateM 3 getRandNumber; return $ l ++ n
  • 还有getRandNumber = fmap getNumber $ randomRIO (1, 10) 和类似的getRandLetter = fmap getLetter $ randomRIO (1, 26)
  • 我很惊讶你在我发帖后的大约 2 分钟内想出了mkRobotNamegetRandNumbergetRandLetter 的更短版本。脱帽致敬...另外,关于Robot 的类型,我选择使用Robot (IO String) 而不是Robot String(我同意这更有意义),因为测试文件所期望的类型...跨度>
  • 测试文件是否期望您拥有Robot (IO String)?如果是这样,请执行mkRobot = mkRobotName >>= Robot . return。那么robotName可以不用管,Robot里面的action就是return一个常量。

标签: haskell mutable io-monad


【解决方案1】:

让我们根据测试的要求从类型开始。 mkRobotIO 中返回一些东西

mkRobot :: IO r

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

robotName :: r -> IO String

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

resetName :: r -> IO ()

根据测试,无论r 是什么,都需要能够表现得像被resetName 变异了一样。对于在IO 中表现为可变的事物,我们有许多选项:IORefs、STRefs、MVarss 和软件事务内存。我对简单问题的首选是IORef。我将采取与您稍有不同的策略,并将IORefRobot 分开。

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 时,我们将使用newIORef 创建一个新的IORef Robot

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

当我们读取名称时,我们将读取RobotreadIORef,然后读取returnRobotname

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) 相同,只是它只会在我们稍后决定向Robot 数据类型添加一些其他字段时更改name

【讨论】:

  • 这非常有用! (这证实了我不知道我在第一个版本的帖子中要问什么的事实......)这是我第一次使用可变结构,我很欣赏详尽的分步解释。跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-27
  • 1970-01-01
  • 1970-01-01
  • 2022-01-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多