Elixir言語の解説サイトLEARNING ELIXIRから、Joseph Kainさんの2016年1月20日付けの記事Use, import, require, what do they mean in Elixir?の翻訳です。
モジュールを参照する宣言文は他の言語にもいろいろあります。どの言語でも「どう使い分けていいのか」結構迷いますね。ではElixirでは?
この記事はその疑問に答えるものです。タイトルに入ってないけどalias
もありますよ。
Elixirにはいくつか他のモジュールを参照するためのスペシャルフォームがあります。それには以下のものが含まれます:
use
import
require
alias
それぞれ独自の意味を持っているのですが慣れるまではどれを使えばいいのかなかなかわかりませんよね。
各スペシャルフォームの実演をするために新しいテスト用のプロジェクトを作りましょう。
$ mix new use_import_require
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/use_import_require.ex
* creating test
* creating test/test_helper.exs
* creating test/use_import_require_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd use_import_require
mix test
Run "mix help" for more commands.
新しい空のモジュールができました。
# lib/use_import_require.exです
defmodule UseImportRequire do
end
この記事の後ろに進むに連れてuse
, import
, require
そして alias
を入れて使っていきます。
Elixirのalias
まずコードを打ちやすく(ということは多分読みやすく)することだけが目的のalias
から。aliasを使うためには別のモジュールを用意しないといけません。では追加しましょう:
# lib/use_import_require/alias_me.exを作成してこう書きます
defmodule UseImportRequire.AliasMe do
def function do
IO.puts "#{__MODULE__}.function"
end
end
さて、もしこのUseImportRequire
の中のfunction
を呼び出したい場合、完全修飾名で参照する必要があります:
UseImportRequire.AliasMe.function
とね。しかし書くのが大変なのでこんな風にalias
を使い、より短い名前で使うことができます:
defmodule UseImportRequire do
alias UseImportRequire.AliasMe
def alias_test do
AliasMe.function
end
end
さてこれです。これがalias
の機能で、より短い名前を使うことができるというわけです。もちろんaliasで別名を使うように一つ以上のモジュールを同時に扱う更なる記法もあります。多重にネストしたモジュールをaliasで柔軟に扱うこともできます。しかし目的といえば、そう、alias
は名前の短縮に使う、ということです。
alias
を使うにあたっては当然トレードオフがあります。上記の簡単な例ではあまり考えることはありません。このアプリはほとんど全てがUseImportRequire
から始まるのでそれを除外することは低いコストでノイズを除去してコードを読みやすくできますから。
しかしプロジェクト内のモジュールの数が増えていくにつれ、結局リーフ名が同じ1モジュール名2を付けることになってしまうかもしれません。そうなると別名を使うのは読む人を混乱させてしまうことになるかもしれません。
別名を使うと曖昧さを生み出してしまうのか?と自問することになるでしょう。
1月23日追記 : コメント欄でPhilip Clarenが指摘したように、alias-asフォームを使うと別名を使うときに名前を選べるようになります。以下の例を追加しました:
defmodule UseImportRequire do
alias UseImportRequire.AliasMe
alias UseImportRequire.AliasMe, as: AnotherName
def alias_test do
AliasMe.function
end
def alias_as_test do
AnotherName.function
end
end
見ればわかるようにas: name
パラメータをalias
に追加できるので使いたい名前を別名に与えることができます。この例ではAnotherName
という名前にしました。
Alias-asは同じリーフ名を持つ2つ以上のモジュールがある場合にとても役立ちます。例えばUseImportRequire.FirstKind.AliasMe
とUseImportRequire.SecondKind.AliasMe
があるような場合、単純に両方を別名で扱うことはできません。そうすると曖昧さが残ってしまいます。alias-asを使うとそれぞれに違う名前を別名として選ぶことができます。
Elixirのimport
さて、まだAliasMe.function
は書くには大変すぎると思ったとします。その場合はimport
を使いましょう。importの例を挙げるためにimportされるモジュールを新規に作ります:
# lib/use_import_require/import_me.ex
defmodule UseImportRequire.ImportMe do
def function do
IO.puts "#{__MODULE__}.function"
end
end
そしてこれをUseImportRequire
から参照してみます:
defmodule UseImportRequire do
alias UseImportRequire.AliasMe
import UseImportRequire.ImportMe
def alias_test do
AliasMe.function
end
def import_test do
function
end
end
これで単純にfunction
で目的の関数を参照できました。短くていいですね。でも代わりに何か犠牲になってるんじゃ…?
このケースではずっと曖昧さを生み出しやすくなってしまっています。例えばもしAliasMe
とImportMe
の両方のモジュールをインポートしたら結局どちらもfunction
という関数を持つので、実のところコンパイルされないでしょう。なお、このコンパイルエラーは実際に曖昧な名前付けをされた関数が呼び出されるまで発生しない遅延型です。
私はimport
を使うのは控えることをおすすめします。あまりにも情報量がカットされてしまうのであなたのコードを読む人に負担になりかねないので。
でもインポートが役立ついくつかの場合もあります。あるモジュールが特定のモジュールを数多く呼び出すことに焦点を合わせているような場合ならimport
は有意義です。ひとつ一般的な例を挙げるならEctoのクエリを拡張して使うモジュールはよくEcto.query
をインポートしています。
import
マクロはまた特定の関数やマクロをインポートすることができます。これによって「名前空間の汚染」を減らして曖昧さや混同の危険を減らすことができます。再び、これはEcto.Query
でよく使われる手法で、ドキュメントでの推奨は:
import Ecto.Query, only: [from: 2]
Ecto.Query.from/2
マクロだけをインポートするためです。
Elixirのrequire
require
マクロはコンパイラにrequireマクロを含んでいるモジュールをコンパイルする前に指定されたモジュールをロードするように指示します。これは特定のモジュールからマクロを参照したい時のみに必要です。例えば以下の様な場合、:
defmodule UseImportRequire do
require UseImportRequire.RequireMe
end
RequireMe
モジュールが使いたいマクロを含んでいる場合に使います。これはマクロの名前については何も特別なことは行いません。やはり完全修飾名で参照しなければなりません。
この使い方ではrequire
とimport
の目的は少しかぶっています。どちらの他のモジュールのマクロを使うことができます。しかし名前空間に与える影響は異なっています。
Elixirのuse
use
マクロはスペシャルマクロ、__using__/1
を特定のモジュールから呼び出します。以下に例を挙げます:
# lib/use_import_require/use_me.ex
defmodule UseImportRequire.UseMe do
defmacro __using__(_) do
quote do
def use_test do
IO.puts "use_test"
end
end
end
end
そして以下の行をUseImportRequire
に追加します:
use UseImportRequire.UseMe
UseImportRequire.UseMe
は__using__/1
マクロを呼び出すことでuse_test/0
関数を定義します。
これがuse
のやっていることの全てです3。ところが__using__/1
マクロが今度はalias
やrequire
やimport
を呼び出すこともよくあります。一方で__using__/1
マクロが使われているモジュールが別名で参照されたりやインポートされることもあります。これによって使われるモジュールでその関数やマクロがどのように参照されるかのポリシーを決めることができます。__using__/1
マクロが他のモジュールへの参照を設定できることは特にサブモジュールについて非常に柔軟性が高くなります。
Phoenix Frameworkではuse
と__using__/1
マクロをalias
やimport
をユーザー定義モジュールで繰り返し呼び出す手間を削減するために利用しています。
Ecto.Migration
に短くていい例があります:
defmacro __using__(_) do
quote location: :keep do
import Ecto.Migration
@disable_ddl_transaction false
@before_compile Ecto.Migration
end
end
Ecto.Migration.__using__/1
マクロはimport
を含んでいるのでuse Ecto.Migration
を使うことはimport Ecto.migration
もまた使うことになります。このマクロはまたEctoのビヘイビアを決めている(であろう)プロパティの設定も行っています。
要約すると、use
マクロは指定されたモジュール内の__using__/1
マクロを呼び出すだけである。何が行われているか理解するには__using__/1
マクロの定義を読む必要がある。ということです。
モジュールの参照
では、単に他のモジュールから関数を呼び出したくなったら上記のマクロのうちどれを使うのが正解でしょうか?答は…どれでもありません。その代わり直接参照ことができます。参照できるモジュールの例です:
# lib/use_import_require/reference_me.ex
defmodule UseImportRequire.ReferenceMe do
def function do
IO.puts "#{__MODULE__}.function"
end
end
そしてこんな風にして関数にアクセスできます:
def reference_test do
UseImportRequire.ReferenceMe.function
end
この通り。モジュールをuse
やimport
やrequire
を使って参照する必要はありません。単に完全修飾名で呼ぶだけです。
スコープ
alias
やimport
を使う場合には便利さと明確さの間でトレードオフがあると述べました。このトレードオフを緩和するもうひとつのやり方があります。alias
とimport
は今まで使ってきたようにモジュールの最も外側のスコープから呼び出す必要はないのです。例えば他の関数の内側から呼び出すことができます。import
をそう使う例を挙げます:
defmodule UseImportRequire.WithScope do
def scope_test do
import UseImportRequire.ReferenceMe
function
end
end
曖昧さの問題からUseImportRequire
モジュールではなくて新しいモジュールを作ってこれを追加しなければなりませんでした。
スコープありのバージョンだと他の関数からは完全修飾名を使わないとfunction
にアクセスできません。例えば以下はコンパイルされないでしょう:
defmodule UseImportRequire.WithScope do
def scope_test do
import UseImportRequire.ReferenceMe
function
end
def failing_scope_test do
function
end
end
alias
マクロも関数内で同じように使えます。
import
やalias
を狭いスコープでこのように呼び出すことはあなたのコード内で発生する「名前空間の汚染」の総量を抑えることができます。
さらに、狭いスコープを使うことでalias
やimport
を参照元の近くに置くことができ多少なりともコードを明白にでき、読む人が何が起きているか追いかけるのを簡単にします。特にあなたのコードが長くなり始めた時に有効です。
例
import
を使ったいい例を挙げましょう:
defmodule Orthrus.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :username, :string
add :password_hash, :string
add :email, :string
timestamps
end
end
end
use Ecto.Migration
の呼び出しはEcto.Migration.__using__/1
を呼び出します。先に見たとおり、このマクロは次はimport Ecto.Migration
を呼び出します。このインポートはマイグレーションについて非常に明白なコードを書けるようにしてくれます。create
,add
,timestamps
をEcto.Migration
を頭に付けてコードを散らかさないでもよくしてくれるのです。
マイグレーションについてはマイグレーション自身が狭い範囲に集中されたタスクであることからよいトレードオフになりました。create table
やadd
への参照を読むとき、データベースのマイグレーションを考えやすくなるのでこのコードは意味が取りやすいのです。
でももし他のそれほど集中されていないタスクがあるなら、import
が最適な選択かどうか自問する方がいいと思います。
-
Hogehoge.Taratara と Ponpoko.TarataraでのTaratara。Elixirの公式ドキュメントではLeaf nameという記載はなさそうだけど一般的なんでしょうか。 ↩
-
例えばUseImportRequire.AliasMeをlib/use_import_require/alias_me.exに、UseImportRequire2.AliasMeをlib/use_import_require2/alias_me.exに置く場合(alias_me.exだとピンとこないけど「read_data.ex」のようなモジュールがfrom_web/,from_sensor/,from_console/...の下に並ぶケースは多そう)に別名がどっちもAlias.Meになるしかない…と最初書いた時点では考えていたようですね。 ↩
-
端的には参照される側のモジュール内の
__using__/1
マクロで公開したい関数やマクロを設定できるということ ↩