将命令式 for 循环转换为惯用语 haskell
transferring an imperative for-loop into idiomatic haskell
我在将命令式算法转换为函数式风格时遇到了一些困难。我无法理解的主要概念是如何根据序列中的位置用值填充序列。以下算法的惯用解决方案在 Haskell?
中看起来如何
A = unsigned char[256]
idx <- 1
for(i = 0 to 255)
if (some_condition(i))
A[i] <- idx
idx++
else
A[i] = 0;
该算法主要是为直方图的映射函数创建查找 table。
你知道有什么资源可以帮助我更好地理解这类问题吗?
循环通常可以用不同的fold
函数来表达。这是一个使用 foldl
的解决方案(如果您 运行 进入 Whosebug 错误,您可以切换到 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]
从您的问题中获取示例的结果。
有多种方法可以解决此问题,具体取决于您要使用的数据结构。最简单的可能是列表和 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
这里使用了带有两个累加器的worker模式,idx
和out
,它向下遍历最后一个参数,直到没有更多的元素剩下,然后returns out
.这当然可以转换成某种 fold
,但无论如何它都不会非常有效,将项目附加到带有 ++
的列表是非常低效的。你可以通过使用 idx : out
和 0 : 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
,接下来就是实现剩下的逻辑了
如果你想更高级,你可以使用 StateT
和 ST
monad 来同时拥有一个实际的可变数组和一个状态。但是,对其工作原理的解释远远超出了这个答案的范围:
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]
,如果当前indexi
满足条件则获取当前idx
,写入当前索引,然后递增idx
。 运行 这是一个有状态的操作,然后冻结向量,最后 运行 ST
monad 方面。这允许一个实际的可变向量安全地隐藏在 ST
monad 中,这样外界就不知道要计算 a
你必须做一些相当奇怪的事情。
显式递归:
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 ))
函数式编程的核心思想之一是将算法表示为数据转换。在像 Haskell 这样的惰性语言中,我们甚至可以更进一步,将惰性数据结构视为具体化的计算。在一个非常真实的意义上,Haskell 的列表比普通链表更像是循环:它们可以增量计算并且不必一次全部存在于内存中。同时,我们仍然可以获得像这种数据类型那样能够传递它并使用模式匹配检查它的许多优点。
考虑到这一点,"trick" 用于表达带有索引的 for 循环是创建它可以采用的所有值的列表。您的示例可能是最简单的情况:i
获取从 0
到 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
)的同时转换列表的模式非常普遍,因此它在 [=23] 方面具有自己的功能=] 类型。核心抽象有点复杂(通读 ["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
函数的第一个例子很有趣。 <*
运算符告诉它先做左边的事情,然后再做右边的事情 但是 return 左边的值 。这让我们得到当前状态,递增它,但仍然 return 我们得到的值 在 它递增之前。我们的状态概念是第一个 class 实体并且我们可以拥有像 <*
这样的库函数这一事实非常强大——我发现这个特殊的习惯用法对于遍历树和其他类似的习惯用法非常有用对其他代码非常有用。
我在将命令式算法转换为函数式风格时遇到了一些困难。我无法理解的主要概念是如何根据序列中的位置用值填充序列。以下算法的惯用解决方案在 Haskell?
中看起来如何A = unsigned char[256]
idx <- 1
for(i = 0 to 255)
if (some_condition(i))
A[i] <- idx
idx++
else
A[i] = 0;
该算法主要是为直方图的映射函数创建查找 table。
你知道有什么资源可以帮助我更好地理解这类问题吗?
循环通常可以用不同的fold
函数来表达。这是一个使用 foldl
的解决方案(如果您 运行 进入 Whosebug 错误,您可以切换到 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]
从您的问题中获取示例的结果。
有多种方法可以解决此问题,具体取决于您要使用的数据结构。最简单的可能是列表和 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
这里使用了带有两个累加器的worker模式,idx
和out
,它向下遍历最后一个参数,直到没有更多的元素剩下,然后returns out
.这当然可以转换成某种 fold
,但无论如何它都不会非常有效,将项目附加到带有 ++
的列表是非常低效的。你可以通过使用 idx : out
和 0 : 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
,接下来就是实现剩下的逻辑了
如果你想更高级,你可以使用 StateT
和 ST
monad 来同时拥有一个实际的可变数组和一个状态。但是,对其工作原理的解释远远超出了这个答案的范围:
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]
,如果当前indexi
满足条件则获取当前idx
,写入当前索引,然后递增idx
。 运行 这是一个有状态的操作,然后冻结向量,最后 运行 ST
monad 方面。这允许一个实际的可变向量安全地隐藏在 ST
monad 中,这样外界就不知道要计算 a
你必须做一些相当奇怪的事情。
显式递归:
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 ))
函数式编程的核心思想之一是将算法表示为数据转换。在像 Haskell 这样的惰性语言中,我们甚至可以更进一步,将惰性数据结构视为具体化的计算。在一个非常真实的意义上,Haskell 的列表比普通链表更像是循环:它们可以增量计算并且不必一次全部存在于内存中。同时,我们仍然可以获得像这种数据类型那样能够传递它并使用模式匹配检查它的许多优点。
考虑到这一点,"trick" 用于表达带有索引的 for 循环是创建它可以采用的所有值的列表。您的示例可能是最简单的情况:i
获取从 0
到 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
)的同时转换列表的模式非常普遍,因此它在 [=23] 方面具有自己的功能=] 类型。核心抽象有点复杂(通读 ["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
函数的第一个例子很有趣。 <*
运算符告诉它先做左边的事情,然后再做右边的事情 但是 return 左边的值 。这让我们得到当前状态,递增它,但仍然 return 我们得到的值 在 它递增之前。我们的状态概念是第一个 class 实体并且我们可以拥有像 <*
这样的库函数这一事实非常强大——我发现这个特殊的习惯用法对于遍历树和其他类似的习惯用法非常有用对其他代码非常有用。