yield、Enumerable、リフレクション

ブロック付きメソッドの作り方を説明します。

eachメソッド

ブロック付きメソッドの代表格であるeachが何をしているかを考えてみます。

[10,20,30].each do |x|
  print x, "\n"
end

これを実行すると

10
20
30

と表示されます。 メソッドeachは何をしているのでしょうか

  • 配列の要素から10を取り出し、10をパラメータxに代入してブロックを実行する
  • 配列の要素から20を取り出し、10をパラメータxに代入してブロックを実行する
  • 配列の要素から30を取り出し、10をパラメータxに代入してブロックを実行する

ブロックはメソッドのようなものですから、順に10,20,30を引数にブロックを呼び出していることになります。 eachの動作をプログラムにすると、およそ次のようなものになります。

x = 10
while x <= 30
  print x, "\n" # iを引数にブロックを実行
  x += 10
end

あるいは、whileループを使わなくても

x=10; print x, "\n" # 10を引数にブロックを実行
x=20; print x, "\n" # 20を引数にブロックを実行
x=30; print x, "\n" # 30を引数にブロックを実行

でもeachの動作を表すことができます。

「xを引数にブロックを実行」という命令は、Rubyではyield(x)と書きます。 つまり、「yieldはブロックを呼び出す命令」です。 yieldにはパラメータをつけることができます。

eachとyieldの実例

ここでは、ユーザデータのオブジェクトを考えてみます。 そのオブジェクトには

  • ユーザ番号
  • ユーザ名
  • メールアドレス
  • 誕生日

を記録することにします。 ユーザ番号はオブジェクト生成時に一意になるような番号を自動的に振ることにし、書きかえはできないようにします。 その他のデータは書き換え可能にします。

class User
  @@count = -1
  attr_reader :id
  attr_accessor :name, :email, :birth_date
  def initialize
    @id = @@count += 1
  end
  def each
    yield("id", @id)
    yield("name", @name)
    yield("email", @email)
    yield("birth_date", @birth_date)
  end
end

user = User.new
user.name = "Toshio Sekiya"
user.email = "abcdefg@example.com"
user.birth_date = "YYYY/MM/DD"
user.each{|k,v| print "#{k}: #{v}\n"}
  • @@countのように、@が2つついた変数は「クラス変数」という。 クラス変数はクラスに保存されていて、そのインスタンスからアクセス可能(共有することになる)
  • @@countは-1に初期化された後には、インスタンスが生成されるたびに(initializeメソッドで)1だけ増やされていく
  • attr_reader :idは読み出しのみ可能なインスタンス変数@idを定義する(前回の記事で説明済み)
  • attr_accessor :name, :email, :birth_dateは読み書き可能な変数@name、@email、@birth_dateを定義する(後述)。
  • eachメソッドでは、@idから@birth_dateまでの「変数名と値」を引数にyieldを使ってブロック実行している
  • userにUserのインスタンスを代入
  • 名前、email、誕生日を代入
  • eachメソッドで、変数名と値をプリント

attr_accessorは次のプログラムと同等の働きをします。

def name
  @name
end
def name=(s)
  @name = s
end
... ... ...
以下emailbirth_dateも同様

最後の行でeachを呼び出し、呼ばれたeachの中でyieldがブロックが呼ぶ、という複雑さは慣れないとわかりにくいと思います。 繰り返し流れを追って、理解してください。

なお、例からわかるように、yieldのパラメータの数とブロックのパラメータの数は一致していなければなりません。

EnumerableモジュールとEnumeratorクラス

eachメソッドから様々なメソッドを作り出すことができます。 例えば、Userクラスにmapメソッドを定義するには次のようにします。

# eachからmapを作る例
class User
  def map
    a = []
    each do |k, v|
      a << yield(k, v)
    end
    a
  end
end

p user.map{|k,v| [k,v]}.to_h

class User〜endでUserクラスの定義を追加しています。 このように、クラス定義は何度でもできます(このことから既存のクラスにもメソッド追加が可能です)。

