<Enjoy-Elixir>mix.exsにいて少し詳しく
Enjoy-Elixirと称する、プログラミング言語Elixirを勉強する記事群の1つです。
mix.exs
mix.exs
は Elixirプロジェクト(Mixプロジェクト)の定義ファイルです。
Elixirプロジェクト(または単にアプリケーション、コンポーネントなどど呼んでもいいですが)は、 mix.exs
の内容を元に初期化を行って仕事ができる状態になります。
この記事では、ElixirのJSONライブラリ michalmuskala/jason
の mix.exs
を教材にします。また比較のための副教材としてデフォルトの mix.exs
も参照します。
デフォルトの mix.exs
は次のように作成します。
# mix コマンドで新しいElixirプロジェクトを作成
$ mix new example
$ cd example
$ ls -1
README.md
lib
mix.exs ** これがデフォルトのmix.exsです **
test
まずは、各 mix.exs
の内容を表示します。
michalmuskala/jason - mix.exs
michalmuskala/jason - mix.exsdefmodule Jason.Mixfile do
use Mix.Project
@source_url "https://github.com/michalmuskala/jason"
@version "1.2.2"
def project() do
[
app: :jason,
version: @version,
elixir: "~> 1.4",
start_permanent: Mix.env() == :prod,
consolidate_protocols: Mix.env() != :test,
deps: deps(),
preferred_cli_env: [docs: :docs],
dialyzer: dialyzer(),
description: description(),
package: package(),
docs: docs()
]
end
def application() do
[
extra_applications: []
]
end
defp deps() do
[
{:decimal, "~> 1.0 or ~> 2.0", optional: true},
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
] ++ maybe_stream_data()
end
defp maybe_stream_data() do
if Version.match?(System.version(), "~> 1.5") do
[{:stream_data, "~> 0.4", only: :test}]
else
[]
end
end
defp dialyzer() do
[
ignore_warnings: "dialyzer.ignore",
plt_add_apps: [:decimal]
]
end
defp description() do
"""
A blazing fast JSON parser and generator in pure Elixir.
"""
end
defp package() do
[
maintainers: ["Michał Muskała"],
licenses: ["Apache-2.0"],
links: %{"GitHub" => @source_url}
]
end
defp docs() do
[
main: "readme",
name: "Jason",
source_ref: "v#{@version}",
canonical: "http://hexdocs.pm/jason",
source_url: @source_url,
extras: ["README.md", "CHANGELOG.md", "LICENSE"]
]
end
end
example - mix.exs
mix new
コマンドで作成したデフォルトの mix.exs
です。(作成時のMixのバージョン Mix 1.12.1 (compiled with Erlang/OTP 24)
)
defmodule Example.MixProject do
use Mix.Project
def project do
[
app: :example,
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
mix.exs
の大枠を掴みながら読み進めていきます。
プロジェクトの定義はモジュールの中で
A project can be defined by using Mix.Project in a module
Elixirプロジェクトの定義はモジュールの中で定義するのがルールです。また、Mix.Project
モジュールをその中で使うのもルールです。ゆえに michalmuskala/jason - mix.exs
では次のように書かれています。
defmodule Jason.Mixfile do
use Mix.Project
#
# ... 省略
#
end
example - mix.exs
では次のようになっています。
defmodule Example.MixProject do
use Mix.Project
#
# ... 省略
#
end
モジュールは、関数をまとめることができるものです。
defmodule
キーワード(実はマクロですが、この時点では特に気にする必要はありません)はモジュールを定義することができます。モジュールの名前とモジュールの中身・機能(doブロック)を受け取り、アプリケーションの中にモジュールを作成します。
jason
で定義されているモジュール名は Jason.Mixfile
です。一方、example
で定義されているモジュールの名前は Example.MixProject
になっていますね。細かいところですが、 Mixfile
と MixProject
の違いは何でしょうか?
この違いは、 mix.exs
を生成したElixirのバージョンの違いによるものです。
v1.5.3
までは Mixfile
が使われていましたが、 v1.6
から MixProject
になっています。
jason
の mix.exs
は v1.6
より前のバージョンで作成されているため、 Mixfile
になっています。
読み進める上では、この違いは特に気にしなくてよいでしょう。
次に use
について確認しましょう。
defmodule Jason.Mixfile do
use Mix.Project
#
# ... 省略
#
end
Uses the given module in the current context.
use
は、マクロです。は引数として、モジュール(とオプション)を受け取ります。use
が実行される時、 use
を記述したスコープで、引数で受け取ったモジュールに定義されている __using__
が実行されます。
use Module
が実行されると、 Module.__useing__
が実行される、ということです。
つまり、 use Mix.Project
が実行されると、 Mix.Project.__using__
が実行されるということですね。
Mix.Project.__using__
を見てみましょう。
# Mix.Project module
@doc false
defmacro __using__(_) do
quote do
@after_compile Mix.Project
end
end
# Invoked after each Mix.Project is compiled.
@doc false
def __after_compile__(env, _binary) do
push(env.module, env.file)
end
Mix.Project.__using__
が実行された時に何が起こるのか簡単に説明します。
@after_compile
はモジュール属性と呼ばれるものです。モジュール属性にはElixirで定義されているものもあれば、独自に定義できるものもあります。 @after_compile
は定義済みモジュール属性で、コールバックの機能があります。指定されたモジュールに定義されている __after_compile__
が呼び出されます。
では、Jason.Mixfile
で use Mix.Project
が呼ばれると何が起こるか説明します。
defmodule Jason.Mixfile do
use Mix.Project
#
# ... 省略
#
end
Jason.Mixfile
モジュールをコンパイルしようとします。use Mix.Project
はMix.Project.__using__
を展開します。Mix.Project.__using__
では@after_compile Mix.Project
が指示されます。Jason.Mixfile
のコンパイルが終わるとMix.Project.__after_compile__
が呼ばれます。(これは @after_compile の指示によるものです)Mix.Project.__after_compile__
の処理で、Mix.ProjectStack
プロセスへの追加処理がされて、プロジェクトが設定されます。
プロジェクトの設定
Elixirプロジェクトの設定は mix.exs
にモジュールを定義して、 use Mix.Project
を記述する必要があることを上で説明しました。
プロジェクトの定義にはさらに project
という関数をモジュールに定義する必要があります。
In order to configure Mix, the module that uses Mix.Project should export a project/0 function that returns a keyword list representing configuration for the project.
Example.MixProject
を見てみましょう。
defmodule Example.MixProject do
use Mix.Project
def project do
[
app: :example,
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# ...省略
end
project
関数を定義して、キーワードリストを返しています。キーワードリストというのは、リストの要素が2要素のタプルになっていて、タプルの1要素目がアトムになっているものです。
project
関数で返すキーワードリストは、プロジェクト内で参照される設定になります。
Mix.Project
がコンパイルされた後で、project
関数が呼びされます。そのため、project
関数を定義しないとエラーになります。
project関数を呼び出している箇所 - atom.project が関数呼び出し
Example.MixProject
を使って、任意の設定を追加して確認してみます。その次に、project
関数を定義せずに起動すると、どういうエラーになるか確認します。
Example.MixProject
をまずは次のように修正してみます。
defmodule Example.MixProject do
use Mix.Project
def project do
[
app: :example,
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps(),
hello: "world"
]
end
# ...省略
end
プロジェクトをコンパイルして使用できる状態で、インタラクティブに操作するためのコマンドは iex -S mix
です。
設定は、Mix.Project.config
で確認できます。
$ iex -S mix
iex> Mix.Project.config[:hello]
"world"
iex> Mix.Project.config
[
aliases: [],
build_embedded: false,
build_per_environment: true,
build_scm: Mix.SCM.Path,
config_path: "config/config.exs",
consolidate_protocols: true,
default_task: "run",
deps_path: "deps",
elixirc_paths: ["lib"],
erlc_paths: ["src"],
erlc_include_path: "include",
erlc_options: [],
lockfile: "mix.lock",
preferred_cli_env: [],
app: :example,
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: false,
deps: [],
hello: "world"
]
project
関数に追加した :hello
の設定値が取得できることが確認できましたね。また、 project
関数で定義した以外にも色々な設定がデフォルトで設定されていることがわかりますね。
次に project
関数を消した場合にどうなるか見てみましょう。
$ iex -S mix
** (UndefinedFunctionError) function Example.MixProject.project/0 is undefined (function not availabl
e)
Example.MixProject.project()
(mix 1.12.1) lib/mix/project.ex:792: Mix.Project.get_project_config/1
(mix 1.12.1) lib/mix/project.ex:114: Mix.Project.push/3
(stdlib 3.15.1) lists.erl:1267: :lists.foldl/3
(stdlib 3.15.1) erl_eval.erl:685: :erl_eval.do_apply/6
project
関数が未定義の場合に、 Mix.Project.get_project_config
で関数未定義エラーになるのがわかりますね。
プロジェクトの設定には、 project
関数の設定が必要ということがわかりました。では、 Jason.Mixfile
の project
を見てみましょう。
defmodule Jason.Mixfile do
use Mix.Project
@source_url "https://github.com/michalmuskala/jason"
@version "1.2.2"
def project() do
[
app: :jason,
version: @version,
elixir: "~> 1.4",
start_permanent: Mix.env() == :prod,
consolidate_protocols: Mix.env() != :test,
deps: deps(),
preferred_cli_env: [docs: :docs],
dialyzer: dialyzer(),
description: description(),
package: package(),
docs: docs()
]
end
# ... 省略
end
Example.MixProject
とは違い、Jason.Mixfile
にはデフォルト設定以外に色々な値が追加で設定されていますね。
:package
, :description
などは Hex
に登録する時に使用されるメタデータとして設定しているようです。
次は Jason.Mixfile
と Example.MixProject
で共通に定義されている application
関数について少し調べてみましょう。
application 関数
application
関数の定義は必須ではありません。
Run “mix help compile.app” to learn about applications.
とコメントで書かれているので確認してみましょう。
まず mix compile.app
というタスクは、 .app
ファイルを作成するタスクです。
Writes an .app file.
.app
ファイルというのは、Erlang
で記載されたアプリケーションを動かすための設定ファイルです。 mix.exs
に記載した設定が各処理を通過した後、最終的に .app
ファイルになると考えればいいでしょう。
An .app file is a file containing Erlang terms that defines your application.
Mix automatically generates this file based on your mix.exs configuration.
そして、 application
関数を定義することで、追加でアプリケーションの設定が可能になります。
In order to generate the .app file, Mix expects your project to have both :app
and :version keys. Furthermore, you can configure the generated application by
defining an application/0 function in your mix.exs that returns a keyword list.
application
関数は、コンパイル処理で、 .app
ファイルを作成する過程で呼び出されます。
project
関数は必須ですが、 application
は追加的設定のキーワードリストを返すので、必須ではありません。
application
関数を未定義にしても、関数の存在チェックが行われて、存在が確認できたら実行されるようになっています。
Example.MixProject
の application
関数を見て見ましょう。
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
:extra_applications
として :logger
が設定されていますね。 :logger
はElixirにデフォルトで同梱されているログ出力アプリケーションです。
:extra_applications
に設定されているアプリケーション(今回の場合 :logger
)は、メインアプリケーション(今回の場合 example
)の起動時に、プロセスが起動されるようになります。
具体的に示すと、 :logger
は、 Logger
アプリケーションです。これはElixirプロジェクトでもあるので、 mix.exs
を持っています。
defmodule Logger.MixProject do
use Mix.Project
def project do
[
app: :logger,
version: System.version(),
build_per_environment: false
]
end
def application do
[
registered: [Logger, Logger.BackendSupervisor, Logger.Supervisor, Logger.Watcher],
mod: {Logger.App, []},
env: [
utc_log: false,
truncate: 8096,
backends: [:console],
translators: [{Logger.Translator, :translate}],
sync_threshold: 20,
discard_threshold: 500,
handle_otp_reports: true,
handle_sasl_reports: false,
discard_threshold_periodic_check: 30_000,
discard_threshold_for_error_logger: 500,
compile_time_purge_matching: [],
compile_time_application: nil,
translator_inspect_opts: [],
console: [],
start_options: []
]
]
end
end
Logger
アプリケーションのエントリポイントは、 application
関数の中の :mod
で定義されていて、 Logger.App
です。 Logger.App
にはプロセスを起動するための start
関数が定義されていることがルールで、自動的に start
関数が呼ばれて Logger
アプリケーションのプロセスが起動されます。
defmodule Logger.App do
@moduledoc false
require Logger
use Application
@doc false
def start(_type, _args) do
start_options = Application.get_env(:logger, :start_options)
otp_reports? = Application.fetch_env!(:logger, :handle_otp_reports)
counter = :counters.new(1, [:atomics])
children = [
%{
id: :gen_event,
start: {:gen_event, :start_link, [{:local, Logger}, start_options]},
modules: :dynamic
},
{Logger.Watcher, {Logger.Config, counter}},
Logger.BackendSupervisor
]
case Supervisor.start_link(children, strategy: :rest_for_one, name: Logger.Supervisor) do
{:ok, sup} ->
primary_config = add_elixir_handler(otp_reports?, counter)
default_handlers =
if otp_reports? do
delete_erlang_handler()
else
[]
end
handlers = [{:primary, primary_config} | default_handlers]
{:ok, sup, handlers}
{:error, _} = error ->
error
end
end
@doc false
def start do
Application.start(:logger)
end
# ... 省略
end
スーパーバイザーやプロセスについては奥深いので、ここでは一旦、置いておきます。
application
関数で返す設定によって、依存アプリケーションを登録することができる、と覚えておけばいいでしょう。
以上で mix.exs
の読み進めは終わりにします。
おまけ
書籍『プログラミング Elixir(v1.2版)』では、HTTPoison
というHTTPクライアントライブラリを依存関係に追加し、 :applications
に :httpoison
を追加する記述があります。
defmodule Issues.Mixfile do
use Mix.Project
def project do
[
app: :issues,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps
]
end
def application do
[
applications: [:logger, :httpoison]
]
end
defp deps do
[
{ :httpoison, "~> 0.8" }
]
end
end
Elixirのバージョンが 1.3
まではこの書き方で、依存関係のあるアプリケーションのプロセスを起動していました。
しかし、 1.4
で仕様が変わり、:deps
に設定したアプリケーションはMixが自動的に :extra_applications
と認識してくれるようになりました。
つまり、 deps
にあるアプリケーションは、 application
関数の :extra_applications
に明示的に追加しなくてもプロセスの起動がされる、ということです。
よって、 1.4
以降では上記の mix.exs
は次のようにします。
defmodule Issues.Mixfile do
use Mix.Project
def project do
[
app: :issues,
version: "0.0.1",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[
{ :httpoison, "~> 0.8" }
]
end
end
以上、おまけでした。
まとめ
今回の記事についてまとめます。
mix.exs
ファイルにモジュールで定義する- モジュールの定義には
defmodule
を使う use Mix.Project
を記述するproject
関数の定義は必須であるapplication
関数の定義はオプションである:extra_applications
で他に依存するアプリケーションを登録できる1.4
以降は、deps
に入れたアプリケーションは:extra_applications
に明示的に追加しなくてOK