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

时间:2020-07-30 19:31:12

标签: haskell state-monad

我需要使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东西的情况下“完成工作”吗?

1 个答案:

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

相关问题