【问题标题】:What are some compelling use cases of infinite data structures?无限数据结构有哪些引人注目的用例?
【发布时间】:2023-03-22 22:59:01
【问题描述】:

某些语言(Haskell、Clojure、Scheme 等)具有惰性求值。惰性求值的“卖点”之一是无限数据结构。这有什么了不起的?能够处理无限数据结构显然具有优势的案例有哪些?

【问题讨论】:

  • Scheme 默认不是惰性的。
  • @knivil:但它确实有(可选的)惰性评估,不是吗?
  • @knivil @FUZxxl Clojure 是同一个 iirc;默认情况下严格,可选惰性。任何可以实现 thunk 的语言也可以类似地考虑,但需要解决更多的语法问题。

标签: data-structures haskell clojure scheme lazy-sequences


【解决方案1】:

无限数据结构提供了(可计算的)实数的优雅表示。例如,像

这样的无限列表
[(0, 4), (1, 3.5), (3.1, 3.2), ...]

可以代表pi。借助内置的惰性,对这种表示进行操作变得容易。

【讨论】:

    【解决方案2】:

    我能想到的几个优点:

    • 更简洁的代码 - 有趣的是,生成无限序列的代码通常比对有界数据进行操作的代码更简单、更简洁。这是因为此类代码通常更接近底层数学定义。
    • 灵活性 - 如果您使用惰性无限结构编写数据,则无需提前决定数据需要多大,而无需担心。它会“正常工作”
    • 性能 - 如果您正确使用惰性,您可以通过仅在需要时计算数据值来使用它来提高性能 - 并且可能根本不需要。因此,您可以避免大量不必要的计算。
    • 无限过程的抽象 - 无限惰性序列作为事件流的抽象有一个有趣的用途,例如随着时间的推移从网络连接中读取输入。这可以创建一些非常优雅和有趣的方式来创建代码来处理事件流。例如请参阅 Clojure 中的 stream-utils 库。
    • 更好的算法 - 有些算法更容易用无限数据结构表达 - 想法是你懒惰地“拉入”解决方案中你需要的部分,同时留下无限算法的其余部分未评估。如果使用这种方法可以降低算法的时间复杂度(比如从 O(n^2)O(n log n)),那么这可能是一个巨大的胜利。

    【讨论】:

      【解决方案3】:

      嗯,上个月我有一个很好的用例。复制对象时,我需要一个生成器来生成唯一名称。这意味着,生成器采用原始名称X,并为副本生成一个新名称。它通过附加文本来做到这一点

      X - copy
      X - copy (2)
      X - copy (3)
      ...
      

      只要名称不在同一组内的一组对象中使用。使用“无限数据结构”(无限的字符串数组)而不是简单的循环有一个优点:如果名称已经在使用中,您可以将 name generate 部分与测试完全分开.所以我可以为不同类型的对象重用生成器函数,其中每个对象类型的使用测试略有不同。

      【讨论】:

        【解决方案4】:

        我打算评论@knivil 的计划。相反,我将把它作为另一个答案提出来。

        惰性数据结构并不是完成大多数任务的唯一方法。这可能会激怒 Python 爱好者。但我相信最好让程序员选择他们使用的技术。懒惰的技术是强大而优雅的。

        使用 Scheme 的 iota 提到了 Knivil。看看依赖懒惰编写完整的方法(使用所有 3 个参数)是多么容易:

        iota count begin step = let xs = begin:map (+step) xs
                                in take count xs
        
        -- or, alternately
        iota count begin step = take count $ map ((+begin).(*step)) [0..]
        

        我也可以滥用懒惰为非空列表写length

        len = fst . last . zip [1..]
        
        -- or even handling empty lists
        len = fst . last . zip [0..] . (undefined:)
        

        考虑一下 Prelude 中定义的强大而优雅的 iterate 函数。

        iterate f x = x : iterate f (f x)
        

        它创建了无限列表[x, f x, f (f x), f (f (f x)), ...]。我本可以将iota 写成iterate

        iota count begin step = take count $ iterate (+step) begin
        

        惰性方法是一种优雅的编程方式。这不是唯一的方法,习惯了 C 或 Java 的人肯定会喊“但我不需要 需要 懒惰,我可以只是 _”,他们是正确的。如果您的语言是图灵完备的,则可以完成。但懒惰也可以如此优雅。

        【讨论】:

        • 你展示了 Haskell 的许多特性以及惰性。但是优雅和简单主要是语法,而且非常主观。对我来说,您在 Hakell 中的示例非常好,但我认为这不是无限数据结构的重点。游戏树示例是一个更好的示例,所以我赞成。
        【解决方案5】:

        这里有两个例子,一大一小:

        Why Functional Programming Matters by John Hughes 有一个很好的例子,一个国际象棋游戏。国际象棋游戏的移动树实际上并不是无限的,但它足够大以至于它也可能是无限的(称之为“接近无限”)。在严格的语言中,您实际上不能将其视为一棵树,因为没有足够的空间来存储整棵树。但是在惰性语言中,您只需定义树,然后定义“nextMove”函数以尽可能地遍历它。惰性求值机制负责细节。

        这个小例子只是简单地将一个索引号与列表中的每个项目关联起来,这样 ["foo", "bar", "baz"] 就变成了 [(1,"foo"), (2,"bar" ), (3,"baz")]。在严格的语言中,您需要一个循环来跟踪最后一个索引并检查您是否在最后。在 Haskell 中,您只需说:

        zip [1..] items
        

        zip 的第一个参数是一个无限列表。您无需提前计算需要多长时间。

        【讨论】:

        • 你的小例子很糟糕。在 Scheme 中,您使用 (map cons (iota (length items)) items),其中 iota 来自 srfi-1。你不需要循环。
        • @knivil:你不需要循环,因为所有迭代算法都可以用递归重写,反之亦然。 map 可能被实现为循环或通过递归,并且可能会或可能不会击败手动版本和循环,但这没关系。关键是使用无限的索引列表进行压缩比所有这些选项都简单。
        • @knivil:还要注意,Scheme 版本会迭代列表两次;另一方面,Haskell 版本只遍历列表一次。
        • 也许它会遍历列表两次,也许不会。这取决于你的编译器。另一方面,懒惰也不是免费的。
        【解决方案6】:

        有规范的纯记忆策略:

        fib = (map fib' [0..] !!)
            where
            fib' 0 = 0
            fib' 1 = 1
            fib' n = fib (n-1) + fib (n-2)
        

        我们将fib' 函数映射到一个无限列表,以构造一个包含所有 fib 值的表。瞧!便宜,易于记忆。

        当然,这在参数中具有线性查找时间。您可以将其替换为无限特里以获取对数查找时间。参看。 data-inttrie.

        【讨论】:

        • 嗯,这是优雅的代码。但是(也许我不完全了解幕后发生的事情)这比仅将结果缓存在表中的动态编程方式更好吗?
        • @Anas it 将结果缓存到表中的动态编程方式。在这种情况下,表是一个无限列表。虽然通常 DP 涉及显式迭代表。使用惰性备忘录表可以让您像 DP 一样编写,但只评估在所需结果中实际使用的子问题。
        猜你喜欢
        • 2014-12-06
        • 1970-01-01
        • 2022-08-06
        • 2012-08-25
        • 1970-01-01
        • 2011-03-05
        • 1970-01-01
        • 2011-05-24
        • 2015-08-29
        相关资源
        最近更新 更多