R中的惰性序列

17
在Clojure中,使用lazy序列构造器轻松创建无限序列。例如,
(def N (iterate inc 0))

返回一个数据对象N,它等价于无限序列

(0 1 2 3 ...)
评估值N会导致无限循环。评估(take 20 N)会返回前20个数字。由于序列是惰性的,只有在请求时才会迭代inc函数。由于Clojure是同构的,惰性序列被递归地存储。
在R中,是否可以做类似的事情?您能否提供一些生成数据对象N的R代码,该对象相当于自然数的完整无限序列?评估完整对象N应该会导致一个循环,但是像head(N)这样的东西应该只返回前面的数。
注:我真正感兴趣的是惰性序列,而不仅仅是自然数本身。
编辑:这里是lazy-seq的Clojure源代码:
(defmacro lazy-seq
"Takes a body of expressions that returns an ISeq or nil, and yields
a Seqable object that will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls. See also - realized?"
{:added "1.0"}
[& body]
(list 'new 'clojure.lang.LazySeq (list* '^{:once true} fn* [] body)))    

我正在寻找在R中具有相同功能的宏。


7
"输出所有自然数的无限序列"?你有无限时间吗?" - Roland
1
由于Clojure是同像的,所以懒惰序列被递归地存储并不是很有意义。同像性在这里并不相关,而“递归存储”也不是百分之百清楚的含义。 - amalloy
@amalloy: 数据(0 1 2 3 ...)和代码(iterate inc 0)作为值是等效的。所谓“递归存储”,指的是用递归公式(使用iterate)表示无限序列,可以应用该公式计算完整序列。 - Tom LaGatta
1
我认为你可能用一些无关的Clojure术语误导了R用户。通过一些模拟的例子,这可能会更清晰;斐波那契数列总是一个好例子。无论如何,我尝试着回答至少展示了你所问的内容。你可以询问R专家是否有更好的方法来完成我所做的事情。 - A. Webb
4个回答

14

替代实现

介绍

自从这篇文章发布以来,我在R中的工作频率有所增加,因此我提供了一种基于R基础的替代实现。同样地,我认为如果下降到C扩展级别,你可以获得更好的性能。原始答案在下面。

在基础R中的第一个挑战是缺乏真正的cons(暴露在R级别上)。R使用c进行混合cons/concat操作,但这并不创建链接列表,而是创建一个新的向量,其中包含两个参数的元素。特别地,必须知道两个参数的长度,这在惰性序列的情况下并不成立。此外,连续的c操作表现出二次性能,而不是常数时间。现在,您可以使用长度为两个的“列表”(实际上是向量,而不是链接列表)来模拟cons单元格,但是......

第二个挑战是数据结构中的promise强制。 R使用隐式promise使用一些惰性评估语义,但这些是二等公民。返回显式promise的函数delay已被弃用,而是使用隐式delayedAssign,仅执行其副作用--“未评估的promise永远不应可见。”函数参数是隐式promise,因此您可以使用它们,但是您无法将promise放入数据结构中而不强制执行它。

计算机科学101

事实证明,这两个挑战可以通过回想计算机科学101来解决。数据结构可以通过闭包实现。

cons <- function(h,t) function(x) if(x) h else t

first <- function(kons) kons(TRUE)
rest <- function(kons) kons(FALSE)  

由于R的惰性函数参数语义,我们的cons函数已经能够处理惰性序列了。
fibonacci <- function(a,b) cons(a, fibonacci(b, a+b))
fibs <- fibonacci(1,1)

为了能够有用,我们需要一套懒序列处理函数。在Clojure中,作为核心语言的序列处理函数也自然地与懒序列配合使用。另一方面,R的序列函数则不会立即兼容。许多函数依赖于预先知道(有限的)序列长度。让我们定义一些能够处理懒序列的函数。
filterz <- function(pred, s) {
  if(is.null(s)) return(NULL)
  f <- first(s)
  r <- rest(s)
  if(pred(f)) cons(f, filterz(pred, r)) else filterz(pred, r) }

take_whilez <- function(pred, s) {
   if(is.null(s) || !pred(first(s))) return(NULL)
   cons(first(s), take_whilez(pred, rest(s))) }

reduce <- function(f, init, s) {
  r <- init
  while(!is.null(s)) {
    r <- f(r, first(s))
    s <- rest(s) }
  return(r) }

让我们使用我们已经创建的内容来计算所有小于4百万的偶数斐波那契数之和(欧拉计划#2):
reduce(`+`, 0, filterz(function(x) x %% 2 == 0, take_whilez(function(x) x < 4e6, fibs)))
# [1] 4613732

原始回答

我对R语言很生疏,但是既然(1)我熟悉Clojure,(2)我认为你没有向R用户表达清楚你的观点,那么我将尝试根据我的Clojure惰性序列工作示例来进行草图。这仅用于示例目的,不以任何方式调整性能。如果不存在C扩展程序,则最好将其实现为C扩展程序。

惰性序列在thunk中具有生成剩余序列的计算。它不会立即调用。在请求每个元素(或一些元素)时,会调用下一个thunk以检索值。如果它继续,该thunk可能会创建另一个thunk来表示序列的尾部。神奇的是,(1)这些特殊的thunk实现了序列接口,并且可以透明地使用它们,(2)每个thunk只被调用一次——它的值被缓存——因此实现的部分是值序列。

首先是标准示例

自然数

numbers <- function(x) as.LazySeq(c(x, numbers(x+1)))
nums <- numbers(1)

take(10,nums) 
#=> [1]  1  2  3  4  5  6  7  8  9 10

#Slow, but does not overflow the stack (level stack consumption)
sum(take(100000,nums))
#=> [1] 5000050000

斐波那契数列
fibonacci <- function(a,b) { 
 as.LazySeq(c(a, fibonacci(b, a+b)))}

fibs <- fibonacci(1,1)

take(10, fibs)
#=> [1]  1  1  2  3  5  8 13 21 34 55

nth(fibs, 20)
#=> [1] 6765

紧随其后的天真的R实现

惰性序列类

is.LazySeq <- function(x) inherits(x, "LazySeq")

as.LazySeq <- function(s) {
  cache <- NULL
  value <- function() {
    if (is.null(cache)) {
      cache <<- force(s)
      while (is.LazySeq(cache)) cache <<- cache()}
    cache}
  structure(value, class="LazySeq")}

一些带有LazySeq实现的通用序列方法。
first <- function(x) UseMethod("first", x)
rest <- function(x) UseMethod("rest", x)

first.default <- function(s) s[1]

rest.default <- function(s) s[-1]

first.LazySeq <- function(s) s()[[1]]

rest.LazySeq <- function(s) s()[[-1]]

nth <- function(s, n) {
  while (n > 1) {
    n <- n - 1
    s <- rest(s) }
  first(s) }

#Note: Clojure's take is also lazy, this one is "eager"
take <- function(n, s) {
  r <- NULL
  while (n > 0) {
    n <- n - 1
    r <- c(r, first(s))
    s <- rest(s) }
  r}

这是一个很棒的答案,@A. Webb。它非常有效。谢谢! - Tom LaGatta
rlang 包使您能够创建 _quosures_,它们充当“一等承诺”——捕获表达式和评估上下文——但与承诺不同的是,它们在被引用时会重新评估。 - egnha

10

你可能可以使用iterators库来实现你想要的功能:

library(iterators)
i <- icount()
nextElem(i)
# 1
nextElem(i)
# 2

你可以无限调用nextElem


我想它最终会溢出。 - flodel
1
不按照第一个参数的文档说明:““count: 迭代器将触发的次数。如果未指定,它将永远计数。””。唯一的解释是魔法。 - Scott Ritchie
@ScottRitchie,我重写了我的问题以使意图更加清晰。感谢您对“迭代器”库的美好参考。在重写的问题中,您认为这个库可以用来构建一般的惰性序列吗? - Tom LaGatta
我认为@A.Webb的答案是你正在寻找的。 - Scott Ritchie

5

由于 R 最适合处理向量,因此我们经常希望迭代器返回一个向量,例如:

library(iterators)
ichunk <- function(n, ..., chunkSize) {
    it <- icount(n)              # FIXME: scale by chunkSize
    chunk <- seq_len(chunkSize)
    structure(list(nextElem=function() {
        (nextElem(it) - 1L) * chunkSize + chunk
    }), class=c("abstractiter", "iter"))
}

通常使用的块大小为数百万级别。至少这样可以加快永久性方法的速度。


-1
你没有说明你的目标,所以我要指出,在任何编程语言中,
j <- 1
while(TRUE) { x[j+1] <- x[j]+1  ; j<-j+1}

这将给你一个无限序列。你实际上想要用迭代器做什么?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接