mapの定義ではeachメソッドだけを使っていることがわかります。 最後の1行では、mapを使って「項目名とその値の配列」の配列を作り、更にハッシュに変換しています。 実行すると次のようにハッシュの中身が表示されます。

{"id"=>0, "name"=>"Toshio Sekiya", "email"=>"abcdefg@example.com", "birth_date"=>"YYYY/MM/DD"}

map以外にも

  • inject たたみこみ演算
  • find 検索
  • sort 整列。ただし各要素に<=>が定義されていることが必要
  • select 検索して一致する要素すべての配列を返す

など様々なメソッドがeachだけから作成可能です。 このようなメソッドを集めたモジュールがEnumerableです。

UserクラスがEnumerableをインクルードすれば、mapなどを定義しなくても使えるようになります。

UserクラスがEnumerableをインクルードしていなくても、mapなどを使えるようにする別の方法があります。 それはEnumeratorというラッパークラスを使う方法です。

user.to_enumによって、userオブジェクトを元にしたEnumeratorオブジェクトを作ることができます。 なお、to_enumはObjectクラスで定義されたインスタンス・メソッドなので、すべてのクラスはObjectの子孫ですから、to_enumを持っています。 中身はuserなのですが、EnumeratorオブジェクトはEnumerableモジュールをインクルードしているので、mapなどのメソッドを使うことができます。

p user.to_enum.map{|k,v| [k,v]}.to_h

実行すると

{"id"=>0, "name"=>"Toshio Sekiya", "email"=>"abcdefg@example.com", "birth_date"=>"YYYY/MM/DD"}

さきほどと同じハッシュが表示されます。 まとめると、eachを定義してあるクラスには

  • Enumerableモジュールをインクルードするとmapなどの様々なメソッドが使えるようになる
  • to_enumでEnumeratorオブジェクトにしてもmapなどの様々なメソッドが使えるようになる

ということです。

引数の展開、block_given?メソッド

Userクラスをリファクターしましょう。 最も問題なのはyieldを@idから@birth_dateまで個別に行っていることです。 もしUserの項目を追加したり削除したりすると、この部分も変更しなければなりません。 そこで、attr_accessorの引数にする項目名(変数名)を配列で保持し、その配列を使ってyieldするようします。 これにより、項目名の変更は配列の変更だけで済みます。

もうひとつは、eachメソッドが引数なしで呼ばれた時にEnumeratorオブジェクトを返すオプションをつけます。

class User
  include Enumerable

  @@count = -1
  @@accessors = ["name", "email", "birth_date"]
  attr_reader :id
  attr_accessor *@@accessors
  def initialize
    @id = @@count += 1
  end
  def each
    if block_given?
      yield("id", @id)
      @@accessors.each do |a|
        yield(a, eval("@#{a}"))
      end
    else
      self.to_enum
    end
  end
  def to_h
    map {|k, v| [k.to_sym, v]}.to_h
  end
end

user = User.new
user.name = "Toshio Sekiya"
user.email = "abcdefg@example.com"
user.birth_date = "YYYY/MM/DD"
user.each{|k,v| print "#{k}: #{v}\n"}
user1 = User.new
user1.name = "Jeaou Robinson"
user1.email = "xyz@example.co.uk"
user1.birth_date = "yyyy/mm/dd"
user1.each{|k,v| print "#{k}: #{v}\n"}

p user.to_h
p user1.each
  • Enumerableモジュールをインクルードすることにより、mapなどのメソッドが追加される
  • @@accessors配列を、attr_accessorの引数を要素にして作成する。 要素はシンボルでなく文字列を使う(attr_accessorの引数はシンボル、文字列の両方が可)
  • attr_accessorの引数に配列を直接与えることはできない。 配列の要素を展開するために、配列の前にアスタリスク(*)をつける。 一般にメソッド呼び出しm(a,b,c)m(*[a,b,c])は同じになる
  • block_given?はそのメソッド(上のプログラムではeachメソッド)がブロック付きで呼ばれればtrue、そうでなければfalseを返す
  • ブロック付きならば、@idは個別にyieldし、@@accessorsの各項目についてはeachで繰り返しyieldする
  • yieldの第2引数はその変数の値なので項目名の前に@をつけてインスタンス変数名にし、evalで値を取得している。 evalは与えられた文字列をRubyコードとして実行するメソッド
  • ブロックがなければ、to_enumメソッドでEnumeratorオブジェクトを返す
  • to_hメソッドで各項目名とその値を組みとするハッシュを返す

このプログラムを実行すると

id: 0
name: Toshio Sekiya
email: abcdefg@example.com
birth_date: YYYY/MM/DD
id: 1
name: Jeaou Robinson
email: xyz@example.co.uk
birth_date: yyyy/mm/dd
{:id=>0, :name=>"Toshio Sekiya", :email=>"abcdefg@example.com", :birth_date=>"YYYY/MM/DD"}
#<Enumerator: #<User:0x00007f1f7a737be8 @id=1, @name="Jeaou Robinson", @email="xyz@example.co.uk", @birth_date="yyyy/mm/dd">:each>

と表示されます。 最後の2行でto_hメソッドと引数なしeachメソッドが期待通りに動いていることが確認できます。

リフレクション

ユーザを表現するオブジェクトはインスタンス変数で各項目を表す方法(この記事のUserクラスのように)とハッシュを使う方法が考えられます。 ハッシュを使う場合は

toshio = {}
toshio[:id] = 0
toshio[:name] = "Toshio Sekiya"
toshio[:email] = "abcdefg@example.com"
toshio[:birth_date] = "YYYY/MM/DD"

p toshio #=> {:id=>0, :name=>"Toshio Sekiya", :email=>"abcdefg@example.com", :birth_date=>"YYYY/MM/DD"}

となります。

これらの違いは何でしょうか?

  • ハッシュでは項目名がハッシュのキー名
  • Userクラスは項目名が変数名

ハッシュは実行しているプログラムの扱う対象です

  • キー名の一覧を取り出すことができる(keysメソッド)
  • キーと値の組を追加できる([ ]=メソッド)
  • キーから値を取得できる([ ]メソッド)
  • キーと値の組を削除できる(deleteメソッド)

これらと同等のことはC言語でも行うことができます。

struct hash { char *key; char *value; };

この構造体をリストでつなげてRubyのハッシュと同等のデータ構造を実現し、それをコントロールする関数を定義すれば良いのです。 実装が面倒ですが、可能だということです。

これに対して変数名は基本的に実行しているプログラムの扱う対象ではありません。

  • 変数名の一覧を取り出す
  • 変数を追加する
  • 変数名からその値を取得する
  • 変数を削除する

C言語を例に取ると、変数の管理はコンパイラがシンボルテーブルで行うのであって、実行プログラムは管理しません。 したがって、上記のような操作、特に変数の追加と削除(これはシンボルテーブルへの追加と削除を意味する)は実行プログラムでは不可能です。

Rubyではどうでしょうか? Rubyにはevalなどがあるので実行プログラムからこれらを行うことができます。

  • 変数名の一覧を取り出す(instance_variablesメソッドなど)
  • 変数を追加する(attr_accessorメソッドをクラスに適用など)
  • 変数名からその値を取得する(evalメソッド)
  • 変数を削除する(remove_instance_variableメソッド)

このように、Rubyでは実行プログラムがRubyの状態(変数のシンボルテーブルなど)を知ることができ、アクセスもできます。 これを「リフレクション」といいます。 レフレクションを使って更にUserクラスのプログラムを書き直してみましょう。

class User
  include Enumerable

  @@count = -1
  attr_reader :id
  attr_accessor :name, :email, :birth_date
  def initialize
    @id = @@count += 1
  end
  def each
    if block_given?
      instance_variables.each do |iv|
        yield(iv.to_s.slice(1..-1), eval(iv.to_s))
      end
    else
      self.to_enum
    end
  end
  def to_h
    map {|k, v| [k.to_sym, v]}.to_h
  end
  def show
    each do |k, v|
      print "#{k}: #{v}\n"
    end
  end
