シンボルとハッシュ

今回はシンボルとハッシュについて説明します。

Rubyのドキュメントには、シンボルについて次のように書かれています。

Rubyの内部実装では、メソッド名や変数名、定数名、クラス名などの`名前’を整数で管理しています。 これは名前を直接文字列として処理するよりも速度面で有利だからです。 そしてその整数をRubyのコード上で表現したものがシンボルです。

シンボルは、ソース上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在です。

この説明を理解するには、一般にプログラム言語が変数などをどのように管理しているかを、おおまかにでも理解する必要があります。

シンボルテーブル

ここでは非常に簡単なプログラム言語を考え、変数をどう扱えば良いかを考えてみます。 この言語は名前を「mini」ということにします。

  • 変数はアルファベットの並びとする。 「abc」「hello」などは変数とすることができる。 「abc1」は数字が含まれるので変数にはならない
  • 変数には正の整数を代入できる。 「abc = 100」のように「変数 = 正の整数」の形で代入文を表す
  • 変数を表示することができる。 「print abc」のように「print 変数」の形で変数に代入されていた正整数を表示する。 このとき、改行も出力する
  • 空行や空白のみからなる行は無視する

この言語のプログラムは例えば次のようなものです。

abc = 100
efg = 50
print abc
print efg

abc = 200
print abc

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

100
50
200

と表示されます。

では、miniは変数をどのように扱うのでしょうか? プログラムの各行に対するminiの動作は次のようになります。

  • 1: 「abcという名前の変数は100を値に持つ」ことを記録する
  • 2: 「efgという名前の変数は50を値に持つ」ことを記録する
  • 3: 記録から変数abcを探し、その値を出力する(表示する)
  • 4: 記録から変数efgを探し、その値を出力する
  • 5: 空行なので無視して、次の行に進む
  • 6: 変数abcは記録済みである。その変数の値を200に変更する
  • 7: 記録から変数abcを探し、その値を出力する

このことから、miniには変数名とその値を記録する表(table)が必要です。 この表を「シンボル・テーブル」ともいいます。

Rubyでminiを書いてみましょう。 シンボルテーブルは二重配列で表すことにします。 上の例では最終的にシンボルテーブルは

[ [ "abc", 200], [ "efg", 50 ] ]

となります。 miniのプログラムは次のようになります。

@sym_table = []

def lookup(k)
  @sym_table.each do |a|
    if a[0] == k
      return a[1]
    end
  end
  nil
end

def install (k, v)
  @sym_table.each do |a|
    if a[0] == k
      a[1] = v
      return
    end
  end
  @sym_table << [k, v]
end

File.readlines(ARGV[0]).each_with_index do |s, i|
  if /^print +(\w+)$/ =~ s
    k = $1
    v = lookup(k)
    if v
      print v, "\n"
    else
      print "Error #{k} is not defined.\n"
      exit
    end
  elsif /(\w+) *= *(\d+)$/ =~ s
    k = $1
    v = $2
    install(k, v)
  elsif /^ *$/ =~ s
    # ignore
  else 
    print "Syntax error in #{i+1}\n"
    exit
  end
end

@のつく変数がここではじめて出てきました。 この変数は「インスタンス変数」といいます。 インスタンス変数はメソッド定義の外でも内でも参照することができます。 インスタンス変数の説明はクラス定義のところでします。 @sym_tableがシンボルテーブルです。

  • lookup(k)はシンボルテーブルから名前がkの変数をさがし、その値を返す。 変数が未登録の場合はnilを返す
  • install(k, v)はシンボルテーブルに名前がkで値がvである変数を登録する。 変数が既に登録済みである場合はその値を更新する
  • each_with_indexのイテレーションでは、まず最初の引数を取り出し、その名前のファイルを行ごとに読み込んで配列にする。 その各行に対して、代入、print、空行、その他に対してそれぞれ必要なことを行う
  • 代入では、installを使って変数をシンボルテーブルに登録したり、値を更新したりする
  • printではlookupを使って変数の値を取得し、表示する
  • 空行は無視(Rubyでは#以下はコメントとして実行せず、無視します)
  • その他の場合はエラーメッセージを出す

このプログラムを動かしてみましょう。 最初に示したminiのプログラムを「mini_sample.txt」という名前で保存しておきます。

$ ruby mini.rb mini_sample.txt
100
50
200
$

期待通りに動作しました。

さて、このプログラムは動作しますが、プログラムが長くなり、変数が多くなると時間がかかるようになります。 ですから、シンボルテーブルへの登録と参照をもっと高速にするアルゴリズムが必要ですが、ここでその説明をすると長くなるので詳しいことは省略します。

シンボル

さて、mini言語の例ででてきた変数名と配列のインデックス(整数)は1対1に対応しています。

  • abcのインデックスは0
  • efgのインデックスは1

これから文字列(変数名)の代わりに整数(インデックス)を使っても良いわけです。 この整数をRubyではシンボルといいます。

ただ、Rubyの実装は上記の例とは全然違うので、例え話と考えてください。 いずれにしても「文字列と整数が1対1に対応する仕組みがあるので、文字列の代わりに整数を使っても良い」というのがシンボルのアイディアです。

シンボルの良いところは、あるシンボルと別のシンボルが同じかどうかはその整数値がイコールかどうかで判断できる、したがって「比較(イコールの判定)を高速にできる」ということです。 このように、シンボルを文字列の代わりに使うと良い面がありますが、逆もあります。 例えばシンボルの文字列を「小文字から大文字に変換する」ためには、いったんシンボルを文字列に直してからその操作を行い、得られた文字列を再びシンボルに直すので、余計な時間がかかることになります。 この場合は文字列のままで操作をするほうが合理的です。

では、シンボルを使うと良いのはどのような場面でしょうか? Rubyのドキュメントには次のように書かれています。

実用面では、シンボルは文字の意味を明確にします。「名前」を指し示す時など、文字列そのものが必要なわけではない時に用います。

  • ハッシュのキー { :key => “value” }
  • アクセサの引数で渡すインスタンス変数名 attr_reader :name
  • メソッド引数で渡すメソッド名 __send__ :to_s
  • C の enum 的な使用 (値そのものは無視してよい場合)

シンボルを使うメリットは

  • 新しく文字列を生成しない分やや効率がよく、比較も高速。
  • 文字の意味がはっきりするのでコードが読みやすくなる
  • immutable なので内容を書き換えられる心配がない

これを現時点で完全に理解するのは難しいと思います。 一番用いられるのは、次の項で説明するハッシュのキーとしてなので、それだけでも覚えておけば十分だと思います。 通常は文字列を使っていれば良いと思います。

さて、シンボルのリテラルは3通りあります。

  • :abc
  • :'abc'
  • %s!abc!

この3つはいずれも文字列abcに対応するシンボルを表します。 最初の書き方が一番使われますが、すべての文字が使えるわけではありません。 アルファベットや数字は大丈夫ですが区切り文字の一部は使えません。 詳しくはRubyのドキュメントで確認してください。

シンボルは文字列オブジェクトと違い、同じ文字列のシンボルはオブジェクトとしても同じです。

  • "abc" == "abc" はtrue。==は異なるオブジェクトでも文字列が同じならばtrueだから
  • "abc".equal?("abc")はfalse。equal?メソッドは同じオブジェクトでなければfalseになるから。 最初の”abc”で文字列オブジェクトが作られ、2番目の”abc”で別の文字列オブジェクトが作られるから。
  • :abc == :abcはtrue。==は同じシンボルならばtrueになる(この「同じ」というのはオブジェクトとして同じと考えても、文字列として同じと考えてもよい)
  • :abc.equal?(:abc)はtrue。同じ文字列のシンボルはひとつしかないから。 最初の:abcでシンボルオブジェクトが作られ、2番目の:abcでは同じオブジェクトが参照されている。

最後に注意を述べます。 「シンボルは文字列を整数で表したもの」といいましたが、シンボル・オブジェクトは整数オブジェクトではありません。 したがって、整数のメソッドのabsなどはシンボルには使えません。 また、シンボルを整数に直すメソッドもありません。 あくまでシンボルは「文字列を表すもの」です。

ハッシュ

ハッシュは配列に似ていますが、インデックスに代わって任意のオブジェクトを用いることができます。

a = [ 10, 100, 1000 ]
h = { "ten" => 10, 100 => 100, nil => 1000 }
print a[0], "\n"
print a[1], "\n"
print a[2], "\n"
print h["ten"], "\n"
print h[100], "\n"
print h[nil], "\n"

実行すると

10
100
1000
10
100
1000

となります。 配列ではインデックスが0から始まる数字で、0番目の要素はa[0]で取り出せました。 ハッシュの場合は任意のオブジェクトに対して値が対応します。 上の例では文字列”ten”に対して整数10が対応するので、h["ten"]で10が取り出せます。

ハッシュのことを連想配列ということもあります。 ハッシュは波カッコで囲み、各要素をコンマで区切ります。 各要素は、「キー」=>「値」というパターンで記述します。

キーとなるオブジェクトにはhashequ?の2つのメソッドが定義されていなければなりません。 Rubyのドキュメントを見ると、Stringクラス(文字列オブジェクトのクラス)にはhashメソッドがありますが、Integerクラス(整数オブジェクトのクラス)にはhashメソッドがありません。 しかし、IntegerクラスにはRubyの組み込みのhash関数が適用されるので、実はhash関数が備わっています。

hashはキーになるオブジェクトからhashメソッドを使い、そのハッシュ値(整数値)を求めて保持して、 a["tex"]を実行する時に、”tex”のhash値とそれを比較して、対応する値を返しています(おそらく)。 キーであるオブジェクトが変更可能で、変更してしまうと、値を取り出せなくなってしまいます。 (そのときはrehashするという方法がありますが)。 ですから、キーは変更不可能なオブジェクトが適しています。 なお、文字列をキーにした場合、「文字列は変更可能」で「キーは変更不可能がのぞましい」という相反する条件を解決するために「ハッシュはキー文字列をコピーし、その複製をフリーズ」します(変更不可にする)。 元の文字列は変更可能のままです。

ハッシュのキーは文字列よりもシンボルの方が適しています。

  • シンボルは元々変更不可である
  • シンボルは比較(イコールかどうか)が高速なのでハッシュの値を高速に取得できる

これより、キーをシンボルで書けるならば、そうすることが望ましいです。

h = { :ten => 10, :hundred => 100, :thousand => 1000 }

この書き方の簡略な方法として、コロンを文字列の右に書くことができます。

h = { ten: 10, hundred: 00, thousand: 000 }

矢印を省略でいるのでタイプ量も減り、効率的です。

ハッシュの例

ハッシュはユーザ管理などに使えます。 ひとりひとりのユーザをハッシュで表し、

a = {name: "山田 太郎", email: "taro@example.co.jp" }
b = {name: "鈴木 花子", email: "hanako@example.com" }

このようにハッシュにすると分かりやすくなります。 同じデータを配列にした場合、どうなるかを見てみましょう。

a = [ "山田 太郎", "taro@example.co.jp" ]
b = [ "鈴木 花子", "hanako@example.com" ]

変数aのユーザ名を表すには、

  • ハッシュならばa[:name]
  • 配列ならばa[0]

明らかにハッシュのほうが分かりやすいです。 項目が増えれば増えるほどハッシュの方に軍配があがります。

ハッシュを使ったシンボルテーブル

このセクションのはじめにシンボルテーブルを説明しました。 プログラムminiでは配列を使ってシンボルテーブルを実装しましたが、ハッシュを使うと非常に簡単になります。 そして、lookupとinstallメソッドはハッシュの参照と代入でカバーすることができます。 以下にそのプログラムを示します。

@sym_table = {}

File.readlines(ARGV[0]).each_with_index do |s, i|
  # print @sym_table, "\n"
  if /^print +(\w+)$/ =~ s
    k = $1
    v = @sym_table[k]
    if v
      print v, "\n"
    else
      print "Error #{k} is not defined.\n"
      exit
    end
  elsif /(\w+) *= *(\d+)$/ =~ s
    k = $1
    v = $2
    @sym_table[k] = v
  elsif /^ *$/ =~ s
    # ignore
  else 
    print "Syntax error in #{i+1}\n"
    exit
  end
end

ハッシュのキーは文字列を使っていますが、to_symメソッドでシンボルにすることも考えられます。 シンボルテーブル@sym_tableが大きくなる可能性がある場合は、シンボルに直したほうが高速になると思います。 いずれにしても、ハッシュを使うことでシンボルテーブルは非常に単純になります。

次に大量のデータを処理するシンボルテーブルの例を紹介します。

シンボルテーブルを配列で作り、いちいち頭から検索すると時間がかかります。 これは大量のデータを処理するときには深刻な問題になります。 一般にはこの問題を解決するために、シンボルテーブルにハッシュ関数を使います。 しかし、Rubyにはシンボルがあるので、シンボルをキーにしたハッシュをシンボルテーブルに用いれば単純で高速なプログラムを作ることができます。

その実験として、「不思議の国のアリス」の単語の頻度を調べるプログラムを作ってみました。 ルイス・キャロルの書いた「不思議の国のアリス」(Alice’s Adventures in Wonderland)は1865年刊行ですでに著作権が切れています。 そして、「プロジェクト・グーテンベルク」というウェブサイトにその電子版が掲載され、無料でダウンロードできます。 そこからプレーンテキスト版「pg28885.txt」をダウンロードして使います。 単語は、アルファベットのみからなるので、文字列メソッドのscanを使いました。

@a = File.read("_example/pg28885.txt").scan(/[[:word:]]+/)
  • File.read(ファイル名)でファイルを読み込み、それを文字列にしたものを返す
  • scanメソッドは引数を文字列から抽出し、その配列を返す
  • /[[:word:]]+/は正規表現で、単語を構成する文字(英数字)が1つ以上続くものを表す

これで単語の配列が得られます。 以下のプログラムでは配列を頭から検索する方法(wc1)と、シンボルをキーとしたハッシュを使う方法(wc2)を行い、 ベンチマーク・オブジェクトを使って時間計測を行いました。

require 'benchmark'

@a = File.read("_example/pg28885.txt").scan(/[[:word:]]+/)

# 配列による単語の頻度調査

def install_a (w)
  @word_table_a.each do |a|
    if a[0] == w
      a[1] += 1
      return
    end
  end
  @word_table_a << [w, 1]
end

def wc1
  @word_table_a = []
  @a.each { |s| install_a(s) }
  @word_table_a.sort{|a,b| a[1] <=> b[1]}.reverse.take(10)
end

# ハッシュによる単語の頻度調査

def install_s (w)
  s = w.to_sym
  if @word_table_s.has_key?(s)
    @word_table_s[s] += 1
  else
    @word_table_s[s] = 1
  end
end

def wc2
  @word_table_s = {}
  @a.each { |s| install_s(s) }
  @word_table_s.to_a.sort{|a,b| a[1] <=> b[1]}.reverse.take(10).map{|a| [a[0].to_s, a[1]]}
end

# 両者の結果が同じかどうかチェック

def w_test
  if wc1 != wc2
    print "wc1 != wc2\n"
  end
end

# ベンチマーク

def bm
  Benchmark.benchmark(Benchmark::CAPTION, 14, nil) do |rep|
    rep.report("wc with array") { wc1 }
    rep.report("wc with hash") { wc2 }
  end
end

# 最も多く現れた単語から10番目まで

def top10
  wc2.each do |a|
    print "#{a[0]}:  #{a[1]}\n"
  end
end

# 実行する作業を選択

# w_test
# top10
bm

このプログラムでは3つの作業を選択できます

  • w_test: 2つの方法で行った結果が等しくなるかのチェック
  • top10: 上位10単語とその出現回数を表示
  • bm: ベンチマーク。両者の実行時間を計測して表示

ベンチマークの結果は次のとおりです。

                     user     system      total        real
wc with array    1.345434   0.000000   1.345434 (  1.345614)
wc with hash     0.027752   0.000000   0.027752 (  0.027792)

ハッシュを用いたほうが圧倒的に速くその時間の比は約48:1です。 この方法が最速ではありませんが、プログラム作成の容易さから考えると有力な手法であると思います。 なお、上位10単語は以下のとおりでした。

the:  1716
and:  886
to:  827
a:  687
of:  616
it:  550
I:  545
she:  520
said:  465
in:  420

冠詞のtheが圧倒的に多く、次がandでした。 saidが9位に入っているのは物語だからでしょう。

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を説明します。

モジュール

less than 1 minute read

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

Kernelモジュール

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 ↑