Elixir1.5とPhoenix1.3の変更内容

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Elixirのv1.5が7/25に、Phoenixのv1.3が7/29にそれぞれリリースされました。
遅くなりましたが、それぞれの変更点を公式ページを参考にしながら試してみます。

Elixir v1.5 released
Phoenix v1.3 released

Elixir 1.5

UTF-8 Atom function names and variables

アトムと変数にUTF-8を使用できるようになりました

test "ステータスコード200を受け取る" do
  assert handle_response(%HTTPoison.Response{status_code: 200,
                                             headers: %{},
                                             body: "[\"body\"]"}) == ["body"]
end

このテストケース名だと1.4まではエラーになります。

** (ArgumentError) argument error
    :erlang.binary_to_atom("test ステータスコード200を受け取る", :utf8)
    (ex_unit) lib/ex_unit/case.ex:411: ExUnit.Case.register_test/4

1.5では成功します。

Finished in 0.1 seconds
3 tests, 0 failures

IEx helpers and breakpoints

ElixirのREPLであるIExに様々な機能が追加されました

Autocompletion

変数やimportしたモジュールの関数が補完されるようになります

exports

モジュールの関数名を出力します

iex(1)> exports Enum
all?/1                all?/2                any?/1                any?/2
at/2                  at/3                  chunk/2               chunk/3
chunk/4               chunk_by/2            chunk_every/2         chunk_every/3
chunk_every/4         chunk_while/4         concat/1              concat/2
count/1               count/2               dedup/1               dedup_by/2
drop/2                drop_every/2          drop_while/2          each/2
empty?/1              fetch/2               fetch!/2              filter/2
filter_map/3          find/2                find/3                find_index/2
find_value/2          find_value/3          flat_map/2            flat_map_reduce/3
group_by/2            group_by/3            intersperse/2         into/2
into/3                join/1                join/2                map/2
map_every/3           map_join/2            map_join/3            map_reduce/3
max/1                 max/2                 max_by/2              max_by/3
member?/2             min/1                 min/2                 min_by/2
min_by/3              min_max/1             min_max/2             min_max_by/2
min_max_by/3          partition/2           random/1              reduce/2
reduce/3              reduce_while/3        reject/2              reverse/1
reverse/2             reverse_slice/3       scan/2                scan/3
shuffle/1             slice/2               slice/3               sort/1
sort/2                sort_by/2             sort_by/3             split/2
split_while/2         split_with/2          sum/1                 take/2
take_every/2          take_random/2         take_while/2          to_list/1
uniq/1                uniq/2                uniq_by/2             unzip/1
with_index/1          with_index/2          zip/1                 zip/2

runtime_info

IEx実行時の情報を出力できる

iex(1) runtime_info

## System and architecture
    
Elixir version:     1.5.0
OTP version:        20
ERTS version:       9.0
Compiled for:       x86_64-apple-darwin15.6.0
Schedulers:         4
Schedulers online:  4
    
## Memory
    
Total:              19 MB
Atoms:              258 kB
Binaries:           88 kB
Code:               6927 kB
ETS:                382 kB
Processes:          4855 kB
    
## Statistics / limits
    
Uptime:             3 minutes and 4 seconds
Run queue:          0
Atoms:              10234 / 1048576 (0% used)
ETS:                20 / 2053 (0% used)
Ports:              5 / 65536 (0% used)
Processes:          47 / 262144 (0% used)

IEx brakpoints

IExでbrakpointが貼れるようになりました。
簡単なデバッグであれば require IEx; IEx.pry が不要になりそうです。

iex(1)> break! Ehee.Gists.breakpoint_test/1
1
iex(5)> Ehee.Gists.brakpoint_test("世界")
Break reached: Ehee.Gists.brakpoint_test/1 (lib/ehee/gists.ex:8)
    
    6:   """
    7:
    8:   def brakpoint_test(message) do
    9:     IO.puts("こんにちは #{message}")
   10:   end
    
pry(1)> message
"世界"
pry(2)> whereami
Location: lib/ehee/gists.ex:8
    
    6:   """
    7:
    8:   def brakpoint_test(message) do
    9:     IO.puts("こんにちは #{message}")
   10:   end

Exception.blame

Debug情報を特定の例外に付けることができる機能です。
現在はFunctionClauseErrorのどの部分が一致しどの部分が一致しなかったかを説明するために使われています。
(赤字がマッチしなかった部分を表しています)

iex-exception-blame

Streamlined child specs

Supervisorの定義をModule指定でできるようになりました。以前より簡潔に記述することができます。

children = [
  MyApp.Repo,
  MyApp.Endpoint
]
    
Supervisor.start_link(children, strategy: :one_for_one)

引数が必要な場合はtupleで指定します。

children = [
  {MyApp.Repo, url: "ecto://localhost:4567/my_dev"},
  MyApp.Endpoint
]

@impl

どの関数がコールバックの実装かを@implでマークできるようになりました。
@implでコールバック関数をマークすることで他の関数と区別しやすくできます。
以下はPlugプロジェクトの例です。

defmodule MyApp do
  @behaviour Plug
    
  @impl true
  def init(_opts) do
    opts
  end
    
  @impl true
  def call(conn, _opts) do
    Plug.Conn.send_resp(conn, 200, "hello world")
  end

