Elixirでは、一つの関数に対して複数の句を定義することが出来ますが、Ecto.Changesetモジュールから抽出した下記の例の一番目の句のように、do部分が存在しない「ボディレス句(bodyless clause」を定義することが出来ます。
# 実装(doの部分)が存在しない
def cast(model_or_changeset, params, required, optional)
def cast(_model, %{__struct__: _} = params, _required, _optional) do
〜省略〜
end
def cast(%Changeset{changes: changes, model: model} = changeset, params, required, optional) do
〜省略〜
end
bodyless clauseは単体でも定義可能ですが、呼び出すことは出来ません。実装の存在する句と一緒に使われることが前提のようです。
このbodyless clauseがどういう用途で使われるのかということに関してですが、下記の記事によると3種類の用法があるようです。
Notes on Elixir: Bodyless Function Clauses
1. ドキュメンテーション用
例えば下記のような関数が定義されていた場合に、この関数をExDocを使ってドキュメント化すると、ドキュメントに記載される引数名には自動的にhash_dict
という名称が使用されます。
def size(%HashDict{size: size}) do
size
end
ドキュメント化される変数名を変更したい場合、下記のようなbodyless clauseを定義すると、ドキュメント化される変数名をdict
にすることが出来ます。これが一つ目の用法になります。
def size(dict)
def size(%HashDict{size: size}) do
size
end
2. デフォルト引数用
デフォルト引数をとる関数に複数の句を定義したい場合、bodyless clauseを使う必要があるようです。
こちらからのコピペですが、下記のような用法になります。
defmodule Concat do
def join(a, b \\ nil, sep \\ " ")
def join(a, b, _sep) when is_nil(b) do
a
end
def join(a, b, sep) do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello") #=> Hello
3. プロトコル用
3番目の用法はプロトコルです。当然といえば当然ですが、プロトコル定義には実装が存在しませんので、先日投稿した記事で解説させて頂いたex_adminの認証用のプロトコルだと下記のような感じでbodyless clauseが使用されています。
defprotocol ExAdmin.Authentication do
@fallback_to_any true
def use_authentication?(conn)
def current_user(conn)
def current_user_name(conn)
def session_path(conn, action)
end
おまけ
下記のコードのパターンマッチ部分の%{__struct__: _} = params
に関して、何をやってるんだろうと思った方がいるかもしれません。(私は思いました)
def cast(_model, %{__struct__: _} = params, _required, _optional) do
〜省略〜
end
こちらは要するに「構造体かどうか」をチェックしているコードのようです。下記で説明されているように、構造体は実際には「__struct__」という特別なフィールドを持っているMapなので、上記の構文にマッチすれば構造体だと判断する、ということのようです。
Structs are bare maps underneath
ガード節では駄目なのだろうか?という疑問が当然湧いてきますが、Elixirにはis_list
、is_tuple
、is_map
のように「リストかどうか/タプルかどうか/マップかどうか」をチェックする関数はあってもis_struct
という関数は存在しないので、上記のようにチェックせざるを得ないようです。