由本人初译,欢迎(comment)大刀,千万别手下留情。 原文地址:http://practicingruby.com/articles/shared/ggcwduoyfqmz
初遇Ruby,Enumerable module提供的杀手级功能给我留下了最深刻的印象。我最终也爱上了Enumerator,即使我花了很长一段时间来弄清楚它是什么,什么人可使用它。
When I first came to Ruby, one of the things that impressed me the most was the killer features provided by the Enumerable module. I eventually also came to love Enumerator, even though it took me a long time to figure out what it was and what one might use it for.
作为一个初学者,我一直认为这些功能是来自Ruby解析器内部,通过一些黑暗魔法形式工作。有很多学习只是为了提高生产力,我推迟了学习引擎盖下是怎么回事的细节。一段时间后,我为这个决定感到后悔,谢谢David A.Black.
As a beginner, I had always assumed that these features worked through some dark form of magic that was buried deep within the Ruby interpreter. With so much left to learn just in order to be productive, I was content to postpone learning the details about what was going on under the hood. After some time, I came to regret that decision, thanks to David A. Black.
David 教Ruby向无经验的初学者们不久展示给他们Enumerable能做什么,同时也使他们实现他们自己的版本!这是一个很好的深刻锻炼,因为他揭开了神奇的面纱:如果你不懂yield, 你能创建所有的方便在Enumerable里。同样,如果你用Ruby的Fiber构造体,Enumerator有趣的功能能够很容易的实施。
David teaches Ruby to raw beginners not only by showing them what Enumerable can do, but also by making them implement their own version of it! This is a profoundly good exercise, because it exposes how nonmagical the features are: if you understand yield, you can build all the methods in Enumerable. Similarly, the interesting features of Enumerator can be implemented fairly easily if you use Ruby's Fiber construct.
在本文中,我们将通过滚动练习你自由的由Enumerable和Enumerator提供的功能子集,讨论沿途的每个工作细节。不管你的技术水平,了解这些构造优雅的设计,无疑将给你在你的新的程序构造设计过程中带来灵感。
In this article, we're going to work through the exercise of rolling your own subset of the functionality provided by Enumerable and Enumerator, discussing each detail along the way. Regardless of your skill level, an understanding of the elegant design of these constructs will undoubtedly give you a great source of inspiration that you can draw from when designing new constructs in your own programs.
Setting the stage with some tests(设置一些测试阶段)
我选择了一个小的Enumerable和Enumerator功能,但具有代表性子集,并写了一些测试,以便明确自己的行为。这些测试将指引我实现本文中全文的其余部分和如果你想尝试你自己练习将为你提供导向服务。
I've selected a small but representative subset of the features that Enumerable and Enumerator provide and written some tests to nail down their behavior. These tests will guide my implementations throughout the rest of this article and serve as a roadmap for you if you'd like to try out the exercise on your own.
如果你有一段时间这样做,尝试至少得到一些成功测试在阅读我的实现代码和解析之前,这方法你将学到更多。但如果你不计划这样做,至少认真阅读这些测试和仔细想想你怎么去实现他们描述的功能。
If you have some time to do so, try to get at least some of the tests to go green before reading my implementation code and explanations, as you'll learn a lot more that way. But if you're not planning on doing that, at least read through the tests carefully and think about how you might go about implementing the features they describe.
Toggle line numbers
1 class SortedList
2 include FakeEnumerable
3
4 def initialize
5 @data = []
6 end
7
8 def <<(new_element)
9 @data << new_element
10 @data.sort!
11
12 self
13 end
14
15 def each
16 if block_given?
17 @data.each { |e| yield(e) }
18 else
19 FakeEnumerator.new(self, :each)
20 end
21 end
22 end
23
24 require "minitest/autorun"
25
26 describe "FakeEnumerable" do
27 before do
28 @list = SortedList.new
29
30 # will get stored interally as 3,4,7,13,42
31 @list << 3 << 13 << 42 << 4 << 7
32 end
33
34 it "supports map" do
35 @list.map { |x| x + 1 }.must_equal([4,5,8,14,43])
36 end
37
38 it "supports sort_by" do
39 # ascii sort order
40 @list.sort_by { |x| x.to_s }.must_equal([13, 3, 4, 42, 7])
41 end
42
43 it "supports select" do
44 @list.select { |x| x.even? }.must_equal([4,42])
45 end
46
47 it "supports reduce" do
48 @list.reduce(:+).must_equal(69)
49 @list.reduce { |s,e| s + e }.must_equal(69)
50 @list.reduce(-10) { |s,e| s + e }.must_equal(59)
51 end
52 end
53
54 describe "FakeEnumerator" do
55 before do
56 @list = SortedList.new
57
58 @list << 3 << 13 << 42 << 4 << 7
59 end
60
61 it "supports next" do
62 enum = @list.each
63
64 enum.next.must_equal(3)
65 enum.next.must_equal(4)
66 enum.next.must_equal(7)
67 enum.next.must_equal(13)
68 enum.next.must_equal(42)
69
70 assert_raises(StopIteration) { enum.next }
71 end
72
73 it "supports rewind" do
74 enum = @list.each
75
76 4.times { enum.next }
77 enum.rewind
78
79 2.times { enum.next }
80 enum.next.must_equal(7)
81 end
82
83 it "supports with_index" do
84 enum = @list.map
85 expected = ["0. 3", "1. 4", "2. 7", "3. 13", "4. 42"]
86
87 enum.with_index { |e,i| "#{i}. #{e}" }.must_equal(expected)
88 end
89 end
如果你决定尝试自己来实现这些功能,你尽可能的靠近Ruby提供的行为,不过不用担心,如果你的实现与Ruby的什么真正用途不同。试想,这如果是一个新的问题需要解决,那么让测试引导你的实现。一旦你这样做了,继续读看我怎么做。
If you do decide to try implementing these features yourself, get as close to the behavior that Ruby provides as you can, but don't worry if your implementation is different from what Ruby really uses. Just think of this as if it's a new problem that needs solving, and let the tests guide your implementation. Once you've done that, read on to see how I did it.
Implementing the FakeEnumerable module(实现FakeEnumerable module)
在我开始实现FakeEnumerable工作之前,我需要得到它的测试故障状态,而不是错误状态。下面的代码完全相同:
Before I began work on implementing FakeEnumerable, I needed to get its tests to a failure state rather than an error state. The following code does exactly that:
Toggle line numbers
1 module FakeEnumerable
2 def map
3 end
4
5 def select
6 end
7
8 def sort_by
9 end
10
11 def reduce(*args)
12 end
13 end
这时我开始一个一个方法实现工作,跟着地图开始。关键是要认识到用Enumerable工作时,每一个功能,将以某种方式建立在每个方法上,使用它产生的结果与产量结合。地图功能可能是最简单平凡组合这些操作,在这里的实施你可以看到。
I then began working on implementing the methods one by one, starting with map. The key thing to realize while working with Enumerable is that every feature will build on top of the each method in some way, using it in combination with yield to produce its results. The map feature is possibly the most straightforward nontrivial combination of these operations, as you can see in this implementation:
Toggle line numbers
1 def map
2 out = []
3
4 each { |e| out << yield(e) }
5
6 out
7 end
这里我们可以看到,map是一个简单的函数,建立一个新数组将每个元素替换为您提供的块返回值。我们能清洁一下,使用一个内嵌的Ojbect#tap,但我不知道我是否喜欢这种做法,因为它有点打破简单的实现。这就是说,我在这里包括它和将在本文整个的其余部分使用它是为你考虑,只是为了简洁起见。
Here we see that map is simply a function that builds up a new array by taking each element and replacing it with the return value of the block you provide to it. We can clean this up to make it a one liner using Object#tap, but I'm not sure if I like that approach because it breaks the simplicity of the implementation a bit. That said, I've included it here for your consideration and will use it throughout the rest of this article, just for the sake of brevity.
Toggle line numbers
1 def map
2 [].tap { |out| each { |e| out << yield(e) } }
3 end
实现select也非常简单。它使用实现map相同的原理构建但增加时有条件检测,判断块返回的值是否为真。对于每一个新产生的元素,如果通过块返回值是逻辑真,那么这个元素将增加到新数组里面,否则,不增加。
Implementing select is quite easy as well. It builds on the same concepts used to implement map but adds a conditional check to see whether the block returns a true value. For each new yielded element, if the value returned by the block is logically true, the element gets added to the newly built array; otherwise, it does not.
Toggle line numbers
1 def select
2 [].tap { |out| each { |e| out << e if yield(e) } }
3 end
实现sort_by要棘手一点。我被骗看API文档,那(也许令人惊讶)描述这个方法怎么实现和甚至可以用Ruby参考实现。显然,sort_by使用一个Schwartzian变换转换我们遍历到包含了排序键和原始元素的元组的集合。然后它使用Array#sort将它们排序,并最终使用结果数组map从原来的集合元素的数组转换成元组阵列。这绝对是更加混淆的解释超过它的代码实施,所以为清晰只需看看下面的代码。
Implementing sort_by is a little more tricky. I cheated and looked at the API documentation, which (perhaps surprisingly) describes how the method is implemented and even gives a reference implementation in Ruby. Apparently, sort_by uses a Schwartzian transform to convert the collection we are iterating over into tuples containing the sort key and the original element. It then uses Array#sort to put these in order, and it finally uses map on the resulting array to convert the array of tuples back into an array of the elements from the original collection. That's definitely more confusing to explain than it is to implement in code, so just look at the following code for clarification:
Toggle line numbers
1 def sort_by
2 map { |a| [yield(a), a] }.sort.map { |a| a[1] }
3 end
有趣的是关于这个实现是依赖于map之上,当前集合的迭代以及它产生的数组。但后来跟踪它到核心,这个方法是仍然除集合实现只有each方法。此外,因为Array#sort是扔进组合,你的排序键需要响应<=>。但对于一个如此强大的方法,收缩性仍然很窄。
The interesting thing about this implementation is that sort_by is dependent on map, both on the current collection being iterated over as well as on the Array it generates. But after tracing it down to the core, this method is still expecting the collection to implement only the each method. Additionally, because Array#sort is thrown into the mix, your sort keys need to respond to <=>. But for such a powerful method, the contract is still very narrow.
实现reduce要复杂一点,因为它有三种不同的方式与它进行交互。这也是有趣的,因为它不一定是返回一个数组对象的几个Enumerable的方法之一。我会让你思考以下实施一个点,提供更多的评论之前,因为通过阅读它应该是一个很好的锻炼。
Implementing reduce is a bit more involved because it has three different ways of interacting with it. It's also interesting because it's one of the few Enumerable methods that isn't necessarily designed to return an Array object. I'll let you ponder the following implementation a bit before providing more commentary, because reading through it should be a good exercise.
Toggle line numbers
1 def reduce(operation_or_value=nil)
2 case operation_or_value
3 when Symbol
4 # convert things like reduce(:+) into reduce { |s,e| s + e }
5 return reduce { |s,e| s.send(operation_or_value, e) }
6 when nil
7 acc = nil
8 else
9 acc = operation_or_value
10 end
11
12 each do |a|
13 if acc.nil?
14 acc = a
15 else
16 acc = yield(acc, a)
17 end
18 end
19
20 return acc
21 end
首先,我不得不说我不是特别满意我的实现;似乎有点太蛮力,我想我可能会丢失一些明显的重构。但它应该是可读的,足以让您感觉到是怎么回事。第一段代码是简单地处理三个不同情况下的reduce()。我们每个呼叫开始产生实际操作。
First, I have to say I'm not particularly happy with my implementation; it seems a bit too brute force and I think I might be missing some obvious refactorings. But it should have been readable enough for you to get a general feel for what's going on. The first paragraph of code is simply handling the three different cases of reduce(). The real operation happens starting with our each call.
没有预定义一个初始值,我们设初始值为集合的第一个元素,且我们第一个yield从第二个元素开始产生。否则,初始值和第一个元素yielded。reduce()的目的是执行一个操作使用一些方法将列表中的每个连续值与最后计算值相结合。通过这种方式,列表就会减少到最终的单个值。这个行为解释了这个方法旧别名在Ruby里面是被称作inject:被称作injected的一个函数,在集合中的每个元素之间通过我们的yield调用。我发现这个操作更容易理解,当我能够看到它的原始概念例如yield和each,因为它使我们能够跟踪到底是怎么回事。
Without a predefined initial value, we set the initial value to the first element in the collection, and our first yield occurs starting with the second element. Otherwise, the initial value and first element are yielded. The purpose of reduce() is to perform an operation on each successive value in a list by combining it in some way with the last calculated value. In this way, the list gets reduced to a single value in the end. This behavior explains why the old alias for this method in Ruby was called inject: a function is being injected between each element in the collection via our yield call. I find this operation much easier to understand when I'm able to see it in terms of primitive concepts such as yield and each because it makes it possible to trace exactly what is going on.
如果你接下来实现reduce()遇到了麻烦,别担心。这肯定是更复杂的枚举方法之一,如果你尝试去实现一些另外的然后返回来学习reduce(),你可能会更幸运。但最美妙的东西是如果你忽略了reduce(:+)语法糖,它引入了新概念除此之外,map()是用什么来实现的。如果认为你理解了map()但不理解reduce(),那意味着你可能需要复习你的基础知识,例如:yield是如何工作的。
If you are having trouble following the implementation of reduce(), don't worry about it. It's definitely one of the more complex Enumerable methods, and if you try to implement a few of the others and then return to studying reduce(), you may have better luck. But the beautiful thing is that if you ignore the reduce(:+) syntactic sugar, it introduces no new concepts beyond that what is used to implement map(). If you think you understand map() but not reduce(), it's a sign that you may need to brush up on your fundamentals, such as how yield works.
如果你一路走来,你的所有FakeEnumerable测试应该都通过了。那意味着是时间开始我们的FakeEnumerator了。
If you've been following along at home, you should at this point be passing all your FakeEnumerable tests. That means it's time to get started on our FakeEnumerator.
Implementing the FakeEnumerator class(实现FakeEnumerator类)
与前面类似,我需要写一些故障状态的测试代码。首先,设立FakeEnumerator类的架构。
Similar to before, I needed to write some code to get my tests to a failure state. First, I set up the skeleton of the FakeEnumerator class.
Toggle line numbers
1 class FakeEnumerator
2 def next
3 end
4
5 def with_index
6 end
7
8 def rewind
9 end
10 end
这时我意识到我需要返回去修改FakeEnumerable#map方法,当没有提供block的时候我的测试依赖于它返回的一个FakeEnumerator对象,在这种情况下类似的方式方法Enumerable#map应该返回一个Enumerator。
Then I realized that I needed to back and at least modify the FakeEnumerable#map method, as my tests rely on it returning a FakeEnumerator object when a block is not provided, in a similar manner to the way Enumerable#map would return an Enumerator in that scenario.
Toggle line numbers
1 module FakeEnumerable
2 def map
3 if block_given?
4 [].tap { |out| each { |e| out << yield(e) } }
5 else
6 FakeEnumerator.new(self, :map)
7 end
8 end
9 end
虽然,从技术层面上讲,我也应该更新我所有的FakeEnumerable方法,那不是非常重要,因为我们的测试没有覆盖它并且没有新的概念需要介绍讨论。伴随map的这个修改,我所有的测试都是故障并非错误,那意味着是时候开始代码的实现工作了。
Although, technically speaking, I should have also updated all my other FakeEnumerable methods, it's not important to do so because our tests don't cover it and that change introduces no new concepts to discuss. With this change to map, my tests all failed rather than erroring out, which meant it was time to start working on the implementation code.
但在我们开始前,值得重构Enumerator的核心目的,我还没有谈到yet。在它的核心,一个Enumerator是一个简单的代理对象它混合在Enumerable里面,进而代表其每个方法由它包装的对象提供给一些其它的迭代器。这个行为将一个内部迭代器转换为一个外部的,它允许你做为一个对象来操作。
But before we get started, it's worth reflecting on the core purpose of an Enumerator, which I haven't talked about yet. At its core, an Enumerator is simply a proxy object that mixes in Enumerable and then delegates its each method to some other iterator provided by the object it wraps. This behavior turns an internal iterator into an external one, which allows you to pass it around and manipulate it as an object.
我们的测试要求我们实现next, rewind, 和 each_index,但之前我们能做些有意义的,我们需要使FakeEnumerator成为FakeEnumerable可用的代理对象。关于这点没有测试因为,对于那些想在家里尝试练习的人,我不想透露太多提示,但以下代码会做这个手法。
Our tests call for us to implement next, rewind, and each_index, but before we can do that meaningfully, we need to make FakeEnumerator into a FakeEnumerable-enabled proxy object. There are no tests for this because I didn't want to reveal too many hints to those who wanted to try this exercise at home, but this code will do the trick:
Toggle line numbers
1 class FakeEnumerator
2 include FakeEnumerable
3
4 def initialize(target, iter)
5 @target = target
6 @iter = iter
7 end
8
9 def each(&block)
10 @target.send(@iter, &block)
11 end
12
13 # other methods go here...
14 end
这里我们可以看到在目标对象上每个使用send来调用原来的iterator方法。除此之外,我们在实现其它集合看到这是普通模式。接下来是实现我们的next方法,那有点 棘手。
Here we see that each uses send to call the original iterator method on the target object. Other than that, this is the ordinary pattern we've seen in implementing other collections. The next step is to implement our next method, which is a bit tricky.
我们需要能够做到的是遍历一次,然后暂时返回一个值。然后,当next再次被调用,我们需要能够做到提前一次迭代并重复这个过程。我们能做些像运行整个迭代和缓存结果到一个数组里面。这个问题使我认识到Ruby的Fiber构造可能非常适合,因为它特别允许你跳入或跳出需要的代码块。所以我决定尝试,看我能做到怎样的程度。后来经过摸索,我得到了如下通过测试的代码:
What we need to be able to do is iterate once, then pause and return a value. Then, when next is called again, we need to be able to advance one more iteration and repeat the process. We could do something like run the whole iteration and cache the results into an array, then do some sort of indexing operation, but that's both inefficient and impractical for certain applications. This problem made me realize that Ruby's Fiber construct might be a good fit because it specifically allows you to jump in and out of a chunk of code on demand. So I decided to try that out and see how far I could get. After some fumbling around, I got the following code to pass the test:
Toggle line numbers
1 # loading the fiber stdlib gives us some extra features, including Fiber#alive?
2 require "fiber"
3
4 class FakeEnumerator
5 def next
6 @fiber ||= Fiber.new do
7 each { |e| Fiber.yield(e) }
8
9 raise StopIteration
10 end
11
12 if @fiber.alive?
13 @fiber.resume
14 else
15 raise StopIteration
16 end
17 end
18 end
这段代码很难读,因为它不是真的线性流程,但我将做用我非常有限的知识尽量解释Fiber结构是如何工作的。当你使用块调用Fiber#new时,在块代中的代码并没有立即执行。反而,当Fiber#resume被调用里开始执行。每次遇到一个Fiber#yield调用的时候,控制返回到Fiber#resume调用者,这个值伴随Fiber#yield返回。每个随后Fiber#resume拿起执行返回点,那个最后调用Fiber#yield处理,并非在代码块的开始。这个过程一直到只有留下一个Fiber@yield调用,这时Fiber#resume结果在一个FiberError里面,因为那儿没有任何执行。
This code is hard to read because it isn't really a linear flow, but I'll do my best to explain it using my very limited knowledge of how the Fiber construct works. Basically, when you call Fiber#new with a block, the code in that block isn't executed immediately. Instead, execution begins when Fiber#resume is called. Each time a Fiber#yield call is encountered, control is returned to the caller of Fiber#resume with the value that was passed to Fiber#yield returned. Each subsequent Fiber#resume picks up execution back at the point where the last Fiber#yield call was made, rather than at the beginning of the code block. This process continues until no more Fiber#yield calls remain, and then the last executed line of code is returned as the final value of Fiber#resume. Any additional attempts to call Fiber#resume result in a FiberError because there is nothing left to execute.
如果你重读前段几次和我后面定义的方法比较,它应该开始应得有意义。但是如果它融化进你的大脑,查证Fiber documentation,这是合理有益的。
If you reread the previous paragraph a couple of times and compare it to the definition of my next method, it should start to make sense. But if it's causing your brain to melt, check out the Fiber documentation, which is reasonably helpful.
这个整个很短的故事,在我们下一个定义中使用一个Fiber,让我们只保持在如何在每个迭代跟踪更远,且跳回迭代器里面的需求来得到下个值。我防止调用resume之前FiberError从不断通过检查来判断Fiber对象是否仍然活着。但我也需要使其不至于在最后Fiber抛出StopIteration错误时执行这部分,为了防止它从每个返回结果,它应该能集合它自身。这是一个kludge,如果你有更好的主意来控制这种情况,请给我留言。
The very short story about this whole thing is that using a Fiber in our next definition lets us keep track of just how far into the each iteration we are and jump back into the iterator on demand to get the next value. I prevent the FiberError from ever occurring by checking to see whether the Fiber object is still alive before calling resume. But I also need to make it so that the final executed statement within the Fiber raises a StopIteration error as well, to prevent it from returning the result of each, which would be the collection itself. This is a kludge, and if you have a better idea for how to handle this case, please leave me a comment.
这儿使用了Fiber对象来实现next,使它能够无限迭代的工作,例如:Enumerabler#cycle。虽然我们没有进入实现的细节,下面的代码应该能够做为为什么这是一个有用功能的提示。
The use of Fiber objects to implement next makes it possible to work with infinite iterators, such as Enumerable#cycle. Though we won't get into implementation details, the following code should give some hints as to why this is a useful feature:
Toggle line numbers
1 >> row_colors = [:red, :green].cycle
2 => #<Enumerator: [:red, :green]:cycle>
3 >> row_colors.next
4 => :red
5 >> row_colors.next
6 => :green
7 >> row_colors.next
8 => :red
9 >> row_colors.next
10 => :green
这是很酷,它尽可能多的让我想深入实施它,我想你现在开始感觉累了。哎呀,我准备休息一下,然后再开始写这遍文章!因此,让我们快点完成实现rewind和each_index以至于我们能包装这些东西。
As cool as that is, and as much as it makes me want to dig into implementing it, I have to imagine that you're getting tired by now. Heck, I've already slept twice since I started writing this article! So let's hurry up and finish implementing rewind and each_index so that we can wrap things up.
我发现一个实现rewind的方法,但关于它的一些东西让我惊呀,如果在哪儿我有一个独立的Fiber对象和是否有怪异的垃圾收集机制。但是然而,因为我实现next依赖于Fiber对象的缓存上保持跟踪在迭代里面它是在哪儿,这最容易的方法是让rewind回到最开始的状态,就是简单的擦除对象。下面的代码通过rewind测试:
I found a way to implement rewind that is trivial, but something about it makes me wonder if I've orphaned a Fiber object somewhere and whether that has weird garbage collection mplications. But nonetheless, because our implementation of next depends on the caching of a Fiber object to keep track of where it is in its iteration, the easiest way to rewind back to the beginning state is to simply wipe out that object. The following code gets my rewind tests passing:
Toggle line numbers
1 def rewind
2 @fiber = nil
3 end
现在只有一个功能,阻碍着我们和我们的工作完成:with_index。在Ruby里面真正real_index比你将看到的更灵巧,但其最简单的功能,如下的代码做法:
Now only one feature stands between us and the completion of our exercise: with_index. The real with_index method in Ruby is much smarter than what you're about to see, but for its most simple functionality, the following code will do the trick:
Toggle line numbers
1 def with_index
2 i = 0
3 each do |e|
4 out = yield(e, i)
5 i += 1
6 out
7 end
8 end
在这里,我没有做蛮力的事情,并保持自己的计数器。这样,我做了一个小的修改以控制流,不至于在每次迭代只产生元素,这个元素和它的index都产生。请记住,每个调用这里是一个代理在其它集合上的一些另外的迭代,这是让我们能够调用@list.map和获得map的行为并非所有行为。虽然你不会每天使用,知道如何用yield实现一个范围过滤器是非常有用的。
Here, I did the brute force thing and maintained my own counter. I then made a small modification to control flow so that rather than yielding just the element on each iteration, both the element and its index are yielded. Keep in mind that the each call here is a proxy to some other iterator on another collection, which is what gives us the ability to call @list.map.with_index and get map behavior rather than each behavior. Although you won't use every day, knowing how to implement an around filter using yield can be quite useful.
随着代码的编写,我的完整的测试终于都绿色了。尽管,我做这些练习前十几次,当我写这篇文章里我仍然在学习一个或两个事物,并且我想还有什么值得我学习的。你怎么样?
With this code written, my full test suite finally went green. Even though I'd done these exercises a dozen times before, I still learned a thing or two while writing this article, and I imagine there is still plenty left for me to learn as well. How about you?
Reflections(反射)
这肯定是我最喜欢的更好的理解ruby的练习。我通常不会进行大的演练,但那儿是一些关于脱回神奇的功能,那表面上看起来真的复杂,那让我很满意。我觉得,如果我的方案是非常假冒廉价,Ruby必须真正做什么,它仍然大大有助于我实现这些功能,因为它给我一个我自己构建的心理模型,从中看到功能。
This is definitely one of my favorite exercises for getting to understand Ruby better. I'm not usually big on contrived practice drills, but there is something about peeling back the magic on features that look really complex on the surface that gives me a great deal of satisfaction. I find that even if my solutions are very much cheap counterfeits of what Ruby must really be doing, it still helps tremendously to have implemented these features in any way I know how, because it gives me a mental model of my own construction from which to view the features.
如果你投入了这个练习,有些事情你可以做,甚至可以压缩整理。这样做最简单的方法是实现一些Enumerable和Enumerator方法。当你这样做,你会发现有些地方我们写得不足,或可以用另外更好的方式实现。这很好,因为它将教你更多关于这些功能如何衔接在一起。我提供的例子你能够改进和讨论,在其中肯定有重构的空间。最后,如果你想采取更严重的事情,你最好看一下测试RubySpec和Rubinius实现。这些天用Ruby实现ruby不只是一些乡亲的乐趣,且如果你真的很喜欢这些底层功能的工作。你应该考虑以某种方式为Rubinius做贡献。该项目的维护者们了不起,这样你可以学到很多。
If you enjoyed this exercise, there are a number of things that you could do to squeeze even more out of it. The easiest way to do so is to implement a few more of the Enumerable and Enumerator methods. As you do that, you'll find areas where the implementations we built out today are clearly insufficient or would be better off written another way. That's fine, because it will teach you even more about how these features hang together. You can also discuss and improve upon the examples I've provided, as there is certainly room for refactoring in several of them. Finally, if you want to take a more serious approach to things, you could take a look at the tests in RubySpec and the implementations in Rubinius. Implementing Ruby in Ruby isn't just something folks do for fun these days, and if you really enjoyed working on these low-level features, you might consider contributing to Rubinius in some way. The maintainers of that project are amazing, and you can learn a tremendous amount that way.
当然,不是每个人都有时间为Ruby做贡献,即使它的目的是提高它们自己对Ruby的理解。因此,我当然解决评论,对这个练习进行评论来分享你的经验。
Of course, not everyone has time to contribute to a Ruby implementation, even if it's for the purpose of advancing their own understanding of Ruby. So I'd certainly settle for a comment here sharing your experiences with this exercise.
初遇Ruby,Enumerable module提供的杀手级功能给我留下了最深刻的印象。我最终也爱上了Enumerator,即使我花了很长一段时间来弄清楚它是什么,什么人可使用它。
When I first came to Ruby, one of the things that impressed me the most was the killer features provided by the Enumerable module. I eventually also came to love Enumerator, even though it took me a long time to figure out what it was and what one might use it for.
作为一个初学者,我一直认为这些功能是来自Ruby解析器内部,通过一些黑暗魔法形式工作。有很多学习只是为了提高生产力,我推迟了学习引擎盖下是怎么回事的细节。一段时间后,我为这个决定感到后悔,谢谢David A.Black.
As a beginner, I had always assumed that these features worked through some dark form of magic that was buried deep within the Ruby interpreter. With so much left to learn just in order to be productive, I was content to postpone learning the details about what was going on under the hood. After some time, I came to regret that decision, thanks to David A. Black.
David 教Ruby向无经验的初学者们不久展示给他们Enumerable能做什么,同时也使他们实现他们自己的版本!这是一个很好的深刻锻炼,因为他揭开了神奇的面纱:如果你不懂yield, 你能创建所有的方便在Enumerable里。同样,如果你用Ruby的Fiber构造体,Enumerator有趣的功能能够很容易的实施。
David teaches Ruby to raw beginners not only by showing them what Enumerable can do, but also by making them implement their own version of it! This is a profoundly good exercise, because it exposes how nonmagical the features are: if you understand yield, you can build all the methods in Enumerable. Similarly, the interesting features of Enumerator can be implemented fairly easily if you use Ruby's Fiber construct.
在本文中,我们将通过滚动练习你自由的由Enumerable和Enumerator提供的功能子集,讨论沿途的每个工作细节。不管你的技术水平,了解这些构造优雅的设计,无疑将给你在你的新的程序构造设计过程中带来灵感。
In this article, we're going to work through the exercise of rolling your own subset of the functionality provided by Enumerable and Enumerator, discussing each detail along the way. Regardless of your skill level, an understanding of the elegant design of these constructs will undoubtedly give you a great source of inspiration that you can draw from when designing new constructs in your own programs.
Setting the stage with some tests(设置一些测试阶段)
我选择了一个小的Enumerable和Enumerator功能,但具有代表性子集,并写了一些测试,以便明确自己的行为。这些测试将指引我实现本文中全文的其余部分和如果你想尝试你自己练习将为你提供导向服务。
I've selected a small but representative subset of the features that Enumerable and Enumerator provide and written some tests to nail down their behavior. These tests will guide my implementations throughout the rest of this article and serve as a roadmap for you if you'd like to try out the exercise on your own.
如果你有一段时间这样做,尝试至少得到一些成功测试在阅读我的实现代码和解析之前,这方法你将学到更多。但如果你不计划这样做,至少认真阅读这些测试和仔细想想你怎么去实现他们描述的功能。
If you have some time to do so, try to get at least some of the tests to go green before reading my implementation code and explanations, as you'll learn a lot more that way. But if you're not planning on doing that, at least read through the tests carefully and think about how you might go about implementing the features they describe.
Toggle line numbers
1 class SortedList
2 include FakeEnumerable
3
4 def initialize
5 @data = []
6 end
7
8 def <<(new_element)
9 @data << new_element
10 @data.sort!
11
12 self
13 end
14
15 def each
16 if block_given?
17 @data.each { |e| yield(e) }
18 else
19 FakeEnumerator.new(self, :each)
20 end
21 end
22 end
23
24 require "minitest/autorun"
25
26 describe "FakeEnumerable" do
27 before do
28 @list = SortedList.new
29
30 # will get stored interally as 3,4,7,13,42
31 @list << 3 << 13 << 42 << 4 << 7
32 end
33
34 it "supports map" do
35 @list.map { |x| x + 1 }.must_equal([4,5,8,14,43])
36 end
37
38 it "supports sort_by" do
39 # ascii sort order
40 @list.sort_by { |x| x.to_s }.must_equal([13, 3, 4, 42, 7])
41 end
42
43 it "supports select" do
44 @list.select { |x| x.even? }.must_equal([4,42])
45 end
46
47 it "supports reduce" do
48 @list.reduce(:+).must_equal(69)
49 @list.reduce { |s,e| s + e }.must_equal(69)
50 @list.reduce(-10) { |s,e| s + e }.must_equal(59)
51 end
52 end
53
54 describe "FakeEnumerator" do
55 before do
56 @list = SortedList.new
57
58 @list << 3 << 13 << 42 << 4 << 7
59 end
60
61 it "supports next" do
62 enum = @list.each
63
64 enum.next.must_equal(3)
65 enum.next.must_equal(4)
66 enum.next.must_equal(7)
67 enum.next.must_equal(13)
68 enum.next.must_equal(42)
69
70 assert_raises(StopIteration) { enum.next }
71 end
72
73 it "supports rewind" do
74 enum = @list.each
75
76 4.times { enum.next }
77 enum.rewind
78
79 2.times { enum.next }
80 enum.next.must_equal(7)
81 end
82
83 it "supports with_index" do
84 enum = @list.map
85 expected = ["0. 3", "1. 4", "2. 7", "3. 13", "4. 42"]
86
87 enum.with_index { |e,i| "#{i}. #{e}" }.must_equal(expected)
88 end
89 end
如果你决定尝试自己来实现这些功能,你尽可能的靠近Ruby提供的行为,不过不用担心,如果你的实现与Ruby的什么真正用途不同。试想,这如果是一个新的问题需要解决,那么让测试引导你的实现。一旦你这样做了,继续读看我怎么做。
If you do decide to try implementing these features yourself, get as close to the behavior that Ruby provides as you can, but don't worry if your implementation is different from what Ruby really uses. Just think of this as if it's a new problem that needs solving, and let the tests guide your implementation. Once you've done that, read on to see how I did it.
Implementing the FakeEnumerable module(实现FakeEnumerable module)
在我开始实现FakeEnumerable工作之前,我需要得到它的测试故障状态,而不是错误状态。下面的代码完全相同:
Before I began work on implementing FakeEnumerable, I needed to get its tests to a failure state rather than an error state. The following code does exactly that:
Toggle line numbers
1 module FakeEnumerable
2 def map
3 end
4
5 def select
6 end
7
8 def sort_by
9 end
10
11 def reduce(*args)
12 end
13 end
这时我开始一个一个方法实现工作,跟着地图开始。关键是要认识到用Enumerable工作时,每一个功能,将以某种方式建立在每个方法上,使用它产生的结果与产量结合。地图功能可能是最简单平凡组合这些操作,在这里的实施你可以看到。
I then began working on implementing the methods one by one, starting with map. The key thing to realize while working with Enumerable is that every feature will build on top of the each method in some way, using it in combination with yield to produce its results. The map feature is possibly the most straightforward nontrivial combination of these operations, as you can see in this implementation:
Toggle line numbers
1 def map
2 out = []
3
4 each { |e| out << yield(e) }
5
6 out
7 end
这里我们可以看到,map是一个简单的函数,建立一个新数组将每个元素替换为您提供的块返回值。我们能清洁一下,使用一个内嵌的Ojbect#tap,但我不知道我是否喜欢这种做法,因为它有点打破简单的实现。这就是说,我在这里包括它和将在本文整个的其余部分使用它是为你考虑,只是为了简洁起见。
Here we see that map is simply a function that builds up a new array by taking each element and replacing it with the return value of the block you provide to it. We can clean this up to make it a one liner using Object#tap, but I'm not sure if I like that approach because it breaks the simplicity of the implementation a bit. That said, I've included it here for your consideration and will use it throughout the rest of this article, just for the sake of brevity.
Toggle line numbers
1 def map
2 [].tap { |out| each { |e| out << yield(e) } }
3 end
实现select也非常简单。它使用实现map相同的原理构建但增加时有条件检测,判断块返回的值是否为真。对于每一个新产生的元素,如果通过块返回值是逻辑真,那么这个元素将增加到新数组里面,否则,不增加。
Implementing select is quite easy as well. It builds on the same concepts used to implement map but adds a conditional check to see whether the block returns a true value. For each new yielded element, if the value returned by the block is logically true, the element gets added to the newly built array; otherwise, it does not.
Toggle line numbers
1 def select
2 [].tap { |out| each { |e| out << e if yield(e) } }
3 end
实现sort_by要棘手一点。我被骗看API文档,那(也许令人惊讶)描述这个方法怎么实现和甚至可以用Ruby参考实现。显然,sort_by使用一个Schwartzian变换转换我们遍历到包含了排序键和原始元素的元组的集合。然后它使用Array#sort将它们排序,并最终使用结果数组map从原来的集合元素的数组转换成元组阵列。这绝对是更加混淆的解释超过它的代码实施,所以为清晰只需看看下面的代码。
Implementing sort_by is a little more tricky. I cheated and looked at the API documentation, which (perhaps surprisingly) describes how the method is implemented and even gives a reference implementation in Ruby. Apparently, sort_by uses a Schwartzian transform to convert the collection we are iterating over into tuples containing the sort key and the original element. It then uses Array#sort to put these in order, and it finally uses map on the resulting array to convert the array of tuples back into an array of the elements from the original collection. That's definitely more confusing to explain than it is to implement in code, so just look at the following code for clarification:
Toggle line numbers
1 def sort_by
2 map { |a| [yield(a), a] }.sort.map { |a| a[1] }
3 end
有趣的是关于这个实现是依赖于map之上,当前集合的迭代以及它产生的数组。但后来跟踪它到核心,这个方法是仍然除集合实现只有each方法。此外,因为Array#sort是扔进组合,你的排序键需要响应<=>。但对于一个如此强大的方法,收缩性仍然很窄。
The interesting thing about this implementation is that sort_by is dependent on map, both on the current collection being iterated over as well as on the Array it generates. But after tracing it down to the core, this method is still expecting the collection to implement only the each method. Additionally, because Array#sort is thrown into the mix, your sort keys need to respond to <=>. But for such a powerful method, the contract is still very narrow.
实现reduce要复杂一点,因为它有三种不同的方式与它进行交互。这也是有趣的,因为它不一定是返回一个数组对象的几个Enumerable的方法之一。我会让你思考以下实施一个点,提供更多的评论之前,因为通过阅读它应该是一个很好的锻炼。
Implementing reduce is a bit more involved because it has three different ways of interacting with it. It's also interesting because it's one of the few Enumerable methods that isn't necessarily designed to return an Array object. I'll let you ponder the following implementation a bit before providing more commentary, because reading through it should be a good exercise.
Toggle line numbers
1 def reduce(operation_or_value=nil)
2 case operation_or_value
3 when Symbol
4 # convert things like reduce(:+) into reduce { |s,e| s + e }
5 return reduce { |s,e| s.send(operation_or_value, e) }
6 when nil
7 acc = nil
8 else
9 acc = operation_or_value
10 end
11
12 each do |a|
13 if acc.nil?
14 acc = a
15 else
16 acc = yield(acc, a)
17 end
18 end
19
20 return acc
21 end
首先,我不得不说我不是特别满意我的实现;似乎有点太蛮力,我想我可能会丢失一些明显的重构。但它应该是可读的,足以让您感觉到是怎么回事。第一段代码是简单地处理三个不同情况下的reduce()。我们每个呼叫开始产生实际操作。
First, I have to say I'm not particularly happy with my implementation; it seems a bit too brute force and I think I might be missing some obvious refactorings. But it should have been readable enough for you to get a general feel for what's going on. The first paragraph of code is simply handling the three different cases of reduce(). The real operation happens starting with our each call.
没有预定义一个初始值,我们设初始值为集合的第一个元素,且我们第一个yield从第二个元素开始产生。否则,初始值和第一个元素yielded。reduce()的目的是执行一个操作使用一些方法将列表中的每个连续值与最后计算值相结合。通过这种方式,列表就会减少到最终的单个值。这个行为解释了这个方法旧别名在Ruby里面是被称作inject:被称作injected的一个函数,在集合中的每个元素之间通过我们的yield调用。我发现这个操作更容易理解,当我能够看到它的原始概念例如yield和each,因为它使我们能够跟踪到底是怎么回事。
Without a predefined initial value, we set the initial value to the first element in the collection, and our first yield occurs starting with the second element. Otherwise, the initial value and first element are yielded. The purpose of reduce() is to perform an operation on each successive value in a list by combining it in some way with the last calculated value. In this way, the list gets reduced to a single value in the end. This behavior explains why the old alias for this method in Ruby was called inject: a function is being injected between each element in the collection via our yield call. I find this operation much easier to understand when I'm able to see it in terms of primitive concepts such as yield and each because it makes it possible to trace exactly what is going on.
如果你接下来实现reduce()遇到了麻烦,别担心。这肯定是更复杂的枚举方法之一,如果你尝试去实现一些另外的然后返回来学习reduce(),你可能会更幸运。但最美妙的东西是如果你忽略了reduce(:+)语法糖,它引入了新概念除此之外,map()是用什么来实现的。如果认为你理解了map()但不理解reduce(),那意味着你可能需要复习你的基础知识,例如:yield是如何工作的。
If you are having trouble following the implementation of reduce(), don't worry about it. It's definitely one of the more complex Enumerable methods, and if you try to implement a few of the others and then return to studying reduce(), you may have better luck. But the beautiful thing is that if you ignore the reduce(:+) syntactic sugar, it introduces no new concepts beyond that what is used to implement map(). If you think you understand map() but not reduce(), it's a sign that you may need to brush up on your fundamentals, such as how yield works.
如果你一路走来,你的所有FakeEnumerable测试应该都通过了。那意味着是时间开始我们的FakeEnumerator了。
If you've been following along at home, you should at this point be passing all your FakeEnumerable tests. That means it's time to get started on our FakeEnumerator.
Implementing the FakeEnumerator class(实现FakeEnumerator类)
与前面类似,我需要写一些故障状态的测试代码。首先,设立FakeEnumerator类的架构。
Similar to before, I needed to write some code to get my tests to a failure state. First, I set up the skeleton of the FakeEnumerator class.
Toggle line numbers
1 class FakeEnumerator
2 def next
3 end
4
5 def with_index
6 end
7
8 def rewind
9 end
10 end
这时我意识到我需要返回去修改FakeEnumerable#map方法,当没有提供block的时候我的测试依赖于它返回的一个FakeEnumerator对象,在这种情况下类似的方式方法Enumerable#map应该返回一个Enumerator。
Then I realized that I needed to back and at least modify the FakeEnumerable#map method, as my tests rely on it returning a FakeEnumerator object when a block is not provided, in a similar manner to the way Enumerable#map would return an Enumerator in that scenario.
Toggle line numbers
1 module FakeEnumerable
2 def map
3 if block_given?
4 [].tap { |out| each { |e| out << yield(e) } }
5 else
6 FakeEnumerator.new(self, :map)
7 end
8 end
9 end
虽然,从技术层面上讲,我也应该更新我所有的FakeEnumerable方法,那不是非常重要,因为我们的测试没有覆盖它并且没有新的概念需要介绍讨论。伴随map的这个修改,我所有的测试都是故障并非错误,那意味着是时候开始代码的实现工作了。
Although, technically speaking, I should have also updated all my other FakeEnumerable methods, it's not important to do so because our tests don't cover it and that change introduces no new concepts to discuss. With this change to map, my tests all failed rather than erroring out, which meant it was time to start working on the implementation code.
但在我们开始前,值得重构Enumerator的核心目的,我还没有谈到yet。在它的核心,一个Enumerator是一个简单的代理对象它混合在Enumerable里面,进而代表其每个方法由它包装的对象提供给一些其它的迭代器。这个行为将一个内部迭代器转换为一个外部的,它允许你做为一个对象来操作。
But before we get started, it's worth reflecting on the core purpose of an Enumerator, which I haven't talked about yet. At its core, an Enumerator is simply a proxy object that mixes in Enumerable and then delegates its each method to some other iterator provided by the object it wraps. This behavior turns an internal iterator into an external one, which allows you to pass it around and manipulate it as an object.
我们的测试要求我们实现next, rewind, 和 each_index,但之前我们能做些有意义的,我们需要使FakeEnumerator成为FakeEnumerable可用的代理对象。关于这点没有测试因为,对于那些想在家里尝试练习的人,我不想透露太多提示,但以下代码会做这个手法。
Our tests call for us to implement next, rewind, and each_index, but before we can do that meaningfully, we need to make FakeEnumerator into a FakeEnumerable-enabled proxy object. There are no tests for this because I didn't want to reveal too many hints to those who wanted to try this exercise at home, but this code will do the trick:
Toggle line numbers
1 class FakeEnumerator
2 include FakeEnumerable
3
4 def initialize(target, iter)
5 @target = target
6 @iter = iter
7 end
8
9 def each(&block)
10 @target.send(@iter, &block)
11 end
12
13 # other methods go here...
14 end
这里我们可以看到在目标对象上每个使用send来调用原来的iterator方法。除此之外,我们在实现其它集合看到这是普通模式。接下来是实现我们的next方法,那有点 棘手。
Here we see that each uses send to call the original iterator method on the target object. Other than that, this is the ordinary pattern we've seen in implementing other collections. The next step is to implement our next method, which is a bit tricky.
我们需要能够做到的是遍历一次,然后暂时返回一个值。然后,当next再次被调用,我们需要能够做到提前一次迭代并重复这个过程。我们能做些像运行整个迭代和缓存结果到一个数组里面。这个问题使我认识到Ruby的Fiber构造可能非常适合,因为它特别允许你跳入或跳出需要的代码块。所以我决定尝试,看我能做到怎样的程度。后来经过摸索,我得到了如下通过测试的代码:
What we need to be able to do is iterate once, then pause and return a value. Then, when next is called again, we need to be able to advance one more iteration and repeat the process. We could do something like run the whole iteration and cache the results into an array, then do some sort of indexing operation, but that's both inefficient and impractical for certain applications. This problem made me realize that Ruby's Fiber construct might be a good fit because it specifically allows you to jump in and out of a chunk of code on demand. So I decided to try that out and see how far I could get. After some fumbling around, I got the following code to pass the test:
Toggle line numbers
1 # loading the fiber stdlib gives us some extra features, including Fiber#alive?
2 require "fiber"
3
4 class FakeEnumerator
5 def next
6 @fiber ||= Fiber.new do
7 each { |e| Fiber.yield(e) }
8
9 raise StopIteration
10 end
11
12 if @fiber.alive?
13 @fiber.resume
14 else
15 raise StopIteration
16 end
17 end
18 end
这段代码很难读,因为它不是真的线性流程,但我将做用我非常有限的知识尽量解释Fiber结构是如何工作的。当你使用块调用Fiber#new时,在块代中的代码并没有立即执行。反而,当Fiber#resume被调用里开始执行。每次遇到一个Fiber#yield调用的时候,控制返回到Fiber#resume调用者,这个值伴随Fiber#yield返回。每个随后Fiber#resume拿起执行返回点,那个最后调用Fiber#yield处理,并非在代码块的开始。这个过程一直到只有留下一个Fiber@yield调用,这时Fiber#resume结果在一个FiberError里面,因为那儿没有任何执行。
This code is hard to read because it isn't really a linear flow, but I'll do my best to explain it using my very limited knowledge of how the Fiber construct works. Basically, when you call Fiber#new with a block, the code in that block isn't executed immediately. Instead, execution begins when Fiber#resume is called. Each time a Fiber#yield call is encountered, control is returned to the caller of Fiber#resume with the value that was passed to Fiber#yield returned. Each subsequent Fiber#resume picks up execution back at the point where the last Fiber#yield call was made, rather than at the beginning of the code block. This process continues until no more Fiber#yield calls remain, and then the last executed line of code is returned as the final value of Fiber#resume. Any additional attempts to call Fiber#resume result in a FiberError because there is nothing left to execute.
如果你重读前段几次和我后面定义的方法比较,它应该开始应得有意义。但是如果它融化进你的大脑,查证Fiber documentation,这是合理有益的。
If you reread the previous paragraph a couple of times and compare it to the definition of my next method, it should start to make sense. But if it's causing your brain to melt, check out the Fiber documentation, which is reasonably helpful.
这个整个很短的故事,在我们下一个定义中使用一个Fiber,让我们只保持在如何在每个迭代跟踪更远,且跳回迭代器里面的需求来得到下个值。我防止调用resume之前FiberError从不断通过检查来判断Fiber对象是否仍然活着。但我也需要使其不至于在最后Fiber抛出StopIteration错误时执行这部分,为了防止它从每个返回结果,它应该能集合它自身。这是一个kludge,如果你有更好的主意来控制这种情况,请给我留言。
The very short story about this whole thing is that using a Fiber in our next definition lets us keep track of just how far into the each iteration we are and jump back into the iterator on demand to get the next value. I prevent the FiberError from ever occurring by checking to see whether the Fiber object is still alive before calling resume. But I also need to make it so that the final executed statement within the Fiber raises a StopIteration error as well, to prevent it from returning the result of each, which would be the collection itself. This is a kludge, and if you have a better idea for how to handle this case, please leave me a comment.
这儿使用了Fiber对象来实现next,使它能够无限迭代的工作,例如:Enumerabler#cycle。虽然我们没有进入实现的细节,下面的代码应该能够做为为什么这是一个有用功能的提示。
The use of Fiber objects to implement next makes it possible to work with infinite iterators, such as Enumerable#cycle. Though we won't get into implementation details, the following code should give some hints as to why this is a useful feature:
Toggle line numbers
1 >> row_colors = [:red, :green].cycle
2 => #<Enumerator: [:red, :green]:cycle>
3 >> row_colors.next
4 => :red
5 >> row_colors.next
6 => :green
7 >> row_colors.next
8 => :red
9 >> row_colors.next
10 => :green
这是很酷,它尽可能多的让我想深入实施它,我想你现在开始感觉累了。哎呀,我准备休息一下,然后再开始写这遍文章!因此,让我们快点完成实现rewind和each_index以至于我们能包装这些东西。
As cool as that is, and as much as it makes me want to dig into implementing it, I have to imagine that you're getting tired by now. Heck, I've already slept twice since I started writing this article! So let's hurry up and finish implementing rewind and each_index so that we can wrap things up.
我发现一个实现rewind的方法,但关于它的一些东西让我惊呀,如果在哪儿我有一个独立的Fiber对象和是否有怪异的垃圾收集机制。但是然而,因为我实现next依赖于Fiber对象的缓存上保持跟踪在迭代里面它是在哪儿,这最容易的方法是让rewind回到最开始的状态,就是简单的擦除对象。下面的代码通过rewind测试:
I found a way to implement rewind that is trivial, but something about it makes me wonder if I've orphaned a Fiber object somewhere and whether that has weird garbage collection mplications. But nonetheless, because our implementation of next depends on the caching of a Fiber object to keep track of where it is in its iteration, the easiest way to rewind back to the beginning state is to simply wipe out that object. The following code gets my rewind tests passing:
Toggle line numbers
1 def rewind
2 @fiber = nil
3 end
现在只有一个功能,阻碍着我们和我们的工作完成:with_index。在Ruby里面真正real_index比你将看到的更灵巧,但其最简单的功能,如下的代码做法:
Now only one feature stands between us and the completion of our exercise: with_index. The real with_index method in Ruby is much smarter than what you're about to see, but for its most simple functionality, the following code will do the trick:
Toggle line numbers
1 def with_index
2 i = 0
3 each do |e|
4 out = yield(e, i)
5 i += 1
6 out
7 end
8 end
在这里,我没有做蛮力的事情,并保持自己的计数器。这样,我做了一个小的修改以控制流,不至于在每次迭代只产生元素,这个元素和它的index都产生。请记住,每个调用这里是一个代理在其它集合上的一些另外的迭代,这是让我们能够调用@list.map和获得map的行为并非所有行为。虽然你不会每天使用,知道如何用yield实现一个范围过滤器是非常有用的。
Here, I did the brute force thing and maintained my own counter. I then made a small modification to control flow so that rather than yielding just the element on each iteration, both the element and its index are yielded. Keep in mind that the each call here is a proxy to some other iterator on another collection, which is what gives us the ability to call @list.map.with_index and get map behavior rather than each behavior. Although you won't use every day, knowing how to implement an around filter using yield can be quite useful.
随着代码的编写,我的完整的测试终于都绿色了。尽管,我做这些练习前十几次,当我写这篇文章里我仍然在学习一个或两个事物,并且我想还有什么值得我学习的。你怎么样?
With this code written, my full test suite finally went green. Even though I'd done these exercises a dozen times before, I still learned a thing or two while writing this article, and I imagine there is still plenty left for me to learn as well. How about you?
Reflections(反射)
这肯定是我最喜欢的更好的理解ruby的练习。我通常不会进行大的演练,但那儿是一些关于脱回神奇的功能,那表面上看起来真的复杂,那让我很满意。我觉得,如果我的方案是非常假冒廉价,Ruby必须真正做什么,它仍然大大有助于我实现这些功能,因为它给我一个我自己构建的心理模型,从中看到功能。
This is definitely one of my favorite exercises for getting to understand Ruby better. I'm not usually big on contrived practice drills, but there is something about peeling back the magic on features that look really complex on the surface that gives me a great deal of satisfaction. I find that even if my solutions are very much cheap counterfeits of what Ruby must really be doing, it still helps tremendously to have implemented these features in any way I know how, because it gives me a mental model of my own construction from which to view the features.
如果你投入了这个练习,有些事情你可以做,甚至可以压缩整理。这样做最简单的方法是实现一些Enumerable和Enumerator方法。当你这样做,你会发现有些地方我们写得不足,或可以用另外更好的方式实现。这很好,因为它将教你更多关于这些功能如何衔接在一起。我提供的例子你能够改进和讨论,在其中肯定有重构的空间。最后,如果你想采取更严重的事情,你最好看一下测试RubySpec和Rubinius实现。这些天用Ruby实现ruby不只是一些乡亲的乐趣,且如果你真的很喜欢这些底层功能的工作。你应该考虑以某种方式为Rubinius做贡献。该项目的维护者们了不起,这样你可以学到很多。
If you enjoyed this exercise, there are a number of things that you could do to squeeze even more out of it. The easiest way to do so is to implement a few more of the Enumerable and Enumerator methods. As you do that, you'll find areas where the implementations we built out today are clearly insufficient or would be better off written another way. That's fine, because it will teach you even more about how these features hang together. You can also discuss and improve upon the examples I've provided, as there is certainly room for refactoring in several of them. Finally, if you want to take a more serious approach to things, you could take a look at the tests in RubySpec and the implementations in Rubinius. Implementing Ruby in Ruby isn't just something folks do for fun these days, and if you really enjoyed working on these low-level features, you might consider contributing to Rubinius in some way. The maintainers of that project are amazing, and you can learn a tremendous amount that way.
当然,不是每个人都有时间为Ruby做贡献,即使它的目的是提高它们自己对Ruby的理解。因此,我当然解决评论,对这个练习进行评论来分享你的经验。
Of course, not everyone has time to contribute to a Ruby implementation, even if it's for the purpose of advancing their own understanding of Ruby. So I'd certainly settle for a comment here sharing your experiences with this exercise.