我需要使Sphere
的每个实例都获得唯一的标识符,以便没有两个Sphere
相等。我不会提前知道要制作多少个球体,因此每次都需要制作一个,但是仍然要增加标识符。
我尝试过的大多数解决方案都遇到了这个问题,我最终遇到一个IO a
,并且需要unsafePerformIO
来获取该值。
此代码非常接近,但是生成的identifier
始终相同:
module Shape ( Sphere (..)
, sphere
, newID
) where
import System.Random
import System.IO.Unsafe (unsafePerformIO)
data Sphere = Sphere { identifier :: Int
} deriving (Show, Eq)
sphere :: Sphere
sphere = Sphere { identifier = newID }
newID :: Int
newID = unsafePerformIO (randomRIO (1, maxBound :: Int))
这同样适用,并且在REPL中也很好用,但是当我将其放在函数中时,它只会在第一次时返回一个新值,然后才返回相同的值。
import Data.Unique
sphere = Sphere { identifier = (hashUnique $ unsafePerformIO newUnique) }
我知道这一切都导致了Monad州的出现,但我还不明白。没有其他方法可以在不破坏所有其他monad东西的情况下“完成工作”吗?
答案 0 :(得分:6)
首先,请不要在此处使用unsafePerformIO
。无论如何,它不会做您想要的事情:它不会“使a
脱离IO a
”,因为IO a
并不包含 a
;相反,unsafePerformIO
隐藏 IO 动作到一个神奇的值后面,该值会在某人评估时执行该值,由于懒惰,可能多次或从不发生。
有没有其他方法可以“完成工作”而又不破坏所有其他monad的东西?
不是。如果要生成唯一的ID,则必须保持某种状态。 (您也许可以完全避免使用唯一的ID,但是我没有足够的上下文可以说。)状态可以通过几种方式处理:手动传递值,使用State
来简化该模式,或者使用IO
。
假设我们要生成顺序ID。那么状态只是一个整数。生成新ID的函数可以简单地将该状态作为输入并返回更新后的状态。我想您会立刻明白为什么太这么简单,因此我们倾向于避免编写如下代码:
-- Differentiating “the next-ID state” from “some ID” for clarity.
newtype IdState = IdState Id
type Id = Int
-- Return new sphere and updated state.
newSphere :: IdState -> (Sphere, IdState)
newSphere s0 = let
(i, s1) = newId s0
in (Sphere i, s1)
-- Return new ID and updated state.
newId :: IdState -> (Id, IdState)
newId (IdState i) = (i, IdState (i + 1))
newSpheres3 :: IdState -> ((Sphere, Sphere, Sphere), IdState)
newSpheres3 s0 = let
(sphere1, s1) = newSphere s0
(sphere2, s2) = newSphere s1
(sphere3, s3) = newSphere s2
in ((sphere1, sphere2, sphere3), s3)
main :: IO ()
main = do
-- Generate some spheres with an initial ID of 0.
-- Ignore the final state with ‘_’.
let (spheres, _) = newSpheres3 (IdState 0)
-- Do stuff with them.
print spheres
显然,这是非常重复且容易出错的,因为我们必须在每个步骤中传递正确的状态。 State
类型具有一个Monad
实例,该实例抽象出了这种重复模式,并允许您改用do
表示法:
import Control.Monad.Trans.State (State, evalState, state)
newSphere :: State IdState Sphere
newSphere = do
i <- newId
pure (Sphere i)
-- or:
-- newSphere = fmap Sphere newId
-- newSphere = Sphere <$> newId
-- Same function as before, just wrapped in ‘State’.
newId :: State IdState Id
newId = state (\ (IdState i) -> (i, IdState (i + 1)))
-- Much simpler!
newSpheres3 :: State IdState (Sphere, Sphere, Sphere)
newSpheres3 = do
sphere1 <- newSphere
sphere2 <- newSphere
sphere3 <- newSphere
pure (sphere1, sphere2, sphere3)
-- or:
-- newSpheres3 = (,,) <$> newSphere <*> newSphere <*> newSphere
main :: IO ()
main = do
-- Run the ‘State’ action and discard the final state.
let spheres = evalState newSpheres3 (IdState 0)
-- Again, do stuff with the results.
print spheres
State
是我通常要达到的目标,因为它可以在纯代码中使用,并且可以与其他效果结合使用StateT
而没有很多麻烦,因为它实际上是不可变的只是在传递值的基础上的一种抽象,您可以轻松高效地保存和回滚状态。
如果您想使用随机性Unique
或使您的状态实际变为可变,则通常必须使用IO
,因为IO
专门用于通常通过与外界或其他线程进行交互来打破参照透明性。 (也有诸如ST
之类的替代方法,用于将命令性代码置于纯API后面,或诸如Control.Concurrent.STM.STM
,Control.Concurrent.Async.Async
和Data.LVish.Par
之类的并发API,但我不会讨论他们在这里。)
幸运的是,这与上面的State
代码非常相似,因此,如果您了解如何使用一种代码,那么应该更容易理解另一种代码。
使用IO
的随机ID(不保证唯一):
import System.Random
newSphere :: IO Sphere
newSphere = Sphere <$> newId
newId :: IO Id
newId = randomRIO (1, maxBound :: Id)
newSpheres3 :: IO (Sphere, Sphere, Sphere)
newSpheres3 = (,,) <$> newSphere <*> newSphere <*> newSphere
main :: IO ()
main = do
spheres <- newSpheres3
print spheres
具有Unique
个ID(也不能保证是唯一的,但不太可能发生冲突):
import Data.Unique
newSphere :: IO Sphere
newSphere = Sphere <$> newId
newId :: IO Id
newId = hashUnique <$> newUnique
-- …
具有顺序ID,并使用可变的IORef
:
import Data.IORef
newtype IdSource = IdSource (IORef Id)
newSphere :: IdSource -> IO Sphere
newSphere s = Sphere <$> newId s
newId :: IdSource -> IO Id
newId (IdSource ref) = do
i <- readIORef ref
writeIORef ref (i + 1)
pure i
-- …
在某些时候,您将必须了解如何使用do
表示法和函子,应用程序和monad,因为这就是Haskell中表示效果的方式。不过,您不一定要了解它们的内部工作原理的每一个细节,就只能使用。当我用一些经验法则学习Haskell时,我走得很远,例如:
do
语句可以是:
操作:(action :: m a)
通常m ()
位于中间
通常以pure (expression :: a) :: m a
结尾
表达式的let
绑定:let (var :: a) = (expression :: a)
用于操作的单子绑定:(var :: a) <- (action :: m a)
f <$> action
将纯函数应用于操作,是do { x <- action; pure (f x) }
的缩写
f <$> action1 <*> action2
将多个参数的纯函数应用于多个动作,是do { x <- action1; y <- action2; pure (f x y) }
的缩写
action2 =<< action1
是do { x <- action1; action2 x }
的缩写