53
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[翻訳] use, import, require --- Elixirではどういう意味なのか

Posted at

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.AliasMeUseImportRequire.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で目的の関数を参照できました。短くていいですね。でも代わりに何か犠牲になってるんじゃ…?

このケースではずっと曖昧さを生み出しやすくなってしまっています。例えばもしAliasMeImportMeの両方のモジュールをインポートしたら結局どちらも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モジュールが使いたいマクロを含んでいる場合に使います。これはマクロの名前については何も特別なことは行いません。やはり完全修飾名で参照しなければなりません。

この使い方ではrequireimportの目的は少しかぶっています。どちらの他のモジュールのマクロを使うことができます。しかし名前空間に与える影響は異なっています。

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マクロが今度はaliasrequireimportを呼び出すこともよくあります。一方で__using__/1マクロが使われているモジュールが別名で参照されたりやインポートされることもあります。これによって使われるモジュールでその関数やマクロがどのように参照されるかのポリシーを決めることができます。__using__/1マクロが他のモジュールへの参照を設定できることは特にサブモジュールについて非常に柔軟性が高くなります。

Phoenix Frameworkではuse__using__/1マクロをaliasimportをユーザー定義モジュールで繰り返し呼び出す手間を削減するために利用しています。

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

この通り。モジュールをuseimportrequireを使って参照する必要はありません。単に完全修飾名で呼ぶだけです。

スコープ

aliasimportを使う場合には便利さと明確さの間でトレードオフがあると述べました。このトレードオフを緩和するもうひとつのやり方があります。aliasimportは今まで使ってきたようにモジュールの最も外側のスコープから呼び出す必要はないのです。例えば他の関数の内側から呼び出すことができます。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マクロも関数内で同じように使えます。

importaliasを狭いスコープでこのように呼び出すことはあなたのコード内で発生する「名前空間の汚染」の総量を抑えることができます。

さらに、狭いスコープを使うことでaliasimportを参照元の近くに置くことができ多少なりともコードを明白にでき、読む人が何が起きているか追いかけるのを簡単にします。特にあなたのコードが長くなり始めた時に有効です。

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,timestampsEcto.Migrationを頭に付けてコードを散らかさないでもよくしてくれるのです。

マイグレーションについてはマイグレーション自身が狭い範囲に集中されたタスクであることからよいトレードオフになりました。create tableaddへの参照を読むとき、データベースのマイグレーションを考えやすくなるのでこのコードは意味が取りやすいのです。

でももし他のそれほど集中されていないタスクがあるなら、importが最適な選択かどうか自問する方がいいと思います。


  1. Hogehoge.Taratara と Ponpoko.TarataraでのTaratara。Elixirの公式ドキュメントではLeaf nameという記載はなさそうだけど一般的なんでしょうか。

  2. 例えば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になるしかない…と最初書いた時点では考えていたようですね。

  3. 端的には参照される側のモジュール内の__using__/1マクロで公開したい関数やマクロを設定できるということ

53
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?