每次调用函数时生成一个顺序值或随机值

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.STMControl.Concurrent.Async.AsyncData.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 =<< action1do { x <- action1; action2 x }

    的缩写