マクロでTypespecsも書く
前の記事でもマクロでゴニョゴニョするときのことを書いたけど、今回もそれ系。
前提
マクロで関数定義してるとします。使う例は以下のAPIクライアント。Enum.map
で回しててdefmacro
使ってないけど、やりたいことはcompile timeに関数を定義するってことなのでその辺は勘弁。
defmodule MyAPI do [ %{fun_name: :get_profile, url: "..."} ] |> Enum.map(fn %{fun_name: fun_name, url: url} -> def unquote(fun_name)(params) do HTTP.get(unquote(url), params) |> handle_response() end end) end
関数名とURLを受け取って定義してる。これでMyAPI.get_profile/1
が定義できちゃう。
やりたいこと
ここに@spec
も書きたい。Profile.t()
が用意されているとして、それを返す関数だよ〜ということを書いておきたい。
いつもなら
@spec get_profile(params :: map()) :: Profile.t()
なので、
defmodule MyAPI do [ %{fun_name: :get_profile, url: "...", return_type: Profile} ] |> Enum.map(fn %{fun_name: fun_name, url: url, return_type: module} -> @spec unquote(fun_name)(params :: map()) :: unquote(module).t() def unquote(fun_name)(params) do # ... end end) end
これでいける。簡単かよ。なお、%{return_type: Profile.t()}
はできない。t/0
なんて関数はありませんと怒られます。
ではちょっと難しくして、Post.t()
のリストを返す関数を追加。
defmodule MyAPI do [ %{fun_name: :get_profile, url: "...", return_type: Profile}, %{fun_name: :get_posts, url: "...", return_type: [Post]} # <- 追加 ] |> Enum.map(fn %{fun_name: fun_name, url: url, return_type: module} -> @spec unquote(fun_name)(params :: map()) :: unquote(module).t() def unquote(fun_name)(params) do # ... end end) end
invalid remote in typespec: [Post].t()
でコンパイルエラー。ここは[Post.t()]
とかlist(Post.t())
とか書かないといけない。じゃあこの変換をやってくれる君をつくろう。
defmodule Type do def convert([return_type]) do [{{:., [], [return_type, :t]}, [], []}] # [return_type.t()]のAST end def convert(return_type) do {{:., [], [return_type, :t]}, [], []} # return_type.t()のAST end end defmodule MyAPI do [ %{fun_name: :get_profile, url: "...", return_type: Profile}, %{fun_name: :get_posts, url: "...", return_type: [Post]} ] |> Enum.map(fn %{fun_name: fun_name, url: url, return_type: return_type} -> @spec unquote(fun_name)(params :: map()) :: unquote(Type.convert(return_type)) # ASTをunquote def unquote(fun_name)(params) do # ... end end) end
急にどうしたんだという感じだけど、ASTをunquote
に渡してあげてる。これで実際IEx立ち上げるなどしてspec見ると、それぞれProfile.t()
と[Post.t()]
が返ると書かれている。上でちょっと触れたけどreturn_type.t()
は関数ではないので呼び出したらそこでエラーになってしまうため、ASTの形で扱っている。ここでさらにマクロを書くともっとキレイに、ASTを意識せずに書ける気がしているんだけどうまくできなかった。なお、自分は最初
@spec unquote(fun_name)(params :: map()) :: Type.convert(unquote(return_type))
と書いてしまい、Type.convert/1
という@type
があってそれを返すんですね〜と解釈されて、そうじゃないんです...みたいな気持ちになりました。
まとめ
@spec
ではASTをunquote
。