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 的东西?
首先,不要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.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
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 }