Rubyによる高度な列挙

列挙とは、定義上、「一定量の何かを1つずつ言及する行為」です。 プログラミングでは、言及する代わりに、モニターへの単純な出力であるか、何らかの要素の選択および/または変換の実行であるかを問わず、実行するアクションを選択します。



プログラミングでは、各ステップでチェーンに追加の変換関数を追加することにより、単位時間ごとにコレクションを選択して処理する多くの方法があります。 また、各ステップは、処理結果を次のステップに転送する前にコレクション全体を消費するか、コレクションの1つ以上の要素をすべての変換ステップに渡すことでコレクションを「怠”に」処理できます。



Ruby列挙の仕組み



この投稿では、ブロックとyieldの機能の概要を簡単に説明します。 興味のあるRuby のブロックは、メソッドまたはproc / lambda内で定義されたコードです。 yieldは、コードの別のブロックが別の場所から挿入される現在のコードのブロックと考えることができます。 見せてあげましょう。



def my_printer puts "Hello World!" end def thrice 3.times do yield end end thrice &method(:my_printer) # Hello World! # Hello World! # Hello World! thrice { puts "Ruby" } # Ruby # Ruby # Ruby
      
      





メソッドは、 yieldコマンドに対して2つの形式のブロックを取ります:procまたはblocks。 メソッドmethodは、 メソッド定義をprocに変換し、上記のmy_printerの例のように、 ブロックとして内部に渡すことができます。



上記のyieldコマンドが記述されている場所では、ブロックとして渡されたコードがyieldの代わりになるかのように、同等です。 したがって、最初のケースでは、 yield呼び出しがputs "Hello World!"に置き換えられ、2番目のyieldputs "Ruby"に置き換えられていること想像してください。



yieldは単純な列挙子としても機能します。 yieldの後に値追加することによりblock / procのパラメーターとして任意の値を内側に渡すことができます。



 def simple_enum yield 4 yield 3 yield 2 yield 1 yield 0 end simple_enum do |value| puts value end # 4 # 3 # 2 # 1 # 0
      
      





最小列挙子の要件



Rubyで列挙子を作成する標準的な方法は、 each 、yields / yields値です。 これを念頭に置いて、任意のRubyオブジェクトでメソッドを宣言し、 Enumerableモジュールからコレクションを処理および実行するための50を超えるメソッドのすべての利点を取得できます。 有効なメソッドを持つオブジェクト内にEnumerableを追加するだけで、これらすべてのメソッドを完全に使用できます(Enumerableモジュールのメソッドを参照)。



列挙子は、 配列(配列)の単純なコレクションに限定されず、宣言されたメソッドを持つコレクションで動作できます(通常、「先祖/先祖」にEnumerableモジュールがあります)。



 Array.ancestors # => [Array, Enumerable, Object, Kernel, BasicObject] Hash.ancestors # => [Hash, Enumerable, Object, Kernel, BasicObject] Hash.method_defined? :each # => true require "set" Set.ancestors # => [Set, Enumerable, Object, Kernel, BasicObject]
      
      





「レイジー」列挙子ではなく「レイジー」列挙子



「遅延」列挙子は、通常、コレクションを処理するための最良の方法と見なされます。 必要な限り、無段階の無限シーケンスをバイパスできます。



人々がピザを収集し、各人がピザの準備/変換の1ステップのみを担当する組立ラインを想像してください。 最初の人は正しい形の生地を投げ、次の人はソース、次のチーズ、追加ごとに一人(ソーセージ、ピーマン、トマト)を加え、別の人はすべてをオーブンに入れ、最後の人は完成したピザをあなたに届けます。 この例では、Rubyビルドの「遅延」バージョンはピザの注文をいくつでも持つことになっていますが、残りのピザはすべて、最初のピザがすべての処理ステップ/ステップを完了するまで待ってから次のピザを作ります。



「遅延」列挙子を使用していない場合、各ステップ/ステージは、コレクション全体が単位時間ごとに1ステップ完了するまで待つことができます。 たとえば、ピザの注文が20件ある場合、生地を投げる人は、そのうちの1人がソースを追加する前に20個作る必要があります。 そして、ラインの各ステップは同様の方法で待機します。 現在、処理する必要のあるコレクションが多いほど、残りの組み立てラインを待機し続けるのはばかげているようです。



より重要な例:すべてのユーザーに送信する必要がある手紙のリストを処理します。 コードにエラーがあり、リスト全体が「怠iに」処理されない場合、誰もメールを受信しない可能性があります。 「怠yな」実行の場合、潜在的には、間違った郵便アドレスが問題/エラーを引き起こす前に、ほとんどのユーザーに手紙を送る可能性があります。 送信レコードに送信の成功ステータスが含まれている場合、エラーが発生したレコード(場所)を追跡する方が簡単です。