@impl属性を付けると以下の利点があります。

  • @impl true属性を付けると自動的に @doc falseとマークし、@doc属性を明示的につけない限りドキュメントを無効にする
  • @impl をコールバックではない関数にマークするとエラーになる(タイプミス、モジュールの動作定義が変更になった場合に気づきやすくなる)
  • ある実装で@impl属性を付けると、同じモジュールの他のすべての実装にも@impl属性を付ける

Phoenix 1.3

続けて、Phoenixの変更点です。

Phx.new

  • 1.3からジェネレーターのコマンドがphxになりました。以前のphoenixコマンドは1.4で削除されます
  • 1.2まではlibと同階層にwebディレクトリがありましたが、1.3からはlibの下にmy_app_webディレクトリができその中にcontrollers, views, templates, channelsが置かれるようになりました。

Contexts

1.3の大きな変更点です。ディレクトリ構成が従来のMVCではなくなりました。
ビジネスロジックを記述するlib/my_appディレクトリとwebに関するモジュール(controller, view, template, channel等)を置くlib/my_app_webディレクトリに別れました。

userリソースを作成してみます。コンテキスト名も指定します。

$ mix phx.gen.json Accounts User users email:string:unique

lib/my_app/の下にAccountsディレクトリとその中にAccountsモジュールが作成されました。
このモジュールに認証やユーザー登録のようなビジネスロジックを記述します。

初期生成時のコードは以下です

defmodule MyApp.Accounts do
  @moduledoc """
  The Accounts context.
  """
    
  import Ecto.Query, warn: false
  alias MyApp.Repo
  alias MyApp.Accounts.User
    
  def list_users do
    Repo.all(User)
  end
   
  def get_user!(id), do: Repo.get!(User, id)
    
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end
    
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end
    
  def delete_user(%User{} = user) do
    Repo.delete(user)
  end
    
  def change_user(%User{} = user) do
    User.changeset(user, %{})
  end
end

Schemaはaccounts/user.exに定義されています

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias MyApp.Accounts.User
    
  schema "users" do
    field :email, :string
    
    timestamps()
  end
    
  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:email])
    |> validate_required([:email])
    |> unique_constraint(:email)
  end
end

ControllerからはAccountsモジュールを経由してユーザー作成、ユーザー取得ロジックを呼び出しています。
他のコントローラからアカウント操作に関するビジネスロジックの再利用がやりやすくなることが目的です。

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, "index.html", users: users)
  end
    
  def new(conn, _params) do
    changeset = Accounts.change_user(%User{})
    render(conn, "new.html", changeset: changeset)
  end
    
  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

Contextを導入することの利点ですが、公式サイトでは以下のように説明しています。

  • アプリケーション(のロジック)が適切に分離され、メンテナンス、再利用しやすくなる
  • 以前のweb/modelsディレクトリの下にビジネスロジックが置かれている場合はファイルの関係性が見ただけではわからなかったが、Contextを導入することにより関係性が分かりやすくなりアプリケーションの見通しが良くなる
    • 例えばcontextsディレクトリにaccounts, salesディレクトリがあればこのアプリケーションにはアカウントシステムとセールスシステムがあることがコードを見なくてもすぐに分かる。

action_fallback

controllerのactionが失敗した場合の処理をまとめて一つの場所に記述できるようになりました。

以前までは複数のコントローラーで同じようなエラー処理を書いていました。

  def MyAppWeb.PageController do
    alias MyApp.CMS
    def show(conn, %{"id" => id}) do
      case CMS.get_page(id, conn.assigns.current_user) do
        {:ok, page} -> render(conn, "show.html", page: page)
        {:error, :not_found} ->
          conn
          |> put_status(404)
          |> render(MyAppWeb.ErrorView, :"404")
        {:error, :unauthorized} ->
          conn
          |> put_status(401)
          |> render(MyAppWeb.ErrorView, :"401")
      end
    end
  end

1.3ではこう書くことができるようになりました。

  def MyAppWeb.PageController do
    alias MyApp.CMS
    
    action_fallback MyAppWeb.FallbackController 
    
    def show(conn, %{"id" => id}) do
      with {:ok, page} <- CMS.get_page(id, conn.assigns.current_user) do
        render(conn, "show.html", page: page)
      end
    end
  end    
    
  defmodule MyAppWeb.FallbackController do
    def call(conn, {:error, :not_found}) do
      conn
      |> put_status(:not_found)
      |> render(MyAppWeb.ErrorView, :"404")
    end
    
    def call(conn, {:error, :unauthorized}) do
      conn
      |> put_status(:unauthorized)
      |> render(MyAppWeb.ErrorView, :"401")
    end
  end
  • action_falbackでエラー処理を行うモジュールを指定する
  • controllerに actin_fallbackを指定し、with式でアクションの処理を記述する
  • エラー処理を一箇所にまとめることができる

Elixir1.2で入ったwith記法がうまく使われています。

まとめ

Elixirはv1.2で大きな変更がありましたが、その後は機能をより良くするようなアップデートが続いています。
v1.5ではユニコード対応やREPLのアップデートなど、プログラマにとってありがたい進化が多い印象です。
一方、Phoenixのv1.3はディレクトリ構造の変更など大きな変更が入り、今までのRailsっぽいフレームワークという印象から大きく変わります。
ファイルの置き場所に最初は迷いも出てくるかもしれませんが、再利用性、見通しの良さといった点が規模の大きなアプリケーションには大きな利点になりそうかなと思いました。

参考情報

Elixir v1.5 released
Phoenix 1.3.0 Released