Haskell:如何在纯函数中产生副作用

时间:2018-11-23 13:08:52

标签: haskell

我是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会产生一些副作用。我该怎么办?谢谢。

2 个答案:

答案 0 :(得分:5)

首先,在学习过程中的这一点上,您可能不应该担心“副作用”。另外,您正在尝试混用两个StateIO两个monad,以至于您似乎都没有掌握任何一个。因此,您可能应该更轻松一些。

可以使用IOIORefs monad中执行状态操作,您可以将其视为可变变量。如果我是你,我还不会去那里。然后是State monad,粗略地说,它是在纯环境中模拟有状态函数的便捷方法。

从理论上讲,您可以将状态函数f :: a -> b视为类型为f :: (a,s) -> (b,s)的纯函数,其中s表示可以访问和更改的某些状态。上面的代码不太适合monad框架,因为在monad m中,我们希望a -> m b表示从ab的有效函数。但是很容易适应。可以不使用类型(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 bs -> (b,s),可以理解为“给我丢失的初始状态s,这样我就可以计算出b有状态函数sa -> 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)

对于来自命令式世界的程序员,我认为最熟悉的答案是forfor_。示例:

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)。