end

user = User.new
user.name = "Toshio Sekiya"
user.email = "abcdefg@example.com"
user.birth_date = "YYYY/MM/DD"
user1 = User.new
user1.name = "Jeaou Robinson"
user1.email = "xyz@example.co.uk"
user1.birth_date = "yyyy/mm/dd"

User.attr_accessor(:location)
user.location = "Japan"
user.show
user1.show
  • @@successors変数は使わない。 eachメソッドの定義では、代わりにinstance_variablesメソッドでインスタンス変数の一覧を取り出している
  • showメソッドは定義されている変数と値の一覧を表示
  • 下から4行目はattr_accessorメソッドをUserクラスに対して実行して@location変数を読み書き可で追加している
  • 次の行でuserオブジェクトにlocationを追加
  • userオブジェクトを表示(locationまで表示される)。 eachメソッドでinstance_variablesを使った効果が現れている
  • user1オブジェクトを表示(@locationが定義されていないので、locationは表示されない)。 詳しく説明すると、User.attr_accessor(:location)は@locationの参照と代入のメソッドを定義しているだけで、@location自身を定義しているのではない。 @locationは初めて代入されるときに同時に定義される。 例えば、userオブジェクトで@locationが定義されたのは、user.location = "Japan"が実行されたときである。 user1では@locationの代入は行われていないので未定義である

実行すると次のようになります。

id: 0
name: Toshio Sekiya
email: abcdefg@example.com
birth_date: YYYY/MM/DD
location: Japan
id: 1
name: Jeaou Robinson
email: xyz@example.co.uk
birth_date: yyyy/mm/dd

Userクラスはいじると面白いのですが、実用上はどうなのでしょうか? ハッシュを使うほうがプログラマーにとって易しいので、保守性も高いような気がします。 わざわざ難しくするのもどうなのか? ただ、アクセサーの構文(user.location = “Japan”など)は読みやすく分かりやすいですね。 一長一短かもしれません。

リフレクションについて書いておいてこういうのも何ですが、

リフレクションを使い過ぎて難しくしてはいけません

プログラムはそもそもやっかいで面倒なもの。 余計な難しさは余計な時間を費やすことになります。

2023

単項マイナスと構文解析

1 minute read

単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合

Raccライブラリと構文解析

3 minute read

パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ

StrScanライブラリと字句解析

less than 1 minute read

StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例

Gem

1 minute read

lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて

Encoding

1 minute read

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

Thread

less than 1 minute read

Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。

Fiber

1 minute read

Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。

RDoc

less than 1 minute read

今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。

Back to Top ↑

2022

Ruby/GTK4

5 minute read

Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...

Shoes – Rubyとグラフィック

5 minute read

Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。

Rails7 テスト

5 minute read

前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。

Rails7 モデルとデータベース

2 minute read

今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。

Rails7とBootstrap

2 minute read

一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。

Rails7のインストール

2 minute read

Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...

GemとBundler

1 minute read

Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。

minitest(3)モックの詳細

2 minute read

minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...

minitest(1)テストとは

2 minute read

アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。

public、private、protected

2 minute read

今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。

アプリ制作、インストール、テスト

1 minute read

2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。

case文

2 minute read

if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。

Lambda

1 minute read

Procオブジェクトを生成するメソッドlambdaについて説明します。

Proc オブジェクト

2 minute read

今回はブロックを一般化したオブジェクトProcを説明します。

モジュール

1 minute read

モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。

Kernelモジュール

less than 1 minute read

Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。

便利なメソッド

1 minute read

ここでは私が便利だと思ったメソッドを紹介します。

文字列と正規表現

3 minute read

文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。

配列

2 minute read

配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。

トップレベルのメソッド

1 minute read

今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。

ブロックとイテレータ

less than 1 minute read

ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。

整数

less than 1 minute read

ここではRubyの最も基本的なオブジェクトである整数について説明します。

Hello world

less than 1 minute read

「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。

Back to Top ↑