Haskell-为什么我要使用无限数据结构?

Haskell-为什么我要使用无限数据结构?,haskell,functional-programming,lazy-evaluation,Haskell,Functional Programming,Lazy Evaluation,在Haskell中,可以这样定义无限列表: [1.. ] If发现了许多描述如何实现无限列表的文章,我理解了这是如何工作的。 然而,我想不出任何理由使用无限数据结构的概念 有人能给我举一个问题的例子吗?在Haskell中使用无限列表可以更容易地解决(或者可能只解决)这个问题。Haskell中列表的基本优点是它们是一个看起来像数据结构的控制结构。您可以编写对数据流进行增量操作的代码,但它看起来像是对列表的简单操作。这与其他需要使用显式增量结构的语言不同,如迭代器(Python的itertools

在Haskell中,可以这样定义无限列表:

[1.. ]
If发现了许多描述如何实现无限列表的文章,我理解了这是如何工作的。 然而,我想不出任何理由使用无限数据结构的概念


有人能给我举一个问题的例子吗?在Haskell中使用无限列表可以更容易地解决(或者可能只解决)这个问题。

Haskell中列表的基本优点是它们是一个看起来像数据结构的控制结构。您可以编写对数据流进行增量操作的代码,但它看起来像是对列表的简单操作。这与其他需要使用显式增量结构的语言不同,如迭代器(Python的
itertools
)、协同程序(C#
IEnumerable
)或范围(D)

例如,
sort
函数的编写方式可以使它在开始产生结果之前对尽可能少的元素进行排序。虽然对整个列表进行排序需要O(n log n)/线性时间,但
最小xs=head(sort xs)
只需要O(n)/线性时间,因为
head
只会检查列表的第一个构造函数,如
x:
,并将尾部保留为未计算的thunk,表示排序操作的剩余部分

这意味着性能是组合的:例如,如果在数据流上有一长串操作,比如
sum。地图(*2)。filter(<5)
,它看起来像是首先过滤所有元素,然后在它们上面映射一个函数,然后求和,在每一步生成一个完整的中间列表。但实际情况是,每个元素一次只处理一个:给定
[1,2,6]
,基本上按如下方式进行,所有步骤都以增量方式进行:

  • 总计=
    0
  • 1<5
    为真
  • 1*2==2
  • 总计=
    0+2
    =
    2
  • 2<5
    为真
  • 2*2==4
  • 总计=
    2+4
    =
    6
  • 6<5
    为假
  • 结果=
    6
这正是用命令式语言(伪代码)编写快速循环的方法:

total=0;
对于x-in-xs{
if(x<5){
总计=总计+x*2;
}
}
这意味着性能是组合的:由于惰性,此代码在处理列表期间具有恒定的内存使用率。在
map
filter
中没有什么特别的东西可以实现这一点:它们可以完全独立

例如,标准库中的
计算列表的逻辑and,例如
和[a,b,c]==a&&b&&c
,它简单地实现为折叠:
和=foldr(&&&)True
。当它到达输入中的
False
元素时,它停止求值,这仅仅是因为
&&
在正确的参数中是懒惰的。懒惰给你作文

关于这一切的伟大论文,请阅读约翰·休斯(John Hughes)的著名著作,其中对懒惰函数式编程(Haskell的前身Miranda)的优势进行了阐述,远远超过了我的能力。

  • 用索引注释列表临时使用无限索引列表:

    zip [0..] ['a','b','c','d'] = [(0,'a'), (1,'b'), (2,'c'), (3,'d')]
    
  • 在保持纯度的同时记忆函数(在这种情况下,此转换会导致指数级速度增加,因为记忆表是递归使用的):

  • 具有副作用的纯模拟程序(免费单子)

    潜在的非终止程序用无限IO结构表示;e、 g.
    forever(putChar'y')=putChar'y'(putChar'y'(putChar'y'…)

  • 尝试:如果定义的类型大致如下所示:

    data Trie a = Trie a (Trie a) (Trie a)
    
    它可以表示由自然索引的
    a
    s的无限集合。请注意,递归没有基本情况,因此每个
    Trie
    都是无限的。但是索引
    n
    处的元素可以在
    log(n)
    时间内访问。这意味着您可以这样做(使用库中的某些函数):

    这将构建一个高效的“反向查找表”,在列表中给定任何值都可以告诉您它出现在哪个索引处,并且它会缓存结果并在信息可用时立即流式处理:

    -- N.B. findIndices [0, 0,1, 0,1,2, 0,1,2,3, 0,1,2,3,4...]
    > table = findIndices (concat [ [0..n] | n <- [0..] ])
    > table `apply` 0
    [0,1,3,6,10,15,21,28,36,45,55,66,78,91,...
    
    --N.B.FindDices[0,0,1,0,1,2,0,1,2,3,0,1,2,3,4…]
    >table=findIndices(concat[[0..n]| n table`apply`0
    [0,1,3,6,10,15,21,28,36,45,55,66,78,91,...
    
    所有这些都来自一行无限的折叠


<>我确信有更多的例子,你可以做很多很酷的事情。

我确信有很多好的答案,但是为了让你开始,无限的数据结构通常允许更简单的定义。考虑<代码>枚举< /C> >,它将列表中的每个元素放进一个带有索引的元组。采用索引的elper函数,或者更简单地说是带有[0..]的
zipWith。由于zip的性质,编写
[0..]
[0..length list-1]更简单、更通用
。这只是意味着您不必通过数据生成函数传递停止条件,但最终可以应用它。一个经典的例子是
以50斐波那契
为例,其中
斐波那契
是根据自身定义的。首先,让我们省略“数学上无限的事物”之间的区别,例如整数和“就我的程序而言,实际上是无限的“就像用户生成的I/O事件流一样。不难看出通用接口在处理这两个事件方面的优势。您可能不会遇到太多的第一个问题,但后一个问题非常常见。根据我的经验,无限数据结构的使用频率不高。有时它们是c
data IO a = Return a
          | GetChar (Char -> IO a)
          | PutChar Char (IO a)
data Trie a = Trie a (Trie a) (Trie a)
findIndices :: [Integer] -> Trie [Integer]
findIndices = foldr (\(i,x) -> modify x (i:)) (pure []) . zip [0..]
-- N.B. findIndices [0, 0,1, 0,1,2, 0,1,2,3, 0,1,2,3,4...]
> table = findIndices (concat [ [0..n] | n <- [0..] ])
> table `apply` 0
[0,1,3,6,10,15,21,28,36,45,55,66,78,91,...