この記事は公開されてから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のどの部分が一致しどの部分が一致しなかったかを説明するために使われています。
(赤字がマッチしなかった部分を表しています)
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
ディレクトリがあればこのアプリケーションにはアカウントシステムとセールスシステムがあることがコードを見なくてもすぐに分かる。
- 例えばcontextsディレクトリに
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っぽいフレームワークという印象から大きく変わります。
ファイルの置き場所に最初は迷いも出てくるかもしれませんが、再利用性、見通しの良さといった点が規模の大きなアプリケーションには大きな利点になりそうかなと思いました。