<Enjoy-Elixir>mix.exsにいて少し詳しく

目次

Enjoy-Elixirと称する、プログラミング言語Elixirを勉強する記事群の1つです。

mix.exs

mix.exs は Elixirプロジェクト(Mixプロジェクト)の定義ファイルです。

Elixirプロジェクト(または単にアプリケーション、コンポーネントなどど呼んでもいいですが)は、 mix.exs の内容を元に初期化を行って仕事ができる状態になります。

この記事では、ElixirのJSONライブラリ michalmuskala/jasonmix.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.exs
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

  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 になっていますね。細かいところですが、 MixfileMixProject の違いは何でしょうか?

この違いは、 mix.exs を生成したElixirのバージョンの違いによるものです。

v1.5.3 までは Mixfile が使われていましたが、 v1.6 から MixProject になっています。

jasonmix.exsv1.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 defmacro __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.Mixfileuse Mix.Project が呼ばれると何が起こるか説明します。

defmodule Jason.Mixfile do
  use Mix.Project
  #
  # ... 省略
  #
end
  • Jason.Mixfile モジュールをコンパイルしようとします。
  • use Mix.ProjectMix.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.Mixfileproject を見てみましょう。

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.MixfileExample.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.MixProjectapplication 関数を見て見ましょう。

  # 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

参考