将命令式for循环转换为惯用的haskell

时间:2015-03-27 17:45:57

标签: algorithm haskell functional-programming imperative

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

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

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

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

4 个答案:

答案 0 :(得分:8)

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

考虑到这一点,&#34;技巧&#34;表达带索引的for循环是为了创建它可以采用的所有值的列表。您的示例可能是最简单的情况:i0中的所有值都带到255,因此我们可以对范围使用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方面的自身功能。类型。核心抽象有点复杂(通过[&#34;你可以发明Monads&#34;] [你]进行了很好的介绍),但结果代码实际上非常令人愉快(除了导入,我想: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函数的第一个案例很有意思。 <*运算符告诉它首先执行左边的操作,右边的第二个执行操作,但返回左边的值。这让我们得到当前状态,增加它但仍然返回之前增加的值。事实上,我们的国家概念是一流的实体,我们可以拥有像<*这样的库函数非常强大 - 我发现这个特殊的习惯用法非常适合遍历树木,其他类似的习语也是如此对其他代码非常有用。

答案 1 :(得分:3)

根据您要使用的数据结构,有几种方法可以解决此问题。最简单的可能是列表和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一起使用,并遍历最后一个参数,直到不再剩余元素,然后返回out。这肯定可以转换为某种fold,但无论如何它都不会非常有效,将项目附加到++的列表效率非常低。您可以使用idx : out0 : out,然后在reverse的输出上使用go来改善它,但它仍然不是理想的解决方案。

另一种解决方案可能是使用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

这当然看起来更加迫切。 1中的flip runState 1表示您的初始状态为idx = 1,然后使用forM(看起来像for循环,但实际上不是){{1}循环变量是[0..255],然后它只是实现其余逻辑的问题。

如果你想要更高级,你可以使用iStateT monad来实现具有状态的实际可变数组。但是,对其工作方式的解释远远超出了这个答案的范围:

ST

我简化了一下,以便每个元素从一开始就设置为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,如果是当前索引{ {1}}符合条件,然后获取当前[0..255],将其写入当前索引,然后递增i。将其作为有状态操作运行,然后冻结向量,最后运行idx monad方面。这允许在idx monad中安全隐藏实际的可变向量,以便外界不知道计算ST你必须做一些相当奇怪的事情。

答案 2 :(得分:1)

显式递归:

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

答案 3 :(得分:0)

循环通常可以使用不同的fold函数表示。这是一个使用foldl的解决方案(如果遇到堆栈溢出错误,可以切换到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]以获取问题中示例的结果。