【问题标题】:Haskell: How to make side effects in pure functionsHaskell:如何在纯函数中产生副作用
【发布时间】:2018-11-23 13:08:52
【问题描述】:

我是一个 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呈现一些副作用。我怎么能这样做?谢谢。

【问题讨论】:

  • 你为什么需要那个fmap
  • @arrowd 为了遍历所有元素,其实我只是在测试StateTmonad的用法
  • 尝试查找traverse。文档说“将结构的每个元素映射到一个动作,从左到右评估这些动作,并收集结果”。

标签: haskell


【解决方案1】:

首先,在学习过程的这一点上,您可能不应该担心“副作用”。此外,您还试图混合两个 monad,StateIO,但您似乎也没有掌握。因此,您可能应该更轻松一些。

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

理论上,您可以将有状态函数f :: a -&gt; b 视为f :: (a,s) -&gt; (b,s) 类型的纯函数,其中s 表示您可以访问和更改的某些状态。以上内容不太适合 monad 框架,因为在 monad m 中,我们希望 a -&gt; m b 表示从 ab 的有效函数。但是很容易适应。类型(a,s) -&gt; (b,s)可以不柯里化得到a -&gt; s -&gt; (b,s),我们把m b取为s -&gt; (b,s),所以a -&gt; m b代表a -&gt; s -&gt; (b,s)

所以这就是 monad State s 所代表的。对于每个类型b,类型State s bs -&gt; (b,s),可以理解为“给我缺少的初始状态s,这样我就可以计算b 和最终状态s。还有一个有状态函数a -&gt; State s ba -&gt; s -&gt; (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。然后我们使用sequence_按顺序执行这些动作,其类型如下(专门用于列表和我们特定的monad)。

 sequence_ :: [m a] -> m ()
 sequence_ :: [State Int ()] -> State Int ()

得到的函数是

size'   :: [Int] -> State Int ()
size' xs = sequence_ (sizeAux' xs) 

同样可以通过它运行,

runState (size' list) 0

在这里再次注意初始状态0 是必不可少的。

在这一点上,这可能仍然有些复杂。您需要更好地理解 monad 类、do 表示法和 State monad 的特殊性。无论如何,这是您应该前进的方向,而不是将 State 与 IO 混为一谈。

【讨论】:

    【解决方案2】:

    对于来自命令式世界的程序员,我认为最熟悉的答案是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 转换器并不是最好的开始方式。我建议先学习基础知识(代数数据类型、模式匹配、递归、高阶函数……),然后学习 monads / functors / applicatives(例如State Int,而不是StateT Int IO),最后学习转移到变形金刚 (StateT Int IO)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-02-05
      • 2013-02-01
      • 1970-01-01
      • 2013-08-12
      • 2019-12-04
      • 1970-01-01
      • 2023-03-18
      • 2019-07-29
      相关资源
      最近更新 更多