在函数式编程中的一个核心思想是将算法表达为数据转换。在像Haskell这样的惰性语言中,我们甚至可以更进一步地将惰性数据结构视为具体化的计算。从非常实际的意义上讲,Haskell的列表更像循环而不是普通的链接列表:它们可以逐步计算,不必一次性存在于内存中。同时,我们仍然可以获得许多类似数据类型的优点,例如能够传递和使用模式匹配进行检查。
有了这个想法,“技巧”就是用索引来表示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
)的模式非常常见,因此它在State
类型方面具有自己的函数。核心抽象稍微复杂一些(请阅读[“你本来可以发明单子”][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
函数的第一个情况很有趣。 <*
运算符告诉它首先执行左侧的操作,然后执行右侧的操作,但返回左侧的值。这使我们可以获得当前状态,增加它,但仍然返回在递增之前获得的值。我们的状态概念是一个一等公民,我们可以拥有像<*
这样的库函数,这非常强大-我发现这种特定的习惯用法对于遍历树非常有用,其他类似的习惯用法对于其他代码也非常有用。