Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyには魔法は存在しない――「マジック」の正体とその解き方

RailsやRubyには「魔法(マジック)が多すぎる」という話を耳にしたことがあるかもしれません。あるいは、ご自身でそう感じた、あるいはそう口にした経験がある方もいるでしょう。

  • でも、その考えはどこから来るのでしょうか?
  • そもそも「Railsの魔法」とは何なのでしょうか?
  • そして、その魔法を解くためには何をすればいいのでしょうか?

私の考えはこうです

何かが魔法のように感じられるのは、そこに自分が知らない情報、つまり欠けている情報があるからです。

手品と同じことです。種を知ってしまえば、そこに魔法はありません

Rubyには魔法は存在しない――「マジック」の正体とその解き方

ソフトウェア開発において「種を知る」ということは、物事が実際にどのように動いているのかを理解することに他なりません。

具体例を見てみよう!

ここではBCryptを例に取り上げます。BCryptはパスワードを安全に保存するためのハッシュアルゴリズムで、Rubyではbcrypt gem経由で利用できます。

以下は、BCryptを使って「testing」という文字列をハッシュ化する例です:

require 'bcrypt'

BCrypt::Password.create("testing")
# "$2a$10$3o.xrISG8fqKfzKqDpgKn.3cwjFV//9C9cZ7MuK5S9sNKFBivB7YG"

BCrypt::Password.create("testing")
# "$2a$10$BFb/e3cWuAIyelcmkfGtE.GTifXHxig4IzVbdZs9C1zSpF/Jr6c3W"

BCrypt::Password.create("testing")
# "$2a$10$Q952BVgM783Y4kPvwvxwC.CS2DWUX9jQuDoE6q2R041kQY0ZWPnSe"

注目すべきは、実行するたびに毎回異なるハッシュが生成されるという点です。一方、MD5(使わないでください!)やSHA1のようなアルゴリズムでは、同じ文字列に対して常に同じ出力が得られます。

なぜBCryptだけ違う動作をするのでしょうか?

その理由はすぐ後で説明しますが、まずはデータベースに保存されたbcryptハッシュと、ユーザー入力(フォームなど)から送られてきたパスワードを比較する方法を見てみましょう。

BCrypt::Password.new(@user.hash) == params[:password]
# true

左辺のBCrypt::Password.newは、データベースに保存されているハッシュを引数に取るBCryptオブジェクトです。

右辺のparams[:password]は、ユーザーがログイン時に入力した平文パスワードです。

ここでは、正しいユーザー名とパスワードの組み合わせが入力されているものと仮定します。

では、なぜこの比較はtrueと評価されるのでしょうか?

これを理解するには、次の2つのことを知っておく必要があります:

  1. BCryptは「ソルト(salt)」と呼ばれる仕組みを使っています。これはレインボーテーブルなどの事前計算済みハッシュ攻撃への対策としてセキュリティを高めるためのランダムな値で、ハッシュ文字列自体の中に埋め込まれて保存されます。
  2. Rubyでは、構文のように見える多くのものが、実はただのメソッドだということです。等価演算子(==)もその典型例です。

この2点を踏まえると、答えはシンプルです。BCryptは独自の==メソッドを定義していて、このメソッドはソルト値を取り出し、パスワード比較の際にそれを考慮できるようになっているのです。

言い換えれば

BCrypt#==は、保存されたハッシュからソルト値を取り出します。

次に、そのソルトを使って平文パスワード(ユーザー入力)をハッシュ化します。その結果、パスワードが正しければ両者のハッシュは一致する、というわけです。

実際のソースコードを見てみると、おおよそ次のような実装になっています:

def ==(secret)
  super(
    BCrypt::Engine.hash_secret(secret, @salt)
  )
end

superは、親クラスの同名メソッド(この場合は==)を呼び出すものであることを思い出してください。

そして、BCrypt::Passwordの親クラスはStringです。

すべてはメソッド次第

理解しておくべき重要なポイントは、いくつかのキーワードや構文要素(括弧など)を除けば、Rubyの世界はすべてクラスとメソッドで成り立っているということです。

扱っているクラスがわかれば(classメソッドで確認できます)、そのオブジェクトで使える操作(メソッド)も必ず把握できます。

しかし、オブジェクトを指定せずにメソッド呼び出しだけが書かれているコードにも遭遇することがあります。

これは多くの場合、そのメソッドが現在のクラス内で定義されていることを意味しますが、必ずしもそうとは限りません。

puts 123

putsは一体どこで定義されているのでしょうか?実際に調べてみましょう:

method(:puts).owner

# Kernel

もう一つ、混乱のもとになりやすいのがメタプログラミングです。メタプログラミングでは、プログラムの実行中にメソッドを作成・変更し、削除することさえ可能です。こうした仕組みはコードを不透明で分かりにくく見せてしまいます。

しかし、どんなメタプログラミング用メソッドが存在し、それぞれがどう動作するのかを把握しておけば、混乱は大幅に減らせます。

まとめ

今回の内容から学べるのは、物事の仕組みを深く理解することが、「魔法」を解き明かし、より良いコードを書き、より優れた開発者へと成長するための鍵だということです。

Rubyのスキルをさらに磨きたい方は、基礎と高度な概念のギャップを埋めるために設計された著書『Ruby Deep Dive』をぜひチェックしてみてください🙂

  1. Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説

    ※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す

  2. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端