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

Cra*_*tow 1 haskell state-monad

我需要让每个实例 Sphere获得一个唯一标识符,以便没有两个Spheres 相等。我不会提前知道需要制作多少个球体,因此需要一次制作一个,但仍会增加标识符。

我尝试过的大多数解决方案都有这个问题,我最终得到了一个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))
Run Code Online (Sandbox Code Playgroud)

这也可以工作,并且在 REPL 中工作得很好,但是当我把它放在一个函数中时,它只在第一次返回一个新值,然后返回相同的值。

import Data.Unique
sphere = Sphere { identifier = (hashUnique $ unsafePerformIO newUnique) }
Run Code Online (Sandbox Code Playgroud)

我知道认为这一切都导致了 State Monad,但我还不明白。有没有其他方法可以“完成工作”,而不用咬掉所有其他 monad 的东西?

Jon*_*rdy 6

首先,不要unsafePerformIO在这里使用。无论如何,它不会做你想做的事:它不会“摆脱a一个IO a”,因为 anIO 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
Run Code Online (Sandbox Code Playgroud)

显然,这是非常重复且容易出错的,因为我们必须在每一步传递正确的状态。该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
Run Code Online (Sandbox Code Playgroud)

State是我通常会达到的,因为它可以在纯代码中使用,并且可以与其他效果结合使用而不会遇到太多麻烦StateT,并且因为它实际上在底层是不可变的,只是在传递值之上的抽象,您可以轻松地和有效地保存和回滚状态。

如果您想使用随机性,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
Run Code Online (Sandbox Code Playgroud)

使用UniqueID(也不能保证是唯一的,但不太可能发生冲突):

import Data.Unique

newSphere :: IO Sphere
newSphere = Sphere <$> newId

newId :: IO Id
newId = hashUnique <$> newUnique

-- …
Run Code Online (Sandbox Code Playgroud)

使用顺序 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

-- …
Run Code Online (Sandbox Code Playgroud)

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 }

  • 额外+1,因为我们只能投票一次。使用`unsafePerformIO`来避免单子就像点燃你的房子来打开后门:它不会起作用,并且存在产生灾难性后果的不小的危险。拥抱单子!你无法抗拒他们的海妖之歌! (2认同)