【问题标题】:transferring an imperative for-loop into idiomatic haskell将命令式 for 循环转换为惯用的 haskell
【发布时间】:2015-06-01 03:39:25
【问题描述】:

我在将命令式算法转换为函数式风格时遇到了一些困难。我无法理解的主要概念是如何根据序列在序列中的位置用值填充序列。在 Haskell 中,以下算法的惯用解决方案如何?

A = unsigned char[256]
idx <- 1
for(i = 0 to 255)
    if (some_condition(i))
        A[i] <- idx
        idx++
    else
        A[i] = 0;

该算法基本上为直方图的映射函数创建了一个查找表。

你知道任何可以帮助我更好地理解这类问题的资源吗?

【问题讨论】:

    标签: algorithm haskell functional-programming imperative


    【解决方案1】:

    函数式编程的核心思想之一是将算法表示为数据转换。在像 Haskell 这样的惰性语言中,我们甚至可以更进一步,将惰性数据结构视为具体计算。在非常真实的意义上,Haskell 的列表比普通的链表更像循环:它们可以增量计算,不必一次全部存在于内存中。同时,我们仍然获得了具有传递数据类型并通过模式匹配检查它的能力的许多优势。

    考虑到这一点,用索引表示 for 循环的“技巧”是创建一个包含它可以采用的所有值的列表。您的示例可能是最简单的情况:i 获取从 0255 的所有值,因此我们可以使用 Haskell 的内置表示法来表示范围:

    [0..255]
    

    在高层次上,这相当于 Haskell 的 for (i = 0 to 255);然后,我们可以通过递归函数或标准库中的高阶函数遍历这个列表来执行循环中的实际逻辑。 (第二个选项是首选。)

    这个特殊的逻辑非常适合fold。折叠让我们逐项接收列表并建立某种结果。在每一步,我们都会得到一个列表项和到目前为止我们构建的结果的值。在这种特殊情况下,我们希望在递增索引的同时从左到右处理列表,因此我们可以使用foldl;一个棘手的部分是它会向后生成列表。

    这是foldl的类型:

    foldl :: (b -> a -> b) -> b -> [a] -> b
    

    所以我们的函数接受我们的中间值和一个列表元素并产生一个更新的中间值。由于我们正在构建一个列表并跟踪一个索引,我们的中间值将是一个包含两者的对。然后,一旦我们得到最终结果,我们就可以忽略idx 值并反转我们得到的最终列表:

    a = let (result, _) = foldl step ([], 1) [0..255] in reverse result
      where step (a, idx) i
              | someCondition i = (idx:a, idx + 1)
              | otherwise       = (0:a, idx)
    

    事实上,在跟踪某些中间状态(在本例中为idx)的同时转换列表的模式非常普遍,因此它在State 类型方面具有自己的功能。核心抽象涉及更多内容(通读 ["You could Have Invented Monads"][you] 以获得很好的介绍),但生成的代码实际上读起来非常愉快(我猜除了导入之外:P) :

    import Control.Applicative
    import Control.Monad 
    import Control.Monad.State
    
    a = evalState (mapM step [0..255]) 1
      where step i
              | someCondition i = get <* modify (+ 1)
              | otherwise       = return 0
    

    我们的想法是我们映射[0..255],同时在后台跟踪一些状态(idx 的值)。 evalState 是我们将所有管道放在一起并获得最终结果的方式。 step 函数应用于每个输入列表元素,还可以访问或修改状态。

    step 函数的第一种情况很有趣。 &lt;* 操作符告诉它先做左边的事情,然后做右边的事情但返回左边的值。这让我们可以获取当前状态,增加它,但仍然返回我们在它增加之前获得的值。我们的状态概念是一流的实体,我们可以拥有像&lt;* 这样的库函数这一事实非常强大——我发现这个特殊的习惯用法对于遍历树非常有用,其他类似的习惯用法对于其他类似的习惯用法也非常有用代码。

    【讨论】:

    • 非常好的答案。到目前为止,我还不知道状态单子。非常感谢!
    【解决方案2】:

    根据您要使用的数据结构,有多种方法可以解决此问题。最简单的可能是列表和Prelude 中可用的基本功能:

    a = go 1 [] [0..255]
        where
            go idx out [] = out
            go idx out (i:is) =
                if condition i
                    then go (idx + 1) (out ++ [idx]) is
                    else go idx (out ++ [0]) is
    

    这使用带有两个累加器idxout 的worker 模式,它会向下遍历最后一个参数直到没有剩余元素,然后返回out。这当然可以转换为某种fold,但无论如何它不会非常有效,将项目附加到带有++ 的列表是非常低效的。您可以通过使用idx : out0 : out 使其更好,然后在go 的输出上使用reverse,但这仍然不是一个理想的解决方案。

    另一种解决方案可能是使用State monad:

    a = flip runState 1 $ forM [0..255] $ \i -> do
            idx <- get
            if condition i
                then do
                    put $ idx + 1    -- idx++
                    return idx       -- A[i] = idx
                else return 0
    

    这当然看起来更有必要。 flip runState 1 中的 1 表示您的初始状态是 idx = 1,然后您使用 forM(看起来像 for 循环但实际上不是)超过 [0..255],循环变量是 i ,然后只需要实现其余的逻辑即可。

    如果你想更高级,你可以使用StateTST monads 来同时拥有一个实际的可变数组和一个状态。但是,对其工作原理的解释远远超出了此答案的范围:

    import Control.Monad.State
    import Control.Monad.ST
    import qualified Data.Vector as V
    import qualified Data.Vector.Mutable as MV
    
    
    a :: V.Vector Int
    a = runST $ (V.freeze =<<) $ flip evalStateT (1 :: Int) $ do
        a' <- lift $ MV.new 256
        lift $ MV.set a' 0
        forM_ [0..255] $ \i -> do
            when (condition i) $ do
                idx <- get
                lift $ MV.write a' i idx
                put $ idx + 1
        return a'
    

    我稍微简化了一下,让每个元素从一开始就设置为0,我们从idx = 1的初始状态开始,循环[0..255],如果当前索引i满足条件则获取当前idx,将其写入当前索引,然后递增idx。将其作为有状态操作运行,然后冻结向量,最后运行 ST monad 方面。这允许一个实际的可变向量安全地隐藏在 ST monad 中,这样外界就不知道要计算 a 你必须做一些相当奇怪的事情。

    【讨论】:

      【解决方案3】:

      循环通常可以使用不同的fold 函数来表示。这是一个使用foldl的解决方案(如果遇到stackoverflow错误,您可以切换到foldl'):

      f :: (Num a) => (b -> Bool) -> a -> [b] -> [a]
      f pred startVal = reverse . fst . foldl step ([], startVal)
          where            
              step (xs, curVal) x 
                  | pred x = (curVal:xs, curVal + 1)
                  | otherwise = (0:xs, curVal)
      

      如何使用它?该函数采用谓词(代码中的someCondition)、索引的初始值和要迭代的元素列表。也就是说,您可以致电f someCondition 1 [0..255] 以从您的问题中获取示例的结果。

      【讨论】:

        【解决方案4】:

        显式递归:

        a = go 0 1
          where go 256 _   = []
                go i   idx | someCondition i = idx : go (i+1) (idx+1)
                           | otherwise       = 0   : go (i+1) idx
        

        展开:(上面显式递归的变体)

        a = unfoldr f (0,1)
            where f (256,_) = Nothing
                  f (i,idx) | someCondition i = Just (idx,(i+1,idx+1))
                            | otherwise       = Just (0  ,(i+1,idx  ))
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-10-13
          • 2015-12-03
          • 2021-03-12
          • 2018-02-22
          • 2017-04-26
          • 2018-08-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多