読者です 読者をやめる 読者になる 読者になる

pockestrap

Web Programmer's memo

require しないで Ruby を書く -- import.rb というアプローチ

import.rbという Gem をリリースしました。

この Gem について解説したいと思います。

import.rb の目的

Kernel.requireを置き換えること。

Kernel.require とは

外部ファイルを読み込むためのメソッドです。

module function Kernel.#require (Ruby 2.2.0)

また、相対パスを指定する場合は、Kernel.require_relativeを使います。 パスの指定が相対パスになる以外は、requireと同じです。

module function Kernel.#require_relative (Ruby 2.2.0)

require の使い方例

cat.rbmain.rb の以下の二つのファイルが同じディレクトリに存在するとします。

  • cat.rb
class Cat
  def meow
    puts 'meow meow'
  end
end
  • main.rb
# まだ require していないため Cat クラスが存在しない
# Cat.new.meow # => uninitialized constant Cat (NameError)

# require 先のスクリプトでCatクラスが定義される
require_relative 'cat'

Cat.new.meow # => meow meow

ここで、ruby main.rbと実行すると、meow meowと出力されます。

Kernel.requireの問題点

一番の問題点は、トップレベルにモジュールを定義せざるを得ないことです。

requireされる側のファイルの目的は、何らかのモノをrequireする側のファイルに渡すことです。
例えば、先ほどのcat.rbは、main.rbに対してCatクラスを渡すことが目的だと言えます。

ここで、cat.rbmain.rbCatクラスを渡すために、トップレベルにCatクラスを定義しました。
これは、グローバル変数を定義することと同じです。何故ならば、トップレベルに定義されたクラスは、どこからでも参照することができるからです。

グローバル変数を使用すると、様々な問題が発生することは説明するまでもないと思います。
どこからでもアクセスできてしまう、名前の衝突、それを回避するための名前空間のネストの増大、etc...

原因

では、何故トップレベルにクラスを定義しなければいけないのでしょうか。
それは、requireが値を返さないからです(正確には、新規にロードしたかどうかがBooleanで返ってきますが、任意の値を返すことは出来ません)。
requireの戻り値を使用できない以上、トップレベルにクラスを定義しなければrequire元にクラスを渡すことが出来ません。

提案

そこで、import.rbという一つの提案を作成しました。
commonjsみたいなやつ

Installation

gem install import.rb

Usage

残念ながら、import.rb自体はrequireする必要があります。

先ほどのmain.rbを、import.rbを使用して書き換えてみましょう。
cat.rbの方は書き換える必要がありません。

require 'import'

# まだ import していないため Cat クラスが存在しない
# Cat.new.meow # => uninitialized constant Cat (NameError)

# cat.rb で定義されたCatクラスが、catというローカル変数に格納される
cat = import('./cat')::Cat

cat.new.meow # => meow meow

# トップレベルにはCatクラスが定義されない!
# Cat.new.meow # => uninitialized constant Cat (NameError)

このように、トップレベルにクラスを定義することなく、外部ファイルにて定義されているクラスを使用することが出来ました。

内部の仕組み

無名モジュールを作成し、requireする先のファイルの中身をmodule_evalしています。
60行しかないので、コードを読んだほうがわかりやすいと思います。

import.rb/import.rb at cba99a42536360e0282d0dfbb85ea31aa09ec345 · pocke/import.rb · GitHub

FAQ

既存のライブラリ/Gemとか使えるの?

importされるファイルがrequireを使っていた場合、正常に動作しなかったり結局グローバルにもクラスが定義されてしまうと思います(未検証)。
importされるファイルが先ほどのcat.rbぐらい単純であれば、問題なく動くでしょう。

import/require 両対応のライブラリ/Gemって作れる?

先ほどのcat.rbぐらい単純であれば、何もしなくても両対応になるでしょう。
複雑なものに関しては、「できたらいいなー」程度にしかまだ考えていません。

.soとかも読み込める?

いいえ、読み込めません。.rbファイルのみを探索します。
ファイルを読み込むのにevalを使用しているためです。

import_relativeないの?

そのうち作ろうと思っています。
ただ、上記のmain.rbのようにimport(./cat)と呼び出してやれば相対パスを見に行くので、現状でも相対パスでのimportは可能です。

Windows でも動く?

私が知りたい(パスのセパレータが/前提なコードを思いっきり書いているけど、Rubyがそのへんよろしくやってくれたような気がするようなしないような気がする)

プロダクションで使おうと思うんだけど、どう?

やめとけ

こうしたらいいんじゃない?

Pull Request/Issueお待ちしています!!!!!

まとめ

思いつきで初Gem作ってみた。