Rubyで「遅延」列挙子を作成するには、 Enumerableモジュールが含まれるオブジェクトでlazyを呼び出すか、 メソッドが宣言されたオブジェクトでto_enum.lazyを呼び出すだけです。



 class Thing def each yield "winning" yield "not winning" end end a = Thing.new.to_enum.lazy Thing.include Enumerable b = Thing.new.lazy a.next # => "winning" b.next # => "winning"
      
      





to_enum呼び出しは、 EnumeratorEnumerableの両方であり、すべてのメソッドにアクセスできるオブジェクトを返します



列挙子のどのメソッドがコレクション全体を一度に「消費」し、どのメソッドがそれを「消費」(実行)-「怠lazに」するかに注意を払うことが重要です。 たとえば、 パーティション方式は一度にコレクション全体を消費するため、無限のコレクションには受け入れられません。 遅延実行のより良い選択は、 chunkまたはselectなどのメソッドです。



 x = (0..Flot::INFINITY) y = x.chunk(&:even?) # => #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x0055eb840be350>:each>> y.next # => [true, [0]] y.next # => [false, [1]] y.next #=> [true, [2]] z = x.lazy.select(&:even?) # => #<Enumerator::Lazy: #<Enumerator::Lazy: 0..Infinity>:select> z.next # => 0 z.next # => 2 z.next # => 4
      
      





無限のコレクションでselectを使用する場合は、最初に遅延メソッドを呼び出して、コレクション全体がselectメソッドによって消費されないようにし、無限の実行によりプログラムを強制終了する必要があります。



遅延列挙子の作成



RubyにはEnumerator :: Lazyクラスがあり 、Rubyでのような独自の列挙子メソッドを記述できます。



 (0..Float::INFINITY).take(2) # => [0, 1]
      
      





良い例として、FizzBu​​zzを実装します。これは任意の番号で実行でき、無限のFizzBu​​zzの結果を得ることができます。



 def divisible_by?(num) ->input{ (input % num).zero? } end def fizzbuzz_from(value) Enumerator::Lazy.new(value..Float::INFINITY) do |yielder, val| yielder << case val when divisible_by?(15) "FizzBuzz" when divisible_by?(3) "Fizz" when divisible_by?(5) "Buzz" else val end end end x = fizzbuzz_from(7) # => #<Enumerator::Lazy: 7..Infinity:each> 9.times { puts x.next } # 7 # 8 # Fizz # Buzz # 11 # Fizz # 13 # 14 # FizzBuzz
      
      





Enumerator :: Lazyを使用すると、 yielderに何をフィードするかは関係ありません。シーケンスの各ステップで値が返されます。 nextを使用すると、列挙子は現在の進行状況を監視します。 ただし、 nextを数回呼び出した後にそれぞれを呼び出すと、コレクションの最初から開始されます。



Enumerator :: Lazy.newに渡すパラメーターは、 列挙子を通過するコレクションです。 Enumerableまたは互換性のあるオブジェクトに対してこのメ​​ソッドを記述した場合、単にselfをパラメーターとして渡すことができます。 valは、コレクションメソッドeachによってユニットごとに生成される唯一の値になり、 yielderは、 それぞれの場合と同様に、渡すコードブロックの唯一のエントリポイントになります。



高度な列挙子の使用



データコレクションを処理する場合、最初に制限フィルターを設定することをお勧めします。その後、コードを使用してデータを処理する時間を大幅に短縮できます。 データベースから処理用のデータを受け取った場合、可能であれば、データベースの内部言語で制限フィルターを設定してから、さらにデータをRubyに転送します。 そのため、はるかに効果的です。



 require "prime" x = (0..34).lazy.select(&Prime.method(:prime?)) x.next # => 2 x.next # => 3 x.next # => 5 x.next # => 7 x.next # => 11
      
      





上記の選択方法の後、他の方法をデータ処理に追加できます。 これらのメソッドは、すべての素数ではなく、素数内の限られたデータのみを処理します。



グルーピング



データを列に処理する優れた方法の1つは、 group_byを使用して、結果をグループの連想配列に変換することです。 その後、すべての結果に興味があるかのように結果をプルします。



 [0,1,2,3,4,5,6,7,8].group_by.with_index {|_,index| index % 3 }.values # => [[0, 3, 6], [1, 4, 7], [2, 5, 8]]
      
      





結果をWebページに表示する場合、データは次の順序で配置されます。



 0 3 6 1 4 7 2 5 8
      
      





上記のgroup_by呼び出しは、値とインデックスの両方をブロックに渡します。 配列の値にアンダースコアを使用して、この値に関心がなく、インデックスにのみ関心があることを示します。 結果として得られるのは、キー0、1、2がグループ化した値の各グループを指す連想配列です。 キーを心配する必要がないので、この連想配列でを呼び出して配列の配列を取得し、必要に応じてさらに表示します。



結果を列の形式で左から右に並べたい場合、次のようにします。



 threes = (0..2).cycle [0,1,2,3,4,5,6,7,8].slice_when { threes.next == 2 }.to_a # => [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
      
      





threes列挙子は、「怠lazな」方法で0から2まで無限に移動します。 その結果、次の結論が得られます。



 0 1 2 3 4 5 6 7 8
      
      





Rubyには、上記の結果をあるビューから別のビューに切り替える転置メソッドもあります。



 x = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] x = x.transpose # => [[0, 3, 6], [1, 4, 7], [2, 5, 8]] x = x.transpose # => [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
      
      





「折りたたみ」



コレクションを結果にまとめる方法を見てみましょう。 他の言語では、これは通常foldメソッドを介して行われます。 Rubyでは、これはreduceinjectで長い間行われてきました。 より最近の解決策、およびeach_with_objectを使用してこれを行うための好ましい方法。 主なアイデアは、あるコレクションを別のコレクションに処理し、結果として機能することです。



整数の合計は次のように簡単です。



 [1,2,3].reduce(:+) # => 6 [1,2,3].inject(:+) # => 6 class AddStore def add(num) @value = @value.to_i + num end def inspect @value end end [1,2,3].each_with_object(AddStore.new) {|val, memo| memo.add(val) } # => 6 # As of Ruby 2.4 [1,2,3].sum # => 6
      
      





each_with_objectは通常、更新可能なオブジェクトを必要とします。 整数オブジェクトをそれ自体から変更することはできません。このため、この簡単な例ではAddStoreオブジェクトを作成しました。



これらのメソッドは、あるコレクションからデータを取得して別のコレクションに入れると、作品でよりよく実証できます。 injectreduceはRubyの同じメソッドエイリアスであり、ブロックに基づいて列挙子を構築し続けるには、ブロックの最後にこの値を返す必要があることに注意してください。



each_with_objectは、列挙子をさらに構築する要素を返すためにブロックの最後の部分を必要としません。



 collection = [:a, 2, :p, :p, 6, 7, :l, :e] collection.reduce("") { |memo, value| memo << value.to_s if value.is_a? Symbol memo # Note the return value needs to be the object/collection we're building } # => "apple" collection.each_with_object("") { |value, memo| memo << value.to_s if value.is_a? Symbol } # => "apple"
      
      





構造



Rubyのオブジェクト構造も列挙オブジェクトであり、それらを使用して便利なオブジェクトを作成し、メソッドを記述することができます。



 class Pair < Struct.new(:first, :second) def same?; inject(:eql?) end def add; inject(:+) end def subtract; inject(:-) end def multiply; inject(:*) end def divide; inject(:/) end def swap! members.zip(entries.reverse) {|a,b| self[a] = b} end end x = Pair.new(23, 42) x.same? # => false x.first # => 23 x.swap! x.first # => 42 x.multiply # => 966
      
      





通常、構造は大規模なコレクションには使用されませんが、データをまとめて整理する方法として有用なデータオブジェクトとして使用されます。これにより、大きくなりすぎたデータではなく、データの透過的な使用が促進されます。

データの過剰成長は、2つ以上の変数が常にグループで使用される場合ですが、同時にそれらが別々に使用される場合は意味がありません。 この変数グループは、オブジェクト/クラスにグループ化する必要があります。



そのため、Rubyの構造は通常、小さなデータのコレクションですが、このデータだけでまったく異なるデータのコレクションを表すことができることを示唆するものはありません。 この場合、構造は同じデータコレクションに変換を実装する方法になる可能性があります。これは、独自のクラスを記述することで同じことを行う可能性が高いです。



まとめると



Rubyは、データコレクションの操作だけでなく作業も簡単にする非常に素晴らしい言語です。 Rubyが提供するもののすべてを調べることで、よりエレガントなコードを記述できるだけでなく、コードをテストおよび最適化してパフォーマンスを向上させることができます。



パフォーマンスが重要な場合は、個々の実装のパフォーマンスを測定し、もちろん、可能な場合はできるだけ早く、処理プロセスでフィルターと制限/制限を設定してください。 読み取りまたは読み取り行ではなく、ファイルでreadlineメソッドを使用して、入力を小さなチャンクに制限する 、SQLでLIMIT番号を使用することを検討してください。



遅延反復は、タスクを異なるスレッドまたはバックグラウンド処理ジョブに分割する際に非常に役立ちます。 「遅延」イテレーションの概念には実際に欠点がないので、どこでもコレクションを消費するように選択できます。 それは柔軟性を提供し、イテレータを備えたRustのようないくつかの言語は「怠lazに」実装されるという標準を採用しています。



データを操作および管理する方法に直面するとき、可能性は無限です。 そして、プログラミングでデータセットを操作するあらゆる方法を学び、作成するのは楽しいプロセスです。 Rubyには、 列挙可能な各メソッドのサンプルが十分に文書化されているため、それらから学ぶのに役立ちます。 プログラミングプロセスをより楽しく、より楽しくするのに役立つ多くの新しいことを実験し、発見することをサポートします。



All Articles