おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(16)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
... ... ...
以下emailとbirth_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オブジェクトを作ることができます。 中身は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"など)は読みやすく分かりやすいですね。 一長一短かもしれません。

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

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

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

徒然Ruby(15)モジュール

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

モジュールの定義

モジュールの定義とクラスの定義は似ています。 クラスの定義にはclassキーワードを使いますが、モジュール定義ではその代わりにmoduleキーワードを使います。

module Abc
  def abc
    print "abcdefg\n"
  end
end

モジュール名はAbcです。 モジュール名は定数で、最初の文字は大文字でなければいけません。 モジュールはクラスと違い、インスタンスを作ることはできません。 もし、Abc.newとしてインスタンスを作ろうとするとエラーになります。

インクルード

先程の例ではインスタンスメソッドabcをモジュールAbcの中で定義しました。 しかし、モジュールではインスタンスを作れないのですから、このメソッドは使いようがないように見えます。 メソッドを使うには、includeメソッドでモジュールを取り込む必要があります。

include Abc
abc

実行すると

abcdefg

と画面に現れます。 つまり、モジュールのメソッドabcを実行できたのです。

モジュールをインクルードすることにより、モジュールのメソッドを引き継ぐことができます。 通常はクラス定義の中でモジュールをインクルードしてメソッドを引き継ぎます。

class Xyz
  include Abc
  def xyz
    abc
  end
end

x = Xyz.new
x.abc
x.xyz

これを実行すると、クラスXyzでモジュールAbcがインクルードされることにより、モジュールのメソッドabcがクラスXyzのインスタンスをレシーバとして呼ぶことができるようになります。 プログラムを実行するとabcdefが2つ表示されます。

abcdef
abcdef
  • Xyzクラスのインスタンスxを生成
  • abcXyzインスタンスメソッドになったから、x.abcによってabcdefが表示される
  • インスタンスメソッドxyzの中でインスタンスメソッドabcを呼び出せる。 def xyz〜endでは、selfが指すのはxyzが呼び出されたときのレシーバ(インスタンス)である。 そして、メソッド定義の中でメソッド呼び出し(abc)があり、そのレシーバが省略されたときは、selfをレシーバとする。 プログラムリストの最後の行で、x.xyzが呼び出されたとき、その中で呼び出されたabcのレシーバはxになる(selfはxになっている)

なお、includeはメソッド定義の中では使えません。 クラス定義の中で、メソッド定義の外でインクルードしてください。

ミックスイン

これまでのところをまとめておきましょう。

このことから、モジュールの目的(のひとつミックスイン)はクラスのインスタンスメソッドを提供することです。 しかしここで次のような疑問が浮かびます。 最初からクラス内でメソッドを定義すればインクルードなど必要ないのに、なぜわざわざモジュールのメソッドを取り込むのでしょうか?

モジュールは特定のクラスのためにメソッドを定義しているのではなく、複数のクラスに提供するためにメソッドを定義しているのです。 例えば、Comparableという組み込みのモジュールがあります。

  • Comparableは<=>メソッドをもとに、==>>=<<=between?clampというメソッドを定義している
  • Comparableをインクルードするクラスには<=>メソッドが定義されていなければならない

Comparableは比較可能なオブジェクトである整数、実数、文字列などに適用されています。 また、ユーザが比較可能なクラスを作るとき、<=>メソッドを定義し、Comparableをインクルードするだけで、==などの7つのメソッドが使えるようになります。

例として、江ノ電の駅のオブジェクトを作り、Comparableをインクルードしてみましょう。 駅には駅番号があり、藤沢のEN01から鎌倉のEN15までとなっています。 この駅番号の大小で駅の大小を決めることにします。

class Enoden
  include Comparable

  attr_reader :station_number, :name
  def initialize station_number, name
    @station_number = station_number
    @name = name
  end
  def <=>(other)
    self.station_number.slice(2..3).to_i <=> other.station_number.slice(2..3).to_i
  end
  def to_s
    "#{@station_number}: #{@name}"
  end
end

station_number = (1..15).map{|i| sprintf("EN%02d", i)}
name = "藤沢 石上 柳小路 鵠沼 湘南海岸公園 江ノ島 腰越 鎌倉高校前 七里ヶ浜 稲村ヶ崎 極楽寺 長谷 由比ヶ浜 和田塚 鎌倉".split(/ /)

enoshima = Enoden.new(station_number[5], name[5])
inamuragasaki = Enoden.new(station_number[9], name[9])
kamakura = Enoden.new(station_number[14], name[14])

print enoshima, "\n"
print inamuragasaki, "\n"
print enoshima < inamuragasaki, "\n"
print [kamakura, enoshima, inamuragasaki].sort.map{|a| a.to_s}, "\n"

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

EN06: 江ノ島
EN10: 稲村ヶ崎
true
["EN06: 江ノ島", "EN10: 稲村ヶ崎", "EN15: 鎌倉"]

プログラム中のattr_readerメソッドは読み出しのみサポートするインスタンス変数を定義します。 次のプログラムと同等です。

def station_number
  @station_number
end
def name
  @name
end

プログラムを説明します。

  • クラスEnodenはモジュールComparableをインクルードする
  • attr_readerメソッドによって、読み出しのみ可のインスタンス変数@station_number@nameを定義
  • オブジェクトを生成する時に、引数に駅番号、駅名を与える
  • 大小比較<=>は駅番号文字列の3〜4文字目を(sliceは0から数えるので2..3となっている)整数に直して比較
  • to_sメソッドは駅番号と駅名を文字列として返す
  • 駅番号の文字列の配列を作成
  • 駅名の文字列の配列を作成
  • 江ノ島稲村ヶ崎、鎌倉のEnodenオブジェクトを作成
  • enoshimaとinamuragasakiを(to_sを使って)プリント
  • enoshimaとinamuragasakiの大小比較。稲村ヶ崎の駅番号の方が大きいのでtrueが返る
  • kamakura, enoshima, inamuragasakiの配列を作り、ソート。 <=>が定義してあるので、ソートが可能。 to_sで駅番号、駅名の文字列に直して表示

今回の例では江ノ電だけを対象にしましたが、一般に日本の駅には駅番号(駅ナンバリング)が用いられるようになってきました。 駅ナンバリングは3字以内の英字(路線)と番号でできています。 これから、Enodenクラスをより一般的な駅を表すStationクラスに格上げすることも可能だと思います。 駅の比較は(1)英字をアルファベットで大小をつけ(2)番号で大小をつけるという2段階で実現できます

さて、具体的にComparableモジュールがクラスにインクルードされ、その実態(オブジェクト)を手に入れるのを見てきました。 比較可能なクラスは様々あり、それらすべてにComparableモジュールが適用できます。 このように、クラス共通のメソッドを定義したモジュールを、各クラスに適用することをミックスインといいます。

Comparableは既存のモジュールですが、ユーザが独自にモジュールを作り、複数のクラスに適用することもできます。 また、Comparable以外にもEnumerableという非常に有用なモジュールがあります。 このモジュールはeachメソッドのあるクラスに適用できます。 eachメソッドを作るにはブロック付きメソッドの定義を知らなければならないので、今回は解説しませんでしたが、別の記事で取り上げられればと思っています。

徒然Ruby(14)サブクラス

クラスの親子関係

Rubyのクラスには親子関係があります。 あるクラスの子をサブクラスといいます。 サブクラスは複数作ることができますが、親クラス(スーパークラス)を子から作ることはできません。 したがって、この親子関係はツリー状になります。

すべてのクラスの祖先はBasicObjectというクラスです。 あるクラスがどういう祖先を持っているかはancestorsメソッドで分かります。

p String.ancestors

これを実行すると

[String, Comparable, Object, Kernel, BasicObject]

と表示されます。 左から右へ「子から親」の順に並んでいます。 このうちComparableとKernelはクラスではなくモジュールです。 モジュールについては別の記事で説明します。 クラスだけの親子で言えば

String => Object => BasicObject

ということになります。 以下では子クラスを「サブクラス」、親クラスを「スーパークラス」と呼びます。

サブクラスはスーパークラスの性質、例えばメソッド、インスタンス変数を受け継ぎます。

親から子になるにつれ、より個別的、具体的なクラスになっていきます。

サブクラスの定義

サブクラスの定義は<をクラス名の後につけ、さらにスーパークラスを置きます。

class A < B
end

この例ではAがBのサブクラス、BがAのスーパークラスです。 BはRubyが既に定義しているクラスでも、ユーザが新たに定義したクラスでも構いません。

サンプルデータの作成

ここではサブクラスの例として統計のサンプルデータの集合を表すクラスを作ってみましょう。

今回は都道府県別人口のデータを処理するクラスを作ってみます。 データは独立行政法人統計センターのSSDSE(教育用標準データセット)から取ってきました。

2022/9/22の時点では「SSDSE-E-2022v2」(2022年第2版)が最新です。

データの中から都道府県名と人口をセットにして、ヒア・ドキュメントの文字列にしました。

北海道 5224614
青森県 1237984
... ...

都道府県と人口の間はタブで区切られています。 このデータをハッシュにして取り出すには

  • 改行を区切りとして各行を要素とする配列にする
  • 各要素をタブを区切りとして、都道府県名と人口の2要素の配列にする
  • その2要素の配列を文字列からシンボル、整数に変更する
  • 配列をハッシュに変換する

それぞれの項目はメソッドで実現できます。 全体として連続したメソッドになりますが、これを「メソッド・チェーン」ということもあります。 ここまでをプログラムにしましょう。 少し長くなりますが、全都道府県の人口データもプログラムリストに入れておきます。

d = <<EOS
北海道 5224614
青森県 1237984
岩手県 1210534
宮城県 2301996
秋田県 959502
山形県 1068027
福島県 1833152
茨城県 2867009
栃木県 1933146
群馬県 1939110
埼玉県 7344765
千葉県 6284480
東京都 14047594
神奈川県  9237337
新潟県 2201272
富山県 1034814
石川県 1132526
福井県 766863
山梨県 809974
長野県 2048011
岐阜県 1978742
静岡県 3633202
愛知県 7542415
三重県 1770254
滋賀県 1413610
京都府 2578087
大阪府 8837685
兵庫県 5465002
奈良県 1324473
和歌山県  922584
鳥取県 553407
島根県 671126
岡山県 1888432
広島県 2799702
山口県 1342059
徳島県 719559
香川県 950244
愛媛県 1334841
高知県 691527
福岡県 5135214
佐賀県 811442
長崎県 1312317
熊本県 1738301
大分県 1123852
宮崎県 1069576
鹿児島県  1588256
沖縄県 1467480
EOS

d = d.split(/\n/)\
     .map{|s| s.split(/\t/)}\
     .map{|a| [a[0].to_sym, a[1].to_i]}\
     .to_h

最後から2〜4行目の最後にバックスラッシュがあります。 これは改行を取り消すためのもので、次の行も同じ一行にくっつけるという意味です。 Rubyでは改行が空白よりも強い区切り(文の区切り)であるので、これをつけておきます。 ただし、これは一般論であり、上記のようなメソッドチェーンではバックスラッシュを取り去ってもRubyは同じように解釈してくれます。 好みの問題ですが、バックスラッシュをつけるほうが文のつながりを意識できて良いと思います。 to_hメソッドは配列をハッシュに変換します。 変換するには配列の内容がハッシュに対応するものでなければなりません。 詳細はRubyのドキュメントのArrayを参照してください。

結果としてdは

d = {:北海道=>5224614, :青森県=>1237984, ... ... ... , :沖縄県=>1467480}

となります。

Statクラス

ハッシュのサブクラスStatを統計処理のために作ります。 Statの中身はハッシュと同じですが、次のようなメソッドを作ります。

  • 値の検索(キーと値の組からなるハッシュを返す)
  • 最大の値をもつ組の検索
  • 最小の値をもつ組の検索
  • 平均値の算出
  • 昇順ソート
  • 並びを逆順にする

最初の3つは検索結果をStatオブジェクトで返します。 平均値は小数点以下第1位までに丸めた実数を返します。 最後の2つは並び替えられたStatオブジェクトを返します。

これらの機能はハッシュにはないものです。

class Stat < Hash
  def initialize h={}
    super()
    update(h)
  end
  def find_by_value(v)
    Stat.new(select{|key, value| value == v})
  end
  def max
    m = map{|k,v| v}.max
    find_by_value(m)
  end
  def min
    m = map{|k,v| v}.min
    find_by_value(m)
  end
  def average
    (map{|k,v| v}.sum.to_f/size).round(1)
  end
  def sort
    Stat.new(super{|a,b| a[1]<=>b[1]}.to_h)
  end
  def reverse
    Stat.new(to_a.reverse.to_h)
  end
end

このプログラムで注意真ければならないのはselfという特別な変数(疑似変数)です。 selfは「その」オブジェクト自身を表します。

def abc
  self.clear
  clear
end

メソッドabcの定義の中のselfとは、次のようなオブジェクトです。 例えばそのオブジェクトが変数xxに代入されていて、xx.abcが実行されたとき、selfは変数xxの指すオブジェクトになります。 同じクラスの別のオブジェクトが変数yyに代入されていて、yy.abcが実行されたときはselfyyの指すオブジェクトになります。

ですので、selfは固定されたオブジェクトではなく、メソッドabcが呼ばれたときのabcが属するオブジェクトになります。 このように、メソッドにはかならずそのメソッドとセットになったオブジェクトがあり、そのオブジェクトをメソッドのレシーバーとも言います。 ですから、「selfはメソッドabcのレシーバーを表す」とも言えます。

1行目はselfを空のハッシュにします。 2行目はレシーバーが書かれていません。 メソッド名だけです。 「メソッド名だけでメソッドが呼ばれたときのレシーバーは、selfとする」という約束があります。 ですから2行目と1行目は同じ結果をもたらします。

それでは、Statクラスの定義を説明しましょう。 クラス定義の中にはレシーバが省略されたメソッドが多数出てきますので注意してください。

  • StatはHashのサブクラス(class Stat < Hashの部分がそれを表す)
  • インスタンスを生成するStat.newが呼ばれたとき、initializeメソッドが初期化をする。 h={}というパラメータがあるので、newメソッドには引数をつけることができる。 引数が与えられなかったときのデフォルト値は{}、つまり空のハッシュ。 super()はこのメソッドのスーパークラスにおける同名のメソッドを呼び出す。 つまりHashのinitializeメソッドが呼び出される。 したがって、super()は空のハッシュ・インスタンスを生成、初期化する。 updateはレシーバが省略されたメソッドなので、selfをレシーバとして実行される。 updateはHashのメソッドで、引数を破壊的に付け加える。 以上から、Stat.newにハッシュが引数で与えられたときは、そのハッシュを中身とするStatオブジェクトが生成される。 引数がなければ空のStatオブジェクトが生成される
  • find_by_valueにはパラメータvがある。 vはハッシュの値(都道府県データでは人口を表す整数)のどれかであることが期待されている。 その値をもつキーと値の対からなるStatオブジェクトを返す。 selectメソッドはブロックの値が真になるような対だけからなるハッシュを返す。
  • maxは、まず値だけの配列をmapメソッドで作り、その最大値を(配列の)maxオブジェクトで取り出す。 最後にfind_by_valueでStatオブジェクトを返す。
  • minは同じことを最小値について行う
  • averageでは、Statの値だけからなる配列を作り、その合計を計算し、実数に変換、要素数で割って平均を求め、最後にroundで小数点以下一位に丸める
  • sortでは、Hashのsortメソッドをsuperで呼び出し、大小をブロックで評価してソートする。
  • reverseでは、ハッシュを配列に変換(各「キーと値」はその2つを要素とする配列に変換される)、reverseメソッドで逆順にし、ハッシュに戻す

説明は長くなってしまいましたが、これはメソッドチェーンでたくさんのことをコンパクトにプログラムしたからです。 逆にいえば、「Rubyでは短いプログラムで多くのことを実行できる」ということになります。

プログラムの実行

データの定義、Statクラスの定義に続けて、次のプログラムを書き加えます。

s = Stat.new(d)
p s.find_by_value(7344765)
p s.find_by_value(0)
p s.max
p s.min
p s.average
p s.sort
p s.sort.reverse

実行してみましょう。

{:埼玉県=>7344765}
{}
{:東京都=>14047594}
{:鳥取県=>553407}
2683959.6
{:鳥取県=>553407, :島根県=>671126, ... ... ... , :東京都=>14047594}
{:東京都=>14047594, :神奈川県=>9237337, ... ... ... , :鳥取県=>553407}
  • 都道府県別人口のデータをハッシュにしたdを用いてStatオブジェクトsを生成
  • 人口7344765の県を検索すると、埼玉県であった
  • 人口0の県を検索すると、そのような県はないので、空のStatオブジェクトが返される
  • 人口最大は東京都
  • 人口最小は鳥取県
  • 人口の平均値は2683959.6人
  • 人口の少ない順に並べると鳥取県・・・・東京都
  • それを逆順にし、人口の多い順に並べると東京都・・・鳥取県

クラスを作ったことにより、これらの統計処理がメソッドひとつで実行できるようになりました。 このクラスは都道府県人口以外の統計処理にも使えます。

また、標準偏差ヒストグラム(グラフィックの手段が必要)などもメソッドで用意すればもっと実用的になるでしょう。

今回はサブクラスを取り上げました。 StatクラスはHashのすべてのメソッドを使うことができるので、一から作るよりも少ない労力で作成することができました。 このようにサブクラスの仕組み(これを継承ともいう)はプログラムの資産を活かすことにつながるわけです。

徒然Ruby(13)演算子の再定義

Ruby演算子とその再定義について書きます。

Ruby演算子

Rubyのドキュメントによると、次のような演算子があります。 表の「意味」は私が付け加えたもので、正確ではなく「およそ」「良く用いられる場合」についての説明です。

優先度 演算子 意味
高い :: 「クラス::定数」など
[] 配列参照など
+(単項演算子) ! ~ 正符号、論理否定、ビット反転
** 累乗
-(単項演算子) 負符号
* / % 積、商、剰余
+ - 和、差
<< >> ビットシフト、追加
& ビット積
| ^ ビット和、排他的論理和
> >= < <= 大小比較
<=> == === != =~ !~ 比較、等、不等
&& 論理積「かつ」
|| 論理和「または」
.. ... 範囲
?: 条件演算子三項演算子
= (+=, -=など) 代入(自己代入)
not 論理否定
低い and or 論理積、和

これらの演算子のうち一部は糖衣構文によってメソッドに置き換えられます。 ということはそれらの演算子はオブジェクトごとに定義されているので、様々な意味付けがありえます。 表の中の「意味」はよく使われるオブジェクトに対する「意味」です。

糖衣構文

ここで、糖衣構文(シンタックス・シュガー)について少し詳しく見てみます。

プログラムの中で

a == b

が出てきたとします。 aとbは変数で、何らかのオブジェクトを指しています。 つまりオブジェクト == オブジェクトの形です。 このとき、この構文は次の構文と同じだとRubyが判断します。

a.==(b)

これはオブジェクトaの==メソッドを引数bをつけて呼び出すことに他なりません。 ですから、二項演算子==は実は左辺のオブジェクトのメソッド(メソッド名)になります。 その意味はオブジェクトごとに決まっています。 概念的には「等しい」ですが、オブジェクトごとに「等しい」の具体的な内容は異なっているわけです。

  • 文字列であれば、文字列の内容が等しい
  • 配列であれば、配列の各要素に対し==メソッドの結果が等しくなっている
  • 整数であれば、数値として等しい(同じ数値のオブジェクトはひとつしかないので、この場合はオブジェクトとしても同じ、すなわちオブジェクトIDが等しい)

また、違う種類のオブジェクトを==で比較すると、大抵の場合は「等しくない」という結果になります。 しかし、絶対異なるというわけでもなく、1.0==1は実数と整数という異なるオブジェクト間の比較ですがtrueになります。 これは、実数の==の定義の中で、引数が整数の場合はそれを実数に変換するなどの比較可能な形にして調べているからです(実装を確認したわけではない)。

代入=はメソッドではなく、再定義できません。 なぜなら、代入の左辺はオブジェクトではなく、変数だからです。 変数自身にはメソッドはありません。

a = 10
a = "abc"

1行目は変数aに整数10を代入しています。 2行目の左辺aは整数オブジェクト10を指してはいますが、代入先はそのオブジェクトではなく、変数a自身です。 aはオブジェクト10から切り離されて、新たに文字列オブジェクト"abc"を指すようになります。

これと似ていますが、次のプログラムは意味が異なります。

a.x = 10

a.xは変数ではありません。 変数aの指すオブジェクトのxメソッドの返した値になります。 ですから、この文では=が代入のイコールだとするとエラーになるはずです(代入はオブジェクトにはできない)。

しかし、エラーにならないこともあるのです。 それはオブジェクトにx=というメソッドが定義されている場合です。 この文は糖衣構文で次のように置き換えられます。

a.x=(10)

元の式ではa.x=の間に半角空白があったのですが、糖衣構文の適用でこの空白は消えてしまいます。 a.xではなくa.x=がひとまとまりです。 一般に空白は区切りを表しますが、これは例外ということになります。

左辺のオブジェクトにx=メソッドが定義されていなければエラーになり、つぎのようなメッセージが現れます。

undefined method `x=' for (左辺のオブジェクト)

エラーの内容は「x=メソッドが左辺のオブジェクトにない」です。 このことからも、糖衣構文の置き換えが行われた後にエラーが発生したことがわかります。

再定義できる演算子

次の演算子は再定義することができます。 再定義はオブジェクトのメソッドとして行います。

|  ^  &  <=>  ==  ===  =~  >   >=  <   <=   <<  >>
+  -  *  /    %   **   ~   +@  -@  []  []=  ` ! != !~

+@-@は単行演算子+-を表します。 メソッド定義をする場合はアットマークをつけた名前を使います。

これらの演算子の意味は、それぞれのオブジェクトのメソッドで確認します。 例えば、<<については各オブジェクトで次のような意味で使われます。

  • Integer(整数)では「左ビット・シフト」
  • Array(配列)では「破壊的な要素追加」
  • String(文字列)では「破壊的な文字列追加」
  • IO(入出力)では「オブジェクトを文字列化して出力」
  • Method(メソッド)では「メソッドを合成したProc」
  • Proc(手続き)では「手続きを合成したProc」

演算子そのものには確定した意味がないことに注意してください。 演算子の意味を決めるのは各オブジェクトのメソッド定義です。

逆に再定義できない演算子

=  (自己代入) ?:  ..  ...  not  &&  and  ||  or  ::

です。

=は再定義できませんが、data=のようにメソッド名の最後につけることは可能です。

これらの再定義できない演算子は言語の制御構造で使われます。 これらの演算子も使い方が一通りでない場合があります。 ドキュメントでは一箇所に演算子の意味をまとめて書いているのではなく、それぞれのトピックの中で説明されています。 つまりドキュメントのあちこちにばらばらに書かれています。

再定義の例

ここでは、2次元ベクトルのクラスを定義して、和と差を再定義してみましょう。

class Vec
  def initialize(x=0, y=0)
    @x = x
    @y = y
  end
  def x
    @x
  end
  def y
    @y
  end
  def +(other)
    Vec.new(@x+other.x, @y+other.y)
  end
  def -(other)
    Vec.new(@x-other.x, @y-other.y)
  end
  def to_s
    "(#{@x}, #{@y})"
  end
end

a = Vec.new(1,2)
b = Vec.new(-2,4)
print "#{a} + #{b} = #{a+b}\n"
print "#{a} - #{b} = #{a-b}\n"

Vecは2次元ベクトルを表すクラスです。 x成分、y成分をそれぞれ@x、@yのインスタンス変数に保持します。 このオブジェクトは一度生成された後は値を変えないことにしています。 このような変更不可のオブジェクトはイミュータブル(immutable)といいます。 変更可能はミュータブル(mutable)です。

オブジェクトを生成する時に、引数をつけます。 引数によってベクトルの各成分が決まります。

メソッドは、各成分を返す、和と差を計算して新たなVecオブジェクトを返す、オブジェクトを文字列にする、です。

演算子+と-を再定義しています。 この再定義のメソッドでは、otherというパラメータが演算の相手方を表しています。 otherはVecオブジェクトを想定しています。 それ以外のオブジェクトを引数にしてメソッドが呼ばれたときの対策ないので、エラーが起こります。 本当はその対策が必要ですが、ここではあくまで例ですので単純化しました。

to_sメソッドは文字列の式展開の中で(背後で)使われます。 もしこのメソッドを定義してなければ、式展開の結果が恐ろしいものになってしまうでしょう。

実行してみます。

(1, 2) + (-2, 4) = (-1, 6)
(1, 2) - (-2, 4) = (3, -2)

きちんと計算できていることがわかります。

演算子、とくに四則計算の演算子は数学用ですので、その他のオブジェクトを定義するときにはあまり使われないかもしれません。 しかし、文字列結合に+が用いられているように、演算子がそのメソッドのイメージに合うこともあるでしょう。 そのときには、ぜひ再定義してみてください。

徒然Ruby(12)クラスとインスタンス

今回からクラスとインスタンスを定義、生成する方法を説明します

クラス定義とインスタンスの生成

はじめはごく簡単な例から始めます。 クラスはclassキーワードを使って定義します。 また、クラス名には定数(最初の文字が大文字)を使います。

class Sample
end

a = Sample.new
b = Sample.new
p a.class
p a.class.ancestors
p a.object_id
p b.object_id
  • class〜endはSampleという名前のクラスを定義している。 このクラスは何も中身がない、最も簡単なクラス。 もちろん、普通は様々な定義をクラスの中に書く。
  • クラスにはnewメソッドがあらかじめ定義されている。 newメソッドはそのクラスのインスタンスを生成する。 通常はひとつのクラスから(newを使うたびに)複数のインスタンスを生成できる。
  • classメソッドをインスタンスに対して実行すると、そのインスタンスのクラスを返す。 pデバッグ用のコマンドで、引数を画面に表示し改行も加える。
  • ancestorsはクラスの親クラス、そのまた親クラスとたどっていき、それらの配列を返す。 このときクラスだけでなくモジュールも配列の要素に加える。 クラスには親子関係がある、ということを覚えておいてください。 詳しくは次回以降の記事で説明します
  • object_idメソッドはオブジェクト(インスタンス)に割り振られた番号(ID)を返す。 異なるオブジェクトには異なるIDが与えられる

実行すると次のように表示されます。

Sample
[Sample, Object, Kernel, BasicObject]
60
80

これから、Sampleクラスは、Objectクラス、Kernelモジュール、BasicObjectクラスをこの順に親(祖先)に持つことがわかります。 また、aとbは両方ともSampleクラスのインスタンス(オブジェクト)ですが、IDが異なり、違うインスタンスであることがわかります。

ここでは(1)classキーワードでクラスを定義できる(2)newメソッドでインスタンスを生成できる、という2点を理解してください。

インスタンス変数

インスタンス変数には、@が変数名の先頭についています。 インスタンス変数はそのインスタンスが保持している変数です。

インスタンス変数は「クラス定義の中のメソッド定義(インスタンスメソッド定義)の中」で定義・参照できます。

class Sample2
  def inc
    @c = 0 unless @c
    @c += 1
  end
  def dec
    @c = 0 unless @c
    @c -= 1
  end
end

a = Sample2.new
p a.inc
p a.inc
p a.inc
p a.dec
p a.dec
p a.dec

クラス定義の中のdef〜endはインスタンスメソッドの定義です。 この例ではincdecいう名前のメソッドを定義しています。 メソッドはこのクラスから生成したインスタンスにドット記法で記述し実行します(例えばa.inc、a.decなど)。

  • unlessはifの逆で、条件がfalseまたはnilのときに実行する
  • インスタンス変数は、何も代入していない段階で参照されるとnilを返す。 incメソッドがはじめて呼ばれたときには@cはnilなので@c=0が実行される
  • @c += 1@c = @c + 1と同じである。 つまり@cがひとつ増える。
  • aはSample2クラスから生成したインスタンス
  • a.incが呼ばれるたびに@cが1ずつ増える。
  • a.decが呼ばれるたびに@cが1ずつ減る

プログラムを実行すると

1
2
3
2
1
0

と表示されます。 @cはインスタンスaに属しているので、他のSample2のインスタンスでincメソッドやdecメソッドを呼んでも、aの@cには関係ありません。

(注意)インスタンス変数をdef〜endの外で記述すると、それは「クラスSample2自身のインスタンス変数」になり、「クラスから生成されたインスタンスインスタンス変数」にはなりません。 ですから、def〜endの中で記述するようにしてください。

initialize メソッド

initializeメソッドは特別なメソッドです。 このメソッドはインスタンスが生成される時に自動的に呼ばれ、様々な初期化をするのに用いられます。 このようなメソッドは「コンストラクタ」と呼ばれます。

class Sample2
  def initialize
    @c = 0
  end
  def inc
    @c += 1
  end
  def dec
    @c -= 1
  end
end

@cの初期化がinitializeメソッドの中で行われ、それぞれのメソッドの役割がより明確になりました。

なお、newメソッドに引数を渡して値を初期化することもできます。 引数はinitializeメソッドに引数として渡されます。

class Sample2
  def initialize(c=0)
    @c = c
  end
  def inc
    @c += 1
  end
  def dec
    @c -= 1
  end
end

a = Sample2.new(10)
p a.inc
p a.inc
p a.inc
p a.dec
p a.dec
p a.dec

initializeメソッドにcというパラメータが設けられました。 =0はデフォルト値といい、メソッドが引数なしで呼ばれた時にデフォルト値が使われます。

a = Sample2.new

このように引数なしでインスタンスが生成されれば、@cはデフォルト値の0に初期化されます。 さきほどの引数付きのnewメソッドの例を実行すると、次のように表示されます。

11
12
13
12
11
10

@cが10に初期化されていたことが分かります。

オブジェクトの例

クラスとインスタンスの全体をまとめて広い意味でオブジェクトということもあります。 小見出しの「オブジェクトの例」はそういう意味で、「クラスの例」と同じです。 何でもオブジェクトにできるのですが、意味のないものを作っても役には立ちません。 さきほどのSampleやSample2は役立たない例ですね。

もう少し役に立ちそうなものを考えてみましょう。 ここではリストを考えてみようと思います。 リストはノードからなる構造で、各ノードには次のノードへのポインタがあります。

リスト
ノード1 => ノード2 => ノード3 => ノード4
例えば、
"dog" => "cat" => "sheep" => "elephant" => "lion"

リストはデータを保有し、その順番を保持します。

  • リストの途中に要素を追加したり削除したりするのが簡単にできる(前後のポインタの付け替えだけですむ)
  • 検索は頭からやらなければならないので、大きなリストでは時間がかかり不適当
  • 最初の要素はすぐに取り出せるが、最後の要素はリストを辿らなければならないので取り出しに時間がかかる

このことから保存すべきデータ数が多いときはリスト以外の構造を考えるべきです。 しかし、データ数が少ないときには便利なこともあります。 ノードをクラスで定義してみましょう。

class Node
  def initialize(d=nil)
    @d = d
    @nxt = nil
  end
  def data
    @d
  end
  def data=(d)
    @d=d
  end
  def nxt
    @nxt
  end
  def nxt=(n)
    @nxt = n
  end
  def insert(n)
    n.nxt = @nxt
    @nxt = n
  end
  def remove
    @nxt = @nxt.nxt
  end
end
  • @dはそのノードが持つデータを指す変数
  • @nxtは次のノードを指す変数。 本当は@nextとしたいところですが、nextがRuby予約語なのでやめておきました。
  • これらのインスタンス変数は、生成されたノード・インスタンス固有のもの
  • dataメソッドは保持しているデータを返す
  • data=メソッドはデータをセットする
  • nxtメソッドは次のノードを返す
  • nxt=メソッドは次のノードをセットする
  • insertメソッドは次のノードとの間に新たなノードを挿入する
  • removeメソッドは次のノードを削除する

メソッド名にアルファベットや数字だけでなく=が使えるのがRubyの特長です。 data=やnxt=は代入のメソッドですが、=がいかにも代入メソッドという感じを出しています。 それに加え、糖衣構文でn.data = "dog"と書くとn.data=("dog)と直して実行します。 いよいよ代入らしい感じが出てきます。

insertとremoveはリンクの繋ぎ変えのために、ノード自身ではなく、次のノードに対して実行します。

それでは、"dog"と"cat"をリストに繋げてみましょう。

start = Node.new
dog = Node.new("dog")
start.insert(dog)
cat = Node.new("cat")
dog.insert(cat)

n = start
while n.nxt
  print n.nxt.data, "\n"
  n = n.nxt
end

変数のdog、catと文字列の"dog"、"cat"は別物なので気をつけてください。

while文は、条件が真(falseでもnilでもない)の間while〜endを繰り返すループです。 while文が上手く機能するように、リストの最初のノード(start)にはデータを入れず、次から入れるようにします。 そして、while文ではノードn自身ではなく、次のノードn.nxtがあるかどうか(nilで判断)、n.nxtのデータをとってくるなど、nの次のデータに対してアクションをすることが秘訣です。 実行すると

dog
cat

と表示され、たしかにリストに"dog"と"cat"が繋げられていました。

今回はここまでにして、次回に更にクラス定義を深めていきたいと思います。

ラインエディタ

最後におまけとして簡単なラインエディタを紹介します。 (ここは読まなくても良いと思いますーーーおまけです)。

ラインエディタのために、Rubyのreadlineライブラリを使います。 これはGNU Readline によるコマンドライン入力インタフェースを提供するライブラリです。

require 'readline'

Start = Node.new
@cur = 0
def get_node(i)
  n = Start
  i.times{n = n.nxt ? n.nxt : n}
  n
end
def all_data
  s = ""
  n = Start
  while n.nxt
    s << n.nxt.data
    n = n.nxt
  end
  s
end
while buf = Readline.readline("> ", false)
  c = buf[0]
  s = buf.slice(2,1000)
  case c
  when "q"
    break
  when "r"
    a = File.readlines(s)
    Start.nxt = nil
    n = Start
    a.each do |s|
      s = s + "\n" unless s[-1] == "\n"
      n.nxt = Node.new(s)
      n = n.nxt
    end
  when "s"
    File.write(s, all_data)
  when "l"
    n = get_node(@cur)
    i = 1
    while n.nxt
      print "#{@cur+i} #{n.nxt.data}"
      n = n.nxt
      i += 1
    end
  when "a"
    a = Node.new("#{s}\n")
    n = get_node(@cur)
    n.insert(a)
    @cur += 1
  when "r"
    n = get_node(@cur)
    n.remove
  when "m"
    @cur = s.to_i
  end
end

Startは大文字から始まっています。 大文字から始まるのは定数です。 定数は書き換えができないので、常に最初に代入したオブジェクトを保持します。

  • 定数はメソッド内では定義できない
  • 定数はクラス定義の中のどこからも(メソッド定義内からも)参照できる
  • クラス定義の外で定義された定数はどこからも参照できる

3項演算子a ? b : cはaが真ならbを、偽ならcを値として返します。 文字列のsliceメソッドは、slice(a, b)で文字列のa番目からb文字を返します。 bが大きすぎれば、文字列の最後までを返します。 case文は、caseの次の式の値とwhenの次の式の値が同じものを上から順に探し、実行します。 Cのswitch文とは違い、どれか一つのwhen節のみを実行します。

動かすには、ノード・クラスの定義の部分とエディタの部分の両方が必要です。

エディタの入力は、最初の1文字がコマンド、次の1文字は無視し、3文字目以下がコマンドの引数になります。

  • q: エディタを終了する
  • r ファイル名: ファイルを読み込む。 読み込み前にあったデータはクリアされる
  • s ファイル名: ファイルに書き込む
  • l: 現在行以下を表示する
  • a 文字列:文字列を現在行の次に挿入する
  • d: 現在行の次の行を削除する
  • m 行: 現在行を引数の行に移動する。行は0から最終行の間の整数

徒然Ruby(11)Kernelモジュール

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

バックティックとsystemメソッド

バックティック(`)で囲まれた文字列をbashコマンドとして実行し、その標準出力を文字列として返します。

print `pwd`

pwd」はUNIXコマンドでカレントディレクトリの絶対パスを返します。 もし、カレントディレクトリが/home/user1ならば、このプログラムによってそれが画面に表示されます。 なお、バックティックのコマンドで得られる文字列には改行が最後に含まれます。

同じことを%記法で書くことができます。

print %x{pwd}

バックティックと%xでは同じ結果になります。

コマンドの実行だけが必要で、その標準出力が不要な場合はsystemコマンドを使います。 systemでは、実行したコマンドが正常終了した場合true、それ以外の終了ステータスではfalseを、コマンドが実行できなかったときはnilを返します。

system "cp file1 file2"

これにより、file1をfile2にコピーします。

これらのメソッドによって、数多くの実行コマンドをRubyプログラムで使うことができます。 蓄積されたソフトウェアを使えることでプログラムの手間を減らすことが可能です。

exit

Rubyプログラムを終了します。 引数を与えるとそれを終了ステータスとして親プロセスに通知することができます。

p, pp, printf, sprintf

pとppメソッドは人間に読みやすい形にして引数の式を出力します。 これらはデバッグでよく用いられます。

p [1,2,3,4] #-> [1, 2, 3, 4]
pp [1,2,3,4] #-> [1, 2, 3, 4]

例では両方が同じ出力ですが、引数によってはppの方が適切な改行を入れたり、より読みやすくフォーマットしてくれます。 これらは、ユーザが作ったオブジェクトに対して使うと効果的です。 デバッグ用途なので、完成したプログラムの出力用途に使うことはありません。

printfはCのprintfと同様のフォーマットで出力をします。 sprintfは同様のフォーマットで文字列を作成します。 フォーマットについてはWikipediaが参考になります。

printf "%04d\n", 123
printf "%04d\n", 12
printf "%4d\n", 12

これを実行すると

0123
0012
  12

と表示されます。

  • %とdで整数を埋め込むことを意味する
  • %の次の0は表示幅に対して数字の桁数が少ない時に頭を0で埋める意味
  • その次の4は表示幅(4桁の幅)

詳しくは、Wikipediaなどのprintfのフォーマットを見てください。 上記のように主に小数点を揃えたりするときに使います。

$stderr

これはメソッドではなく、特殊変数です。 標準エラー出力を表します。 メッセージを標準出力に出さず、標準エラー出力に出す時に使います。

標準出力と標準エラー出力はデフォルトで画面出力になっているので、区別のつかない人もいるかもしれません。 エラーメッセージや警告メッセージを標準出力に出すと、そのプログラムの(エラーメッセージでない)本来の出力と混ざってしまいます。 特に標準出力をファイルにリダイレクトするとエラーメッセージもファイルに書き込まれてしまい、台無しになってしまいます。 これを避けるにはメッセージを標準エラー出力に出すようにします。 標準出力のリダイレクトは標準エラー出力には影響しないので、リダイレクト後も標準エラー出力は画面出力となります。

$stderr.print "エラーですよ\n"

rand

乱数を発生させます。 いろいろな使い方ができますが、例えば1から20までの数字をでたらめに並べたいときには次のようなプログラムが考えられます。

seq = (1..20).map{|i| [i, rand]}.sort{|a,b| a[1]<=>b[1]}.map{|a| a[0]}
print seq, "\n"
#=> [10, 14, 15, 18, 13, 17, 19, 7, 9, 4, 8, 20, 6, 5, 11, 16, 12, 1, 2, 3]

実行ごとに乱数は変わるので、数字の並び方も実行ごとに異なります。

  • 1から20のRangeオブジェクトにもmapメソッドがある。 1から20の数字に対してブロックを実行した結果を要素とした配列を返す
  • 最初のmapにより、数字と乱数がペアになった配列20個の配列ができあがる
  • sortメソッドで乱数の大きさで小さい順にソート
  • mapメソッドで整数の部分だけを取り出す

これで1から20までを、その時の乱数の大きさ順に並べ替えた配列が得られます。

raise

例外を発生させます。 デバッグ時に良く使います。 メッセージを引数として与えることができます。

abc = 10
if abc.class != Integer
  raise "abcが整数ではない!\n"
end

このプログラムでは変数abcが整数なのでif文は成り立たず、例外は発生しません。 もし、abcを文字列など、整数以外のものに変更すると例外が発生し

example11.rb:3:in `<main>': abcが整数ではない! (RuntimeError)

エラーメッセージが出力されます。 デバッグ時に、期待された動作ではない場所にraiseを埋め込んでおきます。

もちろん、完成したバージョンの中にraiseを埋め込んでおくことも可能です。 実行時に、プログラム時に想定されないことが起こるのは良くあることです。 そのとき、立ち直りが不可な状態であれば、例外を発生して終了するしかないでしょう。

require

ライブラリを取り込む時に使います。 Rubyにはビルトイン以外にも豊富なライブラリがあります。 それらを使う時にrequireで取り込みます。 プログラムの最初でrequireすることが多いです。

eval

引数の文字列をRubyのコマンドとして実行します。 何でもできてしまうので、外部から入手した文字列に対してevalすることは極めて危険です。 それだけは絶対にしないでください。

eval 'print "evalしたよ\n"' #=>「evalしたよ」が表示される

引数は実行時に組み立てることになると思います。 (そうでなく、プログラム時に知ることのできる文字列なら、evalを使う必要はないから)。 その組みたての過程でバグが入る可能性がありますから、慎重の上にも慎重を重ねて使ってください。

test

ファイルのテストをするメソッドです。 とくに、2つのファイルの関係をテストするのが役に立ちそうです。

test ">", file1, file2

このテストではfile1の方がfile2よりも最終更新日時が新しければtrueとなります。 この他にもファイルの情報を得るコマンドがあります。

徒然Ruby(10)便利なメソッド

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

配列の便利なメソッド

map

mapは配列で最も使うメソッドのひとつです。 各々の要素に対してブロックを実行し、その値からなる新しい配列を返します。

print [1,2,3].map {|i| i+1} #=>[2, 3, 4]

#から改行まではRubyのコメントです。 コメントは実行の対象ではなく、プログラマーが説明やメモを書くためのものです。 Rubyの習慣として#=>は実行したときの画面出力を書いたり、式の値を書いたりします。 上のプログラムのコメントの意味は「プログラムを実行すると[2, 3, 4]が画面に現れる」ということです。 今までプログラムと実行結果を別々に書いていたのですが、この書き方でよりコンパクトに書くことができます。

  • 配列[1,2,3]にmapメソッドが実行される
  • mapメソッドは配列要素をひとつづつ取り出し、ブロックを実行する。 ブロックは波カッコで囲まれた部分(ブロックはdo〜endでも波カッコでも表せる)
  • 最初に配列から1が取り出されiに代入される。 ブロックで「i+1」が実行されると「1+1=2」となり、「2」がブロックの値になる
  • その値がmapメソッドの作る配列の最初の要素になる
  • 以下、配列の次の要素2を用いたブロックの値が3、3を用いたブロックの値が4なので、mapの返す配列は[2, 3, 4]となる。

ひとことでこのプログラムを表現すると「配列の各要素に1を加えた配列を求める」ということになります。 mapは配列オブジェクトを新たに作るので、元のオブジェクトは変更されません。

ディレクトdir1/dir2以下のすべてのファイルのパス名の配列を作り、表示するには

d = "dir1/dir2"
paths = Dir.children(d).map {|file| d + "/" + file}
paths.each {|p| print "#{p}\n"}

mapメソッドもeachメソッドもブロックが短いので波カッコを使うのが良いと思います。 そうすることでプログラム全体の行数を抑えることができ、プログラムの全体を見渡すことができるようになります。

私は以前はdo〜endをブロックに用いることが多かったのですが、最近は波括弧が多くなってきました。 その使い分けはブロックが長いか短いかで決めています。

  • Dir.children(d)でディレクトリd直下のファイル名の配列が得られる
  • mapは各ファイル名に親ディレクトリをつけたパス名にし、その配列を返す
  • eachで各パスを順に取り出しprintメソッドで1行に1要素を画面出力する

mapでできることはeachでもできます。 最初の1を足す例をeachで書き直すと

b = []
[1,2,3].each {|i| b << i+1 }
print b, "\n" #=> [2, 3, 4]

eachを使う場合は空の配列bを用意して、そこに1を加えた要素を追加していくことになります。 mapより複雑なことをしていることがわかると思います。

一般に、配列は関連した要素(例えばあるディレクトリ直下のファイル全体)の集合を表します。 そのような集合の要素に対しては「共通の操作」を行うことが多いです。 mapはそのような共通の操作をまとめて行います。 ひとつひとつの要素に操作をするeachと比べ、まとめて行えるmapの方が優れているといえます。

inject

injectは「たたみこみ演算」を行います。 要素をひとつずつ取り出し、操作を行い、その結果を次の要素との演算に使います。 例えば配列[1,3,5]の要素を合計する操作を考えましょう。

  • 最初に1を取り出す
  • 次の要素3を加え(1+3=4)その結果4を次の操作に使う
  • 次の要素5を加え(4+5=9)その結果9が答えになる

この操作は同じ演算を繰り返し行っています。 これが「たたみこみ演算」です。

print [1,3,5].inject {|i, j| i+j}, "\n" #=> 9
  • 最初の要素1がiに代入される
  • 次の要素3がjに代入される
  • 1回目のブロックの計算(1+3=4)が行われる
  • 2回目の計算では前回の結果4がiに代入される
  • 次の要素5がjに代入される
  • ブロックの計算(4+5=9)が行われる
  • 9がinjectメソッドの値として返される

計算の初期値として引数を与えることもできます。 例えば、mapで[1,2,3]から[2,3,4]を作ったのと同じことをinjectで実現することができます。

print [1,2,3].inject([]) {|i, j| i << j+1}, "\n" #=>[2,3,4]
  • 最初に初期値として空の配列[ ]がiに代入される
  • 初期値がある場合は「次の要素」は最初の要素1になる。 1がjに代入される
  • 1回目のブロックの計算([ ] << 1+1)が行われる。 配列に(破壊的に)2が付け足され、[2]になり、その配列が返される
  • 2回目の計算では前回の結果[2]がiに代入される
  • 次の要素2がjに代入される
  • ブロックの計算([2] << 2+1)が行われる。 配列に(破壊的に)3が付け足され、[2,3]になり、その配列が返される
  • 3回目の計算では前回の結果[2,3]がiに代入される
  • 次の要素3がjに代入される
  • ブロックの計算([2,3] << 3+1)が行われる。 配列に(破壊的に)4が付け足され、[2,3,4]になり、その配列が返される
  • [2,3,4]がinjectメソッドの値として返される

これは、eachメソッドを使ったのと同じ方法です。 injectはeachに比べ、1行でコンパクトに書けるのが長所です。 私は以前はeachを使っていたのが、最近はinjectを使うようになりました。

sort

sortは配列をソートする(整列する)メソッドです。 たとえば

  • [1,3,2].sort =>[1,2,3]を返す(この配列は新規に作られたもので元の[1,3,2]はそのまま)
  • `["bird", "dog", "cat"].sort =>["bird", "cat", "dog"]を返す。文字列はアルファベット順(より正しくは文字コード順)になる

ソートをするためには、配列要素に<=>メソッドが定義されていることが必要です。 このメソッドはa <=> bに対し

  • a が b より大きいなら正の整数
  • a と b が等しいなら 0
  • a が b より小さいなら負の整数
  • a と b が比較できない場合は nil

を返します。 整数や文字列には<=>演算子があらかじめ定義されています。 なお、この演算子は「宇宙船演算子」といわれます。 その形がUFOに似ているからだといわれますが、その由来には諸説あるようです。 この演算子は様々なプログラム言語に実装されていて、比較(<=>)の元になる役割を果たしています。

sortは宇宙船演算子が定義されていないオブジェクトに対しては、ブロックで大小を評価することによってソートすることができます。 また、宇宙船演算子があっても、別の基準でソートしたいときはブロックを用います。 例えば、[1,3,2]を大きい順にソートするには、宇宙船演算子の符号を逆にするためにマイナスをかけます。 以下では小さい順と大きい順の両方を示します。

print [1,3,2].sort, "\n" #=>[1,2,3]
print [1,3,2].sort{|a,b| -(a<=>b)}, "\n" #=>[3,2,1]

数の文字列"9"、"5"、"13"、"20"、"12"、"4"を文字列としてソートするのと、数字としてソートするのでは結果が違います。

a = ["9", "5", "13", "20", "12", "4"]
print a.sort, "\n"
print a.sort{|a,b| a.to_i<=>b.to_i}, "\n"

to_iは文字列を整数に変換するメソッドです。 実行すると

["12", "13", "20", "4", "5", "9"]
["4", "5", "9", "12", "13", "20"]

となります。 文字列のソートは辞書順なので"12"が"4"よりも前にきます。

その他の配列でよく使うメソッド

uniqは重複を除くメソッドです。

[1,2,2,2,3].uniq #=>[1,2,3]

これは文字列の配列を扱っている時に使うことが多いです。

include?はあるオブジェクトが配列の要素になっているときにtrue、そうでないときにfalseを返します。

[1,3,5,7].include?(7) #=> true
[1,3,5,7].include?(8) #=> false

each系のメソッドは大変良く使います。 eachはすでに解説したので、このセクションでは省略します。

to_sメソッド

to_sメソッドはすべてのオブジェクトで実装されています。 そのオブジェクトを文字列に直すメソッドです。

1.to_s #=> "1"
1.23.to_s #=> "1.23"
[1,2].to_s #=> "[1, 2]"
1..2.to_s #=> "1..2"
{one: 1, two: 2}.to_s #=> {:one=>1, :two=>2}

このメソッドはprintメソッドの中で使われています。 printメソッドは、引数が文字列でなければto_sメソッドを使って文字列に直して表示します。 そのおかげで、任意のオブジェクトをprintが出力できるわけです。

ここでちょっとした注意。

print {one: 1, two: 2}

とするとエラーになります。 これはこのプログラムに曖昧さがあるためです。 メソッドの次には引数だけでなくブロック(実はブロックも引数なのですが)が来る可能性があります。 波括弧がブロックを表すとすると、ブロックの中が「one: 1, two: 2」で、これを無理やり実行しようとするとコロンのところでエラーになってしまいます。 このような曖昧さを避けるには丸括弧を使ってください。

print ({one: 1, two: 2})

printと丸括弧の間にスペースが無いほうが普通なのですが、あったとしてもこの文は実行できます。

to_sメソッドはダブルクォート文字列の式展開でも使われます。 式展開では、その式が文字列でなければto_sを使って文字列に直してから埋め込みます。 "abc = #{100}"では、100は整数なのでto_sが使われます。 このようにto_sは様々な場面で背後で活躍しているわけです。

to_sを直接使うことは少ないと思いますが、to_sのおかげでプログラム中の表現が簡潔になっていることが多いです。

<<メソッド

<<メソッドは文字列、配列、整数などで使われますが、オブジェクトによって意味が違うメソッドです。 また、このメソッドは二項演算子として使えますが、糖衣構文によってメソッドであると解釈されて実行されます。

"abc" << "de" #=> "abc".<<("de") => "abcde"
  • 文字列では、元の文字列に引数の文字列を破壊的に繋げ、その文字列を返す
  • 配列では、引数を配列に破壊的に付け加え、その配列を返す
  • 整数では、整数を二進数とみてそのビットを左に引数分だけずらす。 逆の演算子>>があり、これは右にビットをずらす。 このとき小数点以下は切り捨てられる

余談ですが、機械語に近いレベルでは左シフトが2倍、右シフトが1/2倍を高速で行える演算としてよく用いられます。

chomp、chop、strip系メソッド

文字列のメソッドです。

  • chomp =>文字列末尾の改行を取り除いた新しい文字列オブジェクトを返す
  • chop =>文字列の最後の文字を取り除いた新しい文字列オブジェクトを返す
  • strip =>文字列の前後の空白文字を取り除いた新しい文字列オブジェクトを返す
  • lstrip =>文字列の先頭の空白文字を取り除いた新しい文字列オブジェクトを返す
  • rstrip =>文字列の末尾の空白文字を取り除いた新しい文字列オブジェクトを返す

lengthまたはsize

配列や文字列で使うメソッドで要素数、文字数を返します。

Time.now

現在の時刻のTimeオブジェクトを返します。

print Time.now, "\n" #=> 2022-09-19 16:10:15 +0900

年月日、時分秒が表示されます。 最後の「+900」はAsia/Tokyoの協定標準時(UTC)からの時差です。 したがって、UTCは「2022-09-19 7:10:15」になります。 UTCとの差が無いのはヨーロッパやアフリカのいくつかの国です。 アイスランドレイキャビクの標準時はUTCに一致します。