【发布时间】:2009-09-20 15:42:09
【问题描述】:
您如何用您最喜欢的语言模拟 Python 样式生成器?我在 Scheme 中找到了 this 之一。看到其他实现一定很有趣,尤其是在那些没有一流延续的语言中。
【问题讨论】:
标签: c++ python programming-languages lisp
您如何用您最喜欢的语言模拟 Python 样式生成器?我在 Scheme 中找到了 this 之一。看到其他实现一定很有趣,尤其是在那些没有一流延续的语言中。
【问题讨论】:
标签: c++ python programming-languages lisp
我根本不会在 Lisp/Scheme 中使用 yield。
'yield' 需要语言中的某种协同程序或延续设施。可以通过更简单的函数方式实现 yield 的许多用途。
YIELD 基本上与著名的 COME-FROM 运算符有关。 ;-) 在这里,某个地方的调用可能会导致某个其他例程中的不同地方,具体取决于其执行上下文。所以一个例程突然有多个入口点,它们的顺序是在运行时确定的。对于简单的用途,这可能很好,但我认为对于更复杂的代码,关于代码的推理会变得更加困难。
以问题中链接的 Scheme 示例为例:
(define/y (step)
(yield 1)
(yield 2)
(yield 3)
'finished)
(list (step) (step) (step))
多次调用(step)然后返回不同的值。
我只想创建一个闭包:
(define step
(let ((state '(1 2 3 finished)))
(lambda ()
(pop state))))
这将带有 yield 的函数分成两个不同的东西:一个携带状态的变量和一个改变状态的简单函数。状态不再隐式编码到执行序列中。
(list (step) (step) (step))))
可以想象类似的解决方案可以用于 yield 的其他用途。
与generators from the Common Lisp SERIES library比较:
(let ((x (generator (scan '(1 2 3 finished)))))
(list (next-in x)
(next-in x)
(next-in x)))
如果我们从另一个答案来看这个 Python 示例
def simpleRange(n):
for i in xrange(n):
yield i
for n in simpleRange(5):
print(n)
我们可以看到它复制了控制结构。调用位置和生成器都使用 FOR 迭代控制结构。使用闭包,我们可以摆脱对生成器内部控制结构的使用,只提供状态转换代码。
【讨论】:
下面是一个 C++ 示例,它使用光纤模拟生成器:
Yield Return Iterator for Native C++ Using Fibers
“收益回报”迭代器是 为创建的语言功能 原因之一:简单。这是 通常更容易迭代 跨越整个收藏,存储所有 局部变量中需要的上下文, 而不是制作一个复杂的, 存储其的自定义迭代器对象 跨越后续检索的状态 操作。
还有原始 C 例程setjmp, longjmp 来实现类似的结果。
(luacoroutines都是用上面的方法实现的)
【讨论】:
在 JavaScript 1.7+ 中,我通常只需要添加一些括号和方括号。其他一切都差不多。 JavaScript 1.7 引入了 pythonic 生成器和迭代器等。
# Python
(x + 1 for x in y if x > 100)
// JavaScript 1.8+
(x + 1 for (x in y) if (x > 100))
# Python
def simpleRange(n):
for i in xrange(n):
yield i
for n in simpleRange(5):
print(n)
// JavaScript 1.7+
function simpleRange(n) {
for (let i = 0; i < n; i++)
yield i;
}
for (n in simpleRange(5))
print(n);
# Python
[x + 1 for x in y if x > 100]
// JavaScript 1.7+
[x + 1 for (x in y) if (x > 100)]
【讨论】:
y 对象后,将其键入 Python shell。表示为<generator object <genexpr> at 0x7fa5b310ec30>。告诉我这不是生成器表达式。
C++,使用Generators
简单范围生成器的声明:
$generator(range)
{
int i;
int _max;
int _min;
range(int minv, int maxv):_max(maxv),_min(minv) {}
$emit(int) // will emit int values. Start of body of the generator.
for (i = _min; i <= _max; ++i)
$yield(i);
$stop;
};
它的用法:
range r10(1,10);
for(int n; r10(n);)
printf("%d\n",n);
会输出
1
2
...
10
【讨论】:
对于@dmitry_vk 关于 Common Lisp 的回答,我想补充一点,在 Lisp 中,实际上并不需要生成器。它们的用例完全被闭包、特殊变量和宏的不同应用所覆盖——没有学习新结构的额外概念开销。
有时甚至内置结构也可以工作。让我们看一下 Python wiki 中的示例:
# add squares less than 100
from itertools import count, takewhile
sum = 0
square = (i*i for i in count())
bounded_squares = takewhile(lambda x: x < 100, square)
for i in bounded_squares:
sum += i
使用loop 可以更直接地实现:
CL-USER> (loop :for i :from 0 :while (< i 100) :sum (expt i 2))
328350
由于loop 比Python 的for 更加通用,因此这里无需介绍特殊语法。
让我们考虑另一个用例 - 对一些自定义树进行迭代。假设树由指向其children 的nodes 表示。
(defstruct node
data
children)
我们可以用一个相当小而简单的宏遍历任何树/子树。
(defmacro dotree ((var root &optional result-form) &body body)
`(block nil
(labels ((traverse (node)
(let ((,var node))
,@body
(dolist (child (children node))
(traverse child))
,result-form)))
(when-it ,root
(traverse it)))))
(警告:为了更清楚起见,我没有在其中使用gensym,但你应该使用)。
这是它的用法示例 - 获取所有叶节点的列表:
(let (rez)
(dotree (node tree (reverse rez))
(when (leafp node)
(push node rez))))
这看起来和工作起来就像标准的dolist 宏。而且,与dolist 一样,您可以随时通过调用return 来停止迭代。
总的来说,我还没有看到无法在 Lisp 中以某种不太复杂的方式实现的生成器使用的实用示例。
附:您还可以查看SERIES Lisp 库,它实现了与 90 年代生成器类似的概念。或者 CLAZY - 从 2000 年代后期开始。
【讨论】:
Monads 可用于表示生成器(即使语义略有不同)。
因此,任何允许我们在特殊语法中定义一元操作的语言都可以在这里使用。
yield return)Ruby 可以通过其内置的延续功能模拟生成器。
【讨论】:
Common Lisp 虽然没有原生延续,但允许使用像 cl-cont 这样的 CPS 转换器创建定界延续。因此,Common Lisp 中的生成器可以用与 Scheme 生成器几乎相同的方式编写。
顺便说一下,基于延续的生成器有一个 Python 和 C# 生成器所缺乏的特性:yield 可以在生成器函数调用的动态范围内被调用。 Python 和 C# 生成器允许 yield 仅放置在生成器的主体内。
【讨论】:
鲁比:
生成器函数:
def simple_range(n)
Enumerator.new do |y|
(0..n).each { |v| y.yield(v) }
end
end
【讨论】:
一般yield 在具有一流功能的语言中是多余的。
例如,在TIScript 中,您可以通过这种方式生成生成器:
发电机。注意,它返回内部函数。
function range( from, to )
{
var idx = from - 1;
return function() { if( ++idx <= to ) return idx; } // yields value on call
}
及其用法:
for( var item in range(12,24) )
stdout << item << " ";
TIScript 中的for(elem in source) 与 JS 略有不同。如果 source 是一个函数,它将被调用并将其返回值分配给elem,直到该函数不会返回 void(空函数的默认返回值)。
【讨论】: