💎

Railsにおいてrequireがなぜ要らないのかを追ってみた(Gemfile編)

2022/09/11に公開約4,800字

はじめに

こんにちは。現役でフルスタックエンジニアをしています。

Railsはインストールした①Gemもですが、自ら定義した②クラス・モジュールもrequireをする必要がない、利便性に優れたフレームワークです。

日々使う中でその恩恵にあやかっていますが、どのような内部実装でそれが可能になっているか気になったので調べて、記事にしてみました。

①Gem, ②クラス・モジュールの別記事に分けてつらつら書いていきます。

TL;DR

  • config/application.rbに書かれている設定のおかげ
  • その設定でBundlerが呼び出されていて、そこでGemfile内のGem及び依存関係を列挙してくれてる

きっかけ

「暇やし前からやってみたかった技術課題とかやってみるか」
Ruby使えるやん、ほなRubyでと」
Railsちゃうからrequire書かなあかんわけか」
「前にRailsなんでrequireいらんか調べたことあったけな、あれなんでやったかな」
「そうだ、内部実装を見よう」

つらつら書きましたが、要は、暇だったからです


Rails内の設定箇所

設定箇所はconfig/application.rbに記載されています。
Bundler.require(*Rails.groups)が設定箇所になります。

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)     # これが設定箇所

module RailsApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true
  end
end

さて、設定箇所の上のコメント部分を見ると「GemfileにかかれているGemをrequireする」と書かれているようです。
処理はBundlerが実行しているみたいなので、Bundlerを見にいってみましょう。

Bundlerのロジック

Ruby on Railsを使っている開発者にとってBundlerというのは耳馴染みしかない言葉だとは思いますが、まずはBundlerの役割を確認しておきます。
公式リポジトリのREADMEの説明を抜粋すると、Bundlerは以下のことを行っているみたいです。

  • Rubyを使ったアプリケーションがどのマシンでも同じコードを実行できるようにサポートする
  • Gemのリスト(Gemfile)から、リストに載っているGemだけでなく、それらのGemと依存関係にあるGemもインストールする
  • インストール前には、すべてのGemのバージョンをチェックし、互換性とダウンロードできるかを確認する
  • Gemインストール後、新しいバージョンが利用できるようになれば、それを更新するようサポートする
  • インストールされたGemのバージョンを記録し、他の開発者が同じGemをダウンロードできるようにする

列挙した役割の通り、パッケージ管理ツールですね。
どんなものかはわかったので、早速Bundler.require(*Rails.groups)の実装を見ていきましょう。
コードが有るのはrubygems/bundle/lib/bundler.rbです(ソースコード)


module Bundler

  def setup(*groups)
    # Return if all groups are already loaded
    return @setup if defined?(@setup) && @setup

    definition.validate_runtime!

    SharedHelpers.print_major_deprecations!

    if groups.empty?
      # Load all groups, but only once
      @setup = load.setup
    else
      load.setup(*groups)
    end
  end

  def require(*groups)
    setup(*groups).require(*groups)
  end

  def load
    @load ||= Runtime.new(root, definition)
  end

Railsのソースコード - rails/railties/lib/rails.rb

module Rails

  def groups(*groups)
    hash = groups.extract_options!
    env = Rails.env
    groups.unshift(:default, env)
    groups.concat ENV["RAILS_GROUPS"].to_s.split(",")
    groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) }
    groups.compact!
    groups.uniq!
    groups
  end

どうやら、Railsの環境変数productionなどを引数にして、必要なGemを判別しているようです。
Bundlerによる、GemfileのGemの依存関係の抽出(下記のような配列型)に関しては、Bundler.load内のRuntime.setupが行っています。
ここでの処理は複雑な話になる(自分の理解出来たかも怪しい...)ので割愛しますが、Bundler.require(*Rails.groups)の実行結果を見てみましょう。

[1] pry(main)> Bundler.require(*Rails.groups)
=> [Gem::Dependency.new("rails", Gem::Requirement.new(["~> 6.1.6"]), :runtime),
 Gem::Dependency.new("dotenv-rails", Gem::Requirement.new([">= 0"]), :runtime),
 ... 中略,
 Gem::Dependency.new("tzinfo-data", Gem::Requirement.new([">= 0"]), :runtime)]

Gemfileに載っているGemとその依存関係を管理した配列を作成し一括で書いてくれているようです。
それが他のrequire "rails"同様にapplication.rbで呼び出されているためGemfileのGemはrequireがいらない、ということになるようです。


おわりに

1記事にまとめて2トピック書くつもりでしたが、長くなりそうなので分けることにしました。

フレームワークの便利さに頼ってコーディングを進めていると、時にそれが牙を向いて「よく分からんエラー」に変貌をとげることはしばしばあります。

フレームワークの内部実装を理解する、っていうのは非現実的ですが、そのフレームワークの長所足り得る部分(今回のrequire不要は他フレームワークと比較するとRailsの長所)に関しては、フレームワークのロジックの根幹が集まったところ(私見)なので、そのフレームワークの開発者を使う者であるなら見ておくべきだと感じました。

2つ目の記事の「おわりに」に書くようなことが内容となってしまいましたw

まだまだジュニアで未熟ですが、内部実装も理解してフレームワーク・言語を使いこなせる開発者になれるよう精進します。

拙い文章でしたがお読みいただきありがとうございました。

Discussion

ログインするとコメントできます