Skip to content

Instantly share code, notes, and snippets.

@cesare
Created February 3, 2016 18:17
Show Gist options
  • Save cesare/eeb0257702942348eb8b to your computer and use it in GitHub Desktop.
Save cesare/eeb0257702942348eb8b to your computer and use it in GitHub Desktop.

Enumerable 再訪

Enumarable って?

よく使う Enumerable なメソッド

※分類は主観が入ってます

  • 写像系
    • map
  • 折畳み系
    • reduce, inject
    • each_with_object
  • 順番に処理系
    • each_with_index
  • 探しもの系
    • select
    • find
    • grep
    • reject
  • 判定系
    • member?, include?
    • any?
    • all?
  • 並べ替え系
    • sort
    • sort_by
  • 部分集合系
    • take
    • drop
    • first
    • last
    • take_while
    • drop_while
  • 融合系
    • zip

Enumerable なクラス

Enumerable なクラスを Enumerable なメソッドを使って探してみる。

# 下準備

# クラス階層のトップレベル定義されている定数群
Object.constants  # => シンボルの配列が返る

# 定数の実体を取る方法
Object.const_get :String  # => String のクラスが返る

# 継承しているクラス or include しているモジュールを調べる
String.ancestors
# => [String, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]

# Enumerable なクラスを探す

# トップレベルにある定数の実体
Object.constants
  .map {|name| Object.const_get(name) }

# 定数の中からクラスだけを取る
Object.constants
  .map {|name| Object.const_get(name) }
  .select {|obj| obj.is_a? Class }

# クラス群から Enumerable を include しているものを探す
Object.constants
  .map {|name| Object.const_get(name) }
  .select {|obj| obj.is_a? Class }
  .select {|clazz| clazz.ancestors.include? Enumerable }
# => [Array, Hash, Struct, Range, IO, File, Dir, Enumerator, StringIO, Slop]
  • 配列・リストっぽいやつはだいたい Enumerable
  • IO もだいたい Enumerable
$stdin.is_a? Enumerable
# => true

$stdin.map(&:upcase)
foo
bar
baz
# => ["FOO\n", "BAR\n", "BAZ\n"]

Enumerable Tips

ブロックに渡される引数の形を調べたいときに便利な方法。

# 例えばハッシュの場合
hash = {foo: 'bar', hoge: 'fuga'}
hash.map(&:itself)
# => [[:foo, "bar"], [:hoge, "fuga"]]

Hash の場合、[key, value] という形で渡されることが分かる。

(※) Ruby-2.2.x 以上が必要

ブロックに渡される引数の分解

hash.map do |pair|
  # Hash の場合、map のブロックに渡されるのは [key, value] の形なので
  key = pair.first
  value = pair.last
end

# ↑これを、こう書ける
hash.map do |key, value|
end

# each_with_* メソッドだとこんな感じ
hash.each_with_index do |(key, value), index|
end

Enumerable なクラスを自作する方法

  • include Enumerable
  • each メソッドを実装する
  • 以上
class Foo
  include Enumerable

  def each
    # 頑張って実装
  end
end

each メソッドとは

  • Enumarable モジュールのメソッドではない
  • Enumerable になりたいクラスが実装するもの
  • TemplateMethod パターン

インターフェイス仕様はこんな感じ。

  • ブロックを渡すと順番に要素を渡してブロックを実行
  • ブロックを渡さない場合は Enumerator インスタンスを返す

Enumerator とは

  • 次の値を一つだけ返す #next メソッドを持っている
  • 返す要素が残っていない状態で #next を呼ぶと StopIteration 例外
enum = [1,2,3].each
enum.next  # => 1
enum.next  # => 2
enum.next  # => 3
enum.next  # StopIteration: iteration reached an end

自作 each で手軽に Enumerator を返せるようにするテンプレート

class Foo
  include Enumerable

  def each(&block)
    return to_enum unless block_given?

    # 以下、要素を順にブロックに渡して実行させる実装を頑張る
  end
end

ケーススタディ1: 等比数列ジェネレータ

class GeometricSequence
  include Enumerable

  def initialize(first, common_ratio)
    @first = first  # 初項
    @common_ratio = common_ratio  # 公比
  end

  def each(&block)
    return to_enum unless block_given?

    next_value = @first
    while true do
      yield next_value
      next_value *= @common_ratio
    end
  end
end

seq = GeometricSequence.new(1, 3)
seq.take(10)
# => [1, 3, 9, 27, 81, 243, 729, 2187, 6561, 19683]

enum = seq.each
enum.next  # => 1
enum.next  # => 3
enum.next  # => 9
enum.next  # => 27

ケーススタディ2: あるディレクトリ配下のファイルを全部列挙するクラス

class DirTree
  include Enumerable

  attr_reader :current_path

  def initialize(dir)
    @current_path = dir
  end

  def each(&block)
    return to_enum unless block_given?

    Dir.new(current_path).each do |entry|
      next if entry == '.' || entry == '..'

      path = File.join(current_path, entry)
      if File.directory?(path)
        self.class.new(path).each(&block)
      else
        yield path
      end
    end
  end
end

DirTree.new('.').map(&:itself)

怠惰な Enumerable

  • 遅延評価
  • 必要になるまで値を計算しない
# DirTree での例

# ruby のファイルがありそうな場所を探したい; 最初に見つかったところで ok
DirTree.new('/home/cesare/git')
  .select{|name| name =~ /\.rb$/ }
  .take(3)  # 当分の間、返ってこない

# 遅延評価を使う
DirTree.new('/home/cesare/git').lazy
  .select{|name| name =~ /\.rb$/ }
  .take(3)
  .force  # 3件見つかったら速やかに返ってくる
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment