Rubyの内部列挙
Ruby Magicの別のエディションへようこそ! 1年前、RubyのEnumerable
について学びました モジュール。配列、範囲、ハッシュなどの列挙可能なオブジェクトを操作するときに使用するメソッドを提供します。
当時、私たちはLinkedList
を作成しました #each
を実装してオブジェクトを列挙可能にする方法を示すクラス その上でメソッド。 Enumerable
を含めることによって モジュールでは、#count
のようなメソッドを呼び出すことができました 、#map
および#select
自分で実装しなくても、リンクリストに追加できます。
列挙型の使用方法を学びましたが、どのように機能しますか? Rubyの列挙型の魔法の一部は、すべて単一の#each
に基づく内部実装に由来します。 メソッド、さらには列挙子を連鎖させることもできます。
今日は、Enumerable
のメソッドがどのように機能するかを学習します クラスの実装方法とEnumerator
オブジェクトを使用すると、列挙メソッドを連鎖させることができます。
慣れてきたら、独自のバージョンのEnumerable
を実装して詳しく説明します。 モジュールとEnumerator
クラス。だから、あなたの過剰設計のヘルメットをかぶって行きましょう!
リンクリスト
始める前に、以前に作成したリンクリストクラスの新しいバージョンから始めましょう。
class LinkedList
def initialize(head = nil, *rest)
@head = head
if rest.first.is_a?(LinkedList)
@tail = rest.first
elsif rest.any?
@tail = LinkedList.new(*rest)
end
end
def <<(head)
@head ? LinkedList.new(head, self) : LinkedList.new(head)
end
def inspect
[@head, @tail].compact
end
def each(&block)
yield @head if @head
@tail.each(&block) if @tail
end
end
以前のバージョンとは異なり、この実装では、空のリスト、および3つ以上のアイテムを含むリストを作成できます。このバージョンでは、別のリンクリストを初期化するときに、リンクリストをテールとして渡すこともできます。
irb> LinkedList.new
=> []
irb> LinkedList.new(1)
=> [1]
irb> LinkedList.new(1, 2)
=> [1,[2]]
irb> LinkedList.new(1, 2, 3)
=> [1,[2,[3]]]
irb> LinkedList.new(1, LinkedList.new(2, 3))
=> [1,[2,[3]]]
irb> LinkedList.new(1, 2, LinkedList.new(3))
=> [1,[2,[3]]]
以前は、LinkedLIst
クラスにはEnumerable
が含まれていました モジュール。 Enumerable
のいずれかを使用してオブジェクトにマッピングする場合 のメソッドでは、結果は配列に格納されます。今回は、独自のバージョンを実装して、メソッドが代わりに新しいリンクリストを返すようにします。
列挙可能なメソッド
RubyのEnumerable
モジュールには、#map
などの列挙メソッドが付属しています 、#count
、および#select
。 #each
を実装する メソッドとEnumerable
を含む クラスのモジュールでは、リンクリストでこれらのメソッドを直接使用できます。
代わりに、DIYEnumerable
を実装します Rubyのバージョンの代わりにそれをインポートします。これは通常行うことではありませんが、列挙が内部でどのように機能するかについて明確な洞察を得ることができます。
#count
から始めましょう 。 Enumerable
のインポート可能な各メソッド クラスは#each
を使用します LinkedList
に実装したメソッド オブジェクトをループして結果を計算するクラス。
module DIYEnumerable
def count
result = 0
each { |element| result += 1 }
result
end
end
この例では、#count
を実装しました 新しいDIYEnumerable
のメソッド リンクリストに含めるモジュール。ゼロからカウンターを開始し、#each
を呼び出します ループごとに1つをカウンターに追加するメソッド。すべての要素をループした後、メソッドは結果のカウンターを返します。
module DIYEnumerable
# ...
def map
result = LinkedList.new
each { |element| result = result << yield(element) }
result
end
end
#map
メソッドも同様に実装されます。カウンターを保持する代わりに、空のリストとして開始するアキュムレーターを使用します。リスト内のすべての要素をループし、各要素で渡されたブロックを生成します。各歩留まりの結果は、アキュムレータリストに追加されます。
このメソッドは、入力リスト内のすべての要素をループした後、アキュムレータを返します。
class LinkedList
include DIYEnumerable
#...
end
DIYEnumerable
を含めた後 LinkedList
で 、新しく追加した#count
をテストできます および#map
メソッド。
irb> list = LinkedList.new(73, 12, 42)
=> [73, [12, [42]]]
irb> list.count
=> 3
irb> list.map { |element| element * 10 }
=> [420, [120, [730]]]
どちらの方法でも機能します。 #count
メソッドはリスト内のアイテムを正しくカウントし、#map
メソッドはアイテムごとにブロックを実行し、更新されたリストを返します。
反転リスト
ただし、#map
メソッドがリストを元に戻したようです。 #<<
として、それは理解できます リンクリストクラスのメソッドは、アイテムを追加するのではなく、リストに追加します。これは、リンクリストの再帰的な性質の機能です。
リストの順序を維持することが不可欠な状況では、リストをマッピングするときにリストを逆にする方法が必要です。 RubyはEnumerable#reverse_each
を実装します 、オブジェクトを逆にループします。それは私たちの問題に対する優れた解決策のように聞こえます。残念ながら、リストがネストされているため、このようなアプローチを使用することはできません。リストを完全にループするまで、リストの長さはわかりません。
リストに対してブロックを逆に実行する代わりに、#reverse_each
のバージョンを追加します これはこの2つのステップを実行します。最初にリストをループして、新しいリストを作成してリストを反転します。その後、反転したリストに対してブロックを実行します。
module DIYEnumerable
# ...
def reverse_each(&block)
list = LinkedList.new
each { |element| list = list << element }
list.each(&block)
end
def map
result = LinkedList.new
reverse_each { |element| result = result << yield(element) }
result
end
end
次に、#reverse_each
を使用します #map
で メソッド、正しい順序で返されることを確認します。
irb> list = LinkedList.new(73, 12, 42)
=> [73, [12, [42]]]
irb> list.map { |element| element * 10 }
=> [730, [120, [420]]]
できます! #map
を呼び出すときはいつでも リンクリストのメソッドを使用すると、元のリストと同じ順序で新しいリストが返されます。
列挙子を使用した列挙の連鎖
#each
を介して リンクリストクラスに実装されたメソッドと含まれているDIYEnumerator
、これで、双方向をループして、リンクリストにマッピングできます。
irb> list.each { |x| p x }
73
12
42
irb> list.reverse_each { |x| p x }
42
12
73
irb> list.reverse_each.map { |x| x * 10 }
=> [730, [120, [420]]]
=> [420, [120, [730]]]
ただし、マップが必要な場合はどうなりますか 逆にリストを超えますか?リストをマッピングする前にリストを逆にするため、常に元のリストと同じ順序で返されます。すでに両方の#reverse_each
を実装しています および#map
、したがって、それらをチェーンして逆方向にマップできるようにする必要があります。幸い、RubyのEnumerator
クラスはそれを助けることができます。
前回は、必ずKernel#to_enum
を呼び出しました。 LinkedList#each
の場合 メソッドはブロックなしで呼び出されました。これにより、Enumerator
を返すことで、列挙可能なメソッドを連鎖させることができました。 物体。 Enumerator
の方法を確認するには クラスは機能します。独自のバージョンを実装します。
class DIYEnumerator
include DIYEnumerable
def initialize(object, method)
@object = object
@method = method
end
def each(&block)
@object.send(@method, &block)
end
end
RubyのEnumerator
のように 、列挙子クラスは、オブジェクトのメソッドのラッパーです。ラップされたオブジェクトにバブリングすることで、列挙メソッドを連鎖させることができます。
これは、DIYEnumerator
が機能するために機能します インスタンス自体は列挙可能です。 #each
を実装します ラップされたオブジェクトを呼び出すことにより、DIYEnumerable
が含まれます モジュールなので、列挙可能なすべてのメソッドを呼び出すことができます。
DIYEnumerator
のインスタンスを返します LinkedList#each
にブロックが渡されない場合のクラス メソッド。
class LinkedList
# ...
def each(&block)
if block_given?
yield @head
@tail.each(&block) if @tail
else
DIYEnumerator.new(self, :each)
end
end
end
独自の列挙子を使用して、列挙をチェーンし、空のブロックを#reverse_each
に渡すことなく、元の順序で結果を取得できるようになりました。 メソッド呼び出し。
irb> list = LinkedList.new(73, 12, 42)
=> [73, [12, [42]]]
irb> list.map { |element| element * 10 }
=> [420, [120, [730]]]
熱心で怠惰な列挙
これで、Enumerable
の実装についての概要は終わりです。 モジュールとEnumerator
今のところクラス。列挙可能なメソッドのいくつかがどのように機能するか、および列挙子が列挙可能なオブジェクトをラップすることによって列挙を連鎖させるのにどのように役立つかを学びました。
ただし、私たちのアプローチにはいくつかの問題があります。その性質上、列挙は熱心です 、つまり、列挙可能なメソッドの1つがリストで呼び出されるとすぐに、リストをループします。ほとんどの場合、これで問題ありませんが、リストを逆にマッピングすると、リストが2回反転するため、不要です。
ループの数を減らすために、Enumerator::Lazy
を使用できます。 ループを最後の瞬間まで遅らせ、重複リストを反転させると、自動的にキャンセルされます。
ただし、将来のエピソードのためにそれを保存する必要があります。それを見逃したくない、そしてRubyの魔法の内部の仕組みへのさらなる遠征? Ruby Magicの電子メールニュースレターを購読して、新しい記事が公開されるとすぐに受信トレイに配信されるようにします。
-
Rubyでの挿入ソートを理解する
注:これは、Rubyを使用したさまざまなソートアルゴリズムの実装を検討するシリーズのパート4です。パート1ではバブルソート、パート2では選択ソート、パート3ではマージソートについて説明しました。 データを並べ替えるためのさまざまな方法を引き続き検討するため、挿入並べ替えに目を向けます。挿入ソートが好きな理由はたくさんあります!まず、挿入ソートは安定です。 、これは、等しいキーを持つ要素の相対的な順序を変更しないことを意味します。 インプレースアルゴリズムでもあります 、は、並べ替えられた要素を格納するための新しい配列を作成しないことを意味します。最後に、挿入ソートは、すぐにわかるように、実
-
Ruby2.6の9つの新機能
Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,