我是Haskell的初学者,对于如何在某些纯函数(即非常简单的size
函数)中出现一些副作用感到不安。
size :: [Int] -> StateT Int IO ()
size = fmap (\x -> do
num <- get
put (num + 1)
return x) -- some pseudo code like this...
我知道这里有很多错误... return
,我的意思是此lambda本身会返回x
,因此列表的值可能不会更改...实际上,我想使用StateT
会产生一些副作用。我该怎么办?谢谢。
答案 0 :(得分:5)
首先,在学习过程中的这一点上,您可能不应该担心“副作用”。另外,您正在尝试混用两个State
和IO
两个monad,以至于您似乎都没有掌握任何一个。因此,您可能应该更轻松一些。
可以使用IO
在IORefs
monad中执行状态操作,您可以将其视为可变变量。如果我是你,我还不会去那里。然后是State
monad,粗略地说,它是在纯环境中模拟有状态函数的便捷方法。
从理论上讲,您可以将状态函数f :: a -> b
视为类型为f :: (a,s) -> (b,s)
的纯函数,其中s
表示可以访问和更改的某些状态。上面的代码不太适合monad框架,因为在monad m
中,我们希望a -> m b
表示从a
到b
的有效函数。但是很容易适应。可以不使用类型(a,s) -> (b,s)
来获得a -> s -> (b,s)
,我们将m b
设为s -> (b,s)
,因此a -> m b
代表a -> s -> (b,s)
。
这就是单子State s
的含义。对于每种类型b
,类型State s b
为s -> (b,s)
,可以理解为“给我丢失的初始状态s
,这样我就可以计算出b
有状态函数s
是a -> State s b
,可以理解为“该函数采用a -> s -> (b,s)
并产生给定初始值的计算状态a
产生结果s
和最终状态b
。
这只是为了让您大致了解其工作方式。现在,这里有一些代码可以满足您的需求。让我们从一个简单的方法开始。
s
类型为size :: [Int] -> State Int ()
size [] = put 0
size (x:xs) = do size xs
num <- get
put (num + 1)
,因为您只是在更新整数状态而没有返回任何值(该状态是我们所关心的)。
该过程与用于计算大小(没有累加器)的常规递归函数非常相似,但是我们通过更新状态来完成工作。要运行此示例,只需
State Int ()
用于某些runState (size list) 0
。请注意,此处的初始状态list
无关紧要,因为该算法的工作原理是将空列表的状态设置为0
,然后为每个元素添加0
。
现在可以累积使用的版本
1
再次运行此示例即可,
sizeAc :: [Int] -> State Int ()
sizeAc [] = return ()
sizeAc (x:xs) = do num <- get
put (num + 1)
sizeAc xs
请注意,在这种情况下,您必须使用runState (sizeAc list) 0
作为初始状态。该函数的作用是,对于列表的每个元素,它通过在状态值上加一个来更新状态。对于空列表,它什么也不做。
最后一个带有0
的版本,因为它出现在您的初次尝试中。首先,我们执行计数动作。
map
此操作包括访问状态并使用添加的单元对其进行更新。然后为列表中的每个元素构建此类操作的列表。
count :: State Int ()
count = do num <- get
put (num + 1)
请注意结果的类型是列表。结果是一个列表,其中所有元素都是动作sizeAux' :: [Int] -> [State Int ()]
sizeAux' xs = map (\x -> count) xs
。然后,我们使用count
依次执行这些操作,其类型如下(专门用于列表和特定的monad)。
sequence_
结果函数为
sequence_ :: [m a] -> m ()
sequence_ :: [State Int ()] -> State Int ()
再次可以通过
运行size' :: [Int] -> State Int ()
size' xs = sequence_ (sizeAux' xs)
再次在这里注意初始状态runState (size' list) 0
是必不可少的。
在这一点上,这可能仍然有些复杂。您需要对monad类,状态符号和州monad的特殊性有更好的了解。无论如何,这是您应该去的地方,而不是将State与IO混合使用。
答案 1 :(得分:3)
对于来自命令式世界的程序员,我认为最熟悉的答案是for
和for_
。示例:
import Data.Foldable
size :: [Int] -> StateT Int IO ()
size xs = for_ xs $ \x -> do -- similar to "for x in xs do ..."
num <- get
-- IO example:
lift $ putStrLn $ "Now incrementing " ++ num
put (num + 1)
以上代码作为副作用,会增加Int
状态,但最终会返回无聊的伪值()
。如果我们也想返回最后的Int
状态,则需要使用:
size :: [Int] -> StateT Int IO Int -- return Int instead of ()
size xs = do
for_ xs $ \x -> do
num <- get
lift $ putStrLn $ "Now incrementing " ++ num
put (num + 1)
get -- return the last state
(还要注意,如果初始Int
的状态不是0
,则上面的内容将不会计算大小/长度。我不确定为什么在这里使用StateT Int IO
)
话虽如此,请注意,在Haskell中,当我们可以避免时,我们倾向于避免使用副作用(甚至像上面那样很好地包裹在monad中)。如果可能的话,通常最好使代码不受副作用的影响。
size :: [Int] -> Int
size = length
-- or
size = foldl' (\ s _ -> s+1) 0
如果您是初学者,也许修整monad和monad变压器不是最好的入门方法。我建议先学习基础知识(代数数据类型,模式匹配,递归,高阶函数等),然后再学习单子/函子/应用程序(例如,State Int
,而不是StateT Int IO
) ,最后移到变形金刚(StateT Int IO
)。