每次调用函数时生成一个顺序值或随机值
Generate a sequential or random value each time a function is called
我需要让 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 中工作得很好,但是当我把它放在一个函数中时,它第一次 returns 只是一个新值,之后是相同的值。
import Data.Unique
sphere = Sphere { identifier = (hashUnique $ unsafePerformIO newUnique) }
我知道这一切都会导致 State Monad,但我还不明白。有没有其他方法可以“完成工作”,而不用咬掉所有其他 monad 的东西?
首先这里不要用unsafePerformIO
。它无论如何都不会做你想做的事:它不会“从 IO a
中得到 a
”,因为 IO a
不 包含 一个a
;相反,unsafePerformIO
隐藏 一个 IO action 背后的魔法值 executes action when有人 评估 价值,这可能会发生 多次 或 从不 因为懒惰。
Is there no other way that will "get the job done", without biting off all the other monad stuff?
不是真的。如果你想生成唯一的 ID,你将不得不保持 一些 的状态。 (您也许可以完全避免需要唯一 ID,但我没有足够的上下文可以说。)可以通过几种方式处理状态:手动传递值,使用 State
来简化该模式,或者使用 IO
.
假设我们要生成顺序 ID。那么状态只是一个整数。生成新 ID 的函数可以简单地将该状态作为输入并 return 更新后的状态。我想您马上就会明白为什么 太 简单了,所以我们倾向于避免编写这样的代码:
-- 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
没有太多麻烦,而且因为它实际上是 immutable 在幕后,只是传递值之上的抽象,您可以轻松高效地保存和回滚状态。
如果你想使用随机性,Unique
,或者让你的状态实际上可变,你通常必须使用IO
,因为IO
专门用于打破 参照透明度 那样,通常是通过与外界或其他线程交互。 (还有一些替代方案,例如 ST
,用于将命令式代码置于纯 API 或并发 API 之后,例如 Control.Concurrent.STM.STM
、Control.Concurrent.Async.Async
和 Data.LVish.Par
, 但我不会在这里深入探讨。)
幸运的是,这与上面的 State
代码非常相似,所以如果您了解如何使用其中一个,应该更容易理解另一个。
随机 ID 使用 IO
(不保证唯一):
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
符号和仿函数、应用程序和单子,因为这正是 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 }
的缩写
我需要让 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 中工作得很好,但是当我把它放在一个函数中时,它第一次 returns 只是一个新值,之后是相同的值。
import Data.Unique
sphere = Sphere { identifier = (hashUnique $ unsafePerformIO newUnique) }
我知道这一切都会导致 State Monad,但我还不明白。有没有其他方法可以“完成工作”,而不用咬掉所有其他 monad 的东西?
首先这里不要用unsafePerformIO
。它无论如何都不会做你想做的事:它不会“从 IO a
中得到 a
”,因为 IO a
不 包含 一个a
;相反,unsafePerformIO
隐藏 一个 IO action 背后的魔法值 executes action when有人 评估 价值,这可能会发生 多次 或 从不 因为懒惰。
Is there no other way that will "get the job done", without biting off all the other monad stuff?
不是真的。如果你想生成唯一的 ID,你将不得不保持 一些 的状态。 (您也许可以完全避免需要唯一 ID,但我没有足够的上下文可以说。)可以通过几种方式处理状态:手动传递值,使用 State
来简化该模式,或者使用 IO
.
假设我们要生成顺序 ID。那么状态只是一个整数。生成新 ID 的函数可以简单地将该状态作为输入并 return 更新后的状态。我想您马上就会明白为什么 太 简单了,所以我们倾向于避免编写这样的代码:
-- 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
没有太多麻烦,而且因为它实际上是 immutable 在幕后,只是传递值之上的抽象,您可以轻松高效地保存和回滚状态。
如果你想使用随机性,Unique
,或者让你的状态实际上可变,你通常必须使用IO
,因为IO
专门用于打破 参照透明度 那样,通常是通过与外界或其他线程交互。 (还有一些替代方案,例如 ST
,用于将命令式代码置于纯 API 或并发 API 之后,例如 Control.Concurrent.STM.STM
、Control.Concurrent.Async.Async
和 Data.LVish.Par
, 但我不会在这里深入探讨。)
幸运的是,这与上面的 State
代码非常相似,所以如果您了解如何使用其中一个,应该更容易理解另一个。
随机 ID 使用 IO
(不保证唯一):
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
符号和仿函数、应用程序和单子,因为这正是 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 }