defmodule ExActor.Operations do @moduledoc """ Macros that can be used for simpler definition of `GenServer` operations such as casts or calls. """ @doc """ Defines the starter function and initializer body. # defines and export start/2 defstart start(x, y) do # runs in init/1 callback initial_state(x + y) end # defines and export start_link/2 defstart start_link(x, y) do # runs in init/1 callback initial_state(x + y) end You can also provide additional `GenServer` options via `:gen_server_opts` option. defstart start(x, y), gen_server_opts: [spawn_opts: [min_heap_size: 10000]], do: ... If you need to set `GenServer` options at runtime, use `gen_server_opts: :runtime` and then the starter function will receive one more argument where you can pass options: defstart start(x, y), gen_server_opts: :runtime do ... end ... MyServer.start(x, y, name: :foo, spawn_opts: [min_heap_size: 10000]) Body can be omitted. In this case, just the interface function is generated. This can be useful if you want to define both `start` and `start_link`: defstart start(x, y) defstart start_link(x, y) do # runs for both cases end Keep in mind that generated `info/1` matches on the number of arguments, so this won't work: defstart start_link(x) defstart start_link(x, y) do # doesn't handle start_link(x) end If you want to handle various versions, you can just define start heads without the body, and then use `defhandleinfo/2` or just implement `handle_info/1`. ## Other notes - If the `export` option is set while using `ExActor`, it will be honored in starters. - You can use patterns in arguments. Pattern matching is performed in the interface function. For each specified clause, there will be one corresponding interface function clause. - You can provide additional guard via `:when` option. The guard applies to the `init/1`. ### Request format (arg passed to `init/1`) - no arguments -> `nil` - one arguments -> `{x}` - more arguments -> `{x, y, ...}` """ defmacro defstart({fun, _, args}, opts \\ [], body \\ []) do define_starter(false, fun, args, opts ++ body) end @doc """ Same as `defstart/2` but the interface function is private. Can be useful when you need to do pre/post processing in the caller process. defmodule MyServer do def start_link(x, y) do ... do_start_link(x, y) ... end defstartp do_start_link(x, y), link: true do ... end end """ defmacro defstartp({fun, _, args}, options \\ [], body \\ []) do define_starter(true, fun, args, options ++ body) end defp define_starter(private, fun, args, options) do quote bind_quoted: [ private: private, fun: Macro.escape(fun, unquote: true), args: Macro.escape(args || [], unquote: true), options: Macro.escape(options, unquote: true) ] do {interface_matches, payload, match_pattern} = ExActor.Operations.start_args(args) {arity, interface_matches, gen_server_fun, gen_server_opts} = ExActor.Operations.prepare_start_interface(fun, interface_matches, options, @exactor_global_options) unless private do def unquote(fun)(unquote_splicing(interface_matches)) do GenServer.unquote(gen_server_fun)(__MODULE__, unquote(payload), unquote(gen_server_opts)) end else defp unquote(fun)(unquote_splicing(interface_matches)) do GenServer.unquote(gen_server_fun)(__MODULE__, unquote(payload), unquote(gen_server_opts)) end end if options[:do] do definit( unquote(match_pattern), unquote(Keyword.take(options, [:when]) ++ [do: options[:do]]) ) end end end @doc false def extract_args(args) do arg_names = for {arg, index} <- Enum.with_index(args) do case arg do {:\\, _, [{arg_name, _, _} = inner_arg, _]} when is_atom(arg_name) and arg_name != := -> inner_arg {arg_name, _, _} when is_atom(arg_name) and not (arg_name in [:_, :\\, :=]) -> arg _ -> Macro.var(:"arg#{index}", __MODULE__) end end interface_matches = for {arg, arg_name} <- Enum.zip(args, arg_names) do case arg do {:\\, context, [match, default]} -> {:\\, context, [quote(do: unquote(match) = unquote(arg_name)), default]} match -> quote(do: unquote(match) = unquote(arg_name)) end end args = for arg <- args do case arg do {:\\, _, [match, _]} -> match _ -> arg end end {arg_names, interface_matches, args} end @doc false def start_args(args) do {arg_names, interface_matches, args} = extract_args(args) {payload, match_pattern} = case args do [] -> {nil, nil} [_|_] -> { quote(do: {unquote_splicing(arg_names)}), quote(do: {unquote_splicing(args)}) } end {interface_matches, payload, match_pattern} end @doc false def prepare_start_interface(fun, interface_matches, options, global_options) do interface_matches = unless options[:gen_server_opts] == :runtime do interface_matches else interface_matches ++ [quote(do: unquote(Macro.var(:gen_server_opts, __MODULE__)) \\ [])] end arity = length(interface_matches) gen_server_fun = case (options[:link]) do true -> :start_link false -> :start nil -> if fun in [:start, :start_link] do fun else raise "Function name must be either start or start_link. If you need another name, provide explicit :link option." end end gen_server_opts = unless options[:gen_server_opts] == :runtime do case global_options[:export] do default when default in [nil, false] -> [] name -> [name: Macro.escape(name)] end ++ (options[:gen_server_opts] || []) else Macro.var(:gen_server_opts, __MODULE__) end {arity, interface_matches, gen_server_fun, gen_server_opts} end @doc """ Similar to `defstart/3` but generates just the `init` clause. Note: keep in mind that `defstart` wraps arguments in a tuple. If you want to handle `defstart start(x)`, you need to define `definit {x}` """ defmacro definit(arg \\ quote(do: _), opts), do: do_definit([{:arg, arg} | opts]) defp do_definit(opts) do quote bind_quoted: [opts: Macro.escape(opts, unquote: true)] do if (opts[:when]) do def init(unquote_splicing([opts[:arg]])) when unquote(opts[:when]), do: unquote(opts[:do]) else def init(unquote_splicing([opts[:arg]])), do: unquote(opts[:do]) end end end @doc """ Defines the cast callback clause and the corresponding interface fun. See `defcall/3` for more details. """ defmacro defcast(req_def, options \\ [], body \\ []) do generate_funs(:defcast, req_def, options ++ body) end @doc """ Same as `defcast/3` but the interface function is private. Can be useful when you need to do pre/post processing in the caller process. def exported_interface(...) do # do some client side preprocessing here my_request(...) # do some client side post processing here end # Not available outside of this module defcastp my_request(...), do: ... """ defmacro defcastp(req_def, options \\ [], body \\ []) do generate_funs(:defcast, req_def, [{:private, true} | options] ++ body) end @doc """ Defines the call callback clause and the corresponding interface fun. defcall operation, do: reply(response) defcall get, state: state, do: reply(state) defcall inc(x \\\\ 1), state: state, do: set_and_reply(state + x, response) ## Request format (passed to `handle_call/3`) - no arguments -> `:my_request` - one arguments -> `{:my_request, x}` - more arguments -> `{:my_request, x, y, ...}` ## Timeout defcall long_call, state: state, timeout: :timer.seconds(10), do: ... You can also make the timeout parameterizable defcall long_call(...), timeout: some_variable, do: ... This will generate the interface function as: def long_call(..., some_variable) where `some_variable` will be used as the timeout in `GenServer.call`. You won't have the access to this variable in your body though, since the body specifies the handler function. Default timeout value can also be provided via standard `\\\\` syntax. ## Pattern matching defcall a(1), do: ... defcall a(2), do: ... defcall a(x), when: x > 1, do: ... defcall a(x), state: 1, do: ... defcall a(_), state: state, do: ... ### Details A single `defcall` (or similar) construct usually defines a clause for two functions: the interface function and the handler function. For example: defcall foo(x, y), state: state, do: ... will generate: def foo(pid, x, y), do: ... def handle_call({:foo, x, y}, _, state), do: ... If you're writing multi-clauses, the following rules apply: - Arguments are pattern-matched in interface and in handler function. - The `when:` clause applies only to the interface function. - The `state` argument is pattern-matched in the handler function. `ExActor` will try to be smart to some extent, and defer from generating the interface clause if it's not needed. For example: defcall foo(_, _), state: nil, do: ... defcall foo(x, y), state: state, do: ... will generate only a single interface function that always matches its arguments and sends them to the server process. There will be of course two `handle_call` clauses. You can freely mix this with argument pattern matches: defcall foo(1, 2), ... defcall foo(x, y), when: x > y, ... defcall foo(_, _), state: nil, do: ... defcall foo(x, y), state: state, do: ... The example above will generate three interface clauses: - `def foo(1, 2)` - `def foo(x, y) when x > y` - `def foo(x, y)` Of course, there will be four `handle_call` clauses, each with the corresponding body provided via `do` option. ### Separating interface and handler clauses If you want to be more explicit about pattern matching, you can use a body-less construct: defcall foo(x, y) You can freely use multiple `defcall` body-less clauses if you need to pattern match arguments. To generate handler clauses you can use `defhandlecall/3`: defhandlecall foo(_, _), state: nil, do: ... defhandlecall foo(x, y), state: state, do: ... This approach requires some more typing, but it's more explicit. If you need to perform complex combinations of pattern matches on arguments and the state, it's probably better to use this technique as it gives you more control over what is matched at which point. Also, this approach allows you to use the state in the `when` option (guard): defcall a(x) defhandlecall a(x), state: state, when: state > 0, do: ... defhandlecall a(x), state: state, do: ... """ defmacro defcall(req_def, options \\ [], body \\ []) do generate_funs(:defcall, req_def, options ++ body) end @doc """ Same as `defcall/3` but the interface function is private. Can be useful when you need to do pre/post processing in the caller process. def exported_interface(...) do # do some client side preprocessing here my_request(...) # do some client side post processing here end # Not available outside of this module defcallp my_request(...), do: ... """ defmacro defcallp(req_def, options \\ [], body \\ []) do generate_funs(:defcall, req_def, [{:private, true} | options] ++ body) end @doc """ Similar to `defcall/3`, but generates just the `handle_call` clause, without creating the interface function. """ defmacro defhandlecall(req_def, options \\ [], body \\ []) do generate_request_def(:defcall, req_def, options ++ body) end @doc """ Similar to `defcast/3`, but generates just the `handle_call` clause, without creating the interface function. """ defmacro defhandlecast(req_def, options \\ [], body \\ []) do generate_request_def(:defcast, req_def, options ++ body) end # Generation of call/cast functions. Essentially, this is just # deferred to be evaluated in the module context. defp generate_funs(type, req_def, options) do quote bind_quoted: [ type: type, req_def: Macro.escape(req_def, unquote: true), options: Macro.escape(options, unquote: true) ] do ExActor.Operations.def_request(type, req_def, Keyword.merge(options, @exactor_global_options)) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) end end @doc false def def_request(type, req_def, options) do {req_name, interface_matches, payload, _} = req_args(req_def) quote do req_id = unquote(Macro.escape(req_id(req_def, options))) unless HashSet.member?(@generated_funs, req_id) do unquote(define_interface(type, req_name, interface_matches, payload, options)) @generated_funs HashSet.put(@generated_funs, req_id) end unquote(if options[:do] do implement_request(type, req_def, options) end) end end defp req_id({req_name, _, args}, options) do { req_name, Enum.map( strip_context(args || []), fn {var_name, _, scope} when is_atom(var_name) and is_atom(scope) -> :matchall other -> other end ), strip_context(options[:when]) } end defp req_id(req_name, options) when is_atom(req_name) do req_id({req_name, [], []}, options) end defp strip_context(ast) do Macro.prewalk(ast, fn {a, _context, b} -> {a, [], b} other -> other end ) end defp generate_request_def(type, req_def, options) do quote bind_quoted: [ type: type, req_def: Macro.escape(req_def, unquote: true), options: Macro.escape(options, unquote: true) ] do ExActor.Operations.implement_request(type, req_def, Keyword.merge(options, @exactor_global_options)) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) end end @doc false def implement_request(type, req_def, options) do {_, _, _, match_pattern} = req_args(req_def) quote do unquote(implement_handler(type, options, match_pattern)) end end defp req_args(req_def) do {req_name, args} = parse_req_def(req_def) {arg_names, interface_matches, args} = extract_args(args) {payload, match_pattern} = case args do [] -> {req_name, req_name} [_|_] -> { quote(do: {unquote_splicing([req_name | arg_names])}), quote(do: {unquote_splicing([req_name | args])}) } end {req_name, interface_matches, payload, match_pattern} end defp parse_req_def(req_name) when is_atom(req_name), do: {req_name, []} defp parse_req_def({req_name, _, args}), do: {req_name, args || []} # Defines the interface function to call/cast defp define_interface(type, req_name, interface_matches, payload, options) do quote bind_quoted: [ private: options[:private], type: type, req_name: req_name, server_fun: server_fun(type), interface_args: Macro.escape(interface_args(interface_matches, options), unquote: true), gen_server_args: Macro.escape(gen_server_args(options, type, payload), unquote: true), guard: Macro.escape(options[:when], unquote: true) ] do {interface_args, gen_server_args} = unless type in [:multicall, :abcast] do {interface_args, gen_server_args} else { [quote(do: nodes \\ [node() | :erlang.nodes()]) | interface_args], [quote(do: nodes) | gen_server_args] } end arity = length(interface_args) unless private do if guard do def unquote(req_name)(unquote_splicing(interface_args)) when unquote(guard) do GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) end else def unquote(req_name)(unquote_splicing(interface_args)) do GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) end end else if guard do defp unquote(req_name)(unquote_splicing(interface_args)) when unquote(guard) do GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) end else defp unquote(req_name)(unquote_splicing(interface_args)) do GenServer.unquote(server_fun)(unquote_splicing(gen_server_args)) end end end end end defp server_fun(:defcast), do: :cast defp server_fun(:defcall), do: :call defp server_fun(:multicall), do: :multi_call defp server_fun(:abcast), do: :abcast defp interface_args(args, options) do server_match(options[:export]) ++ args ++ timeout_match(options[:timeout]) end defp server_match(export) when export == nil or export == true, do: [quote(do: server)] defp server_match(_), do: [] defp timeout_match(nil), do: [] defp timeout_match(:infinity), do: [] defp timeout_match(timeout) when is_integer(timeout), do: [] defp timeout_match(pattern), do: [pattern] defp gen_server_args(options, type, msg) do [server_ref(options, type), msg] ++ timeout_arg(options, type) end defp server_ref(options, op) when op in [:multicall, :abcast] do case options[:export] do local when is_atom(local) and local != nil and local != false -> local {:local, local} -> local _ -> quote(do: server) end end defp server_ref(options, _) do case options[:export] do default when default in [nil, false, true] -> quote(do: server) local when is_atom(local) -> local {:local, local} -> local {:global, _} = global -> global {:via, _, _} = via -> Macro.escape(via) end end defp timeout_arg(options, type) when type in [:defcall, :multicall] do case options[:timeout] do {:\\, _, [var, _default]} -> [var] timeout when timeout != nil -> [timeout] _ -> [] end end defp timeout_arg(_, _), do: [] @doc false # Implements the handler function (handle_call, handle_cast, handle_timeout) def implement_handler(type, options, msg) do state_arg = get_state_identifier(Keyword.fetch(options, :state)) {handler_name, handler_args} = handler_sig(type, options, msg, state_arg) quote bind_quoted: [ type: type, handler_name: handler_name, handler_args: Macro.escape(handler_args, unquote: true), guard: Macro.escape(options[:when], unquote: true), body: Macro.escape(options[:do], unquote: true) ] do if guard do def unquote(handler_name)(unquote_splicing(handler_args)) when unquote(guard), do: unquote(body) else def unquote(handler_name)(unquote_splicing(handler_args)), do: unquote(body) end end end defp get_state_identifier({:ok, match}), do: quote(do: unquote(match) = unquote(ExActor.Helper.state_var)) defp get_state_identifier(:error), do: get_state_identifier({:ok, quote(do: _)}) defp handler_sig(:defcall, options, msg, state_arg), do: {:handle_call, [msg, options[:from] || quote(do: _from), state_arg]} defp handler_sig(:defcast, _, msg, state_arg), do: {:handle_cast, [msg, state_arg]} defp handler_sig(:definfo, _, msg, state_arg), do: {:handle_info, [msg, state_arg]} @doc """ Defines the info callback clause. Responses work just like with casts. defhandleinfo :some_message, do: ... defhandleinfo :another_message, state: ..., do: """ defmacro defhandleinfo(msg, opts \\ [], body) do impl_defhandleinfo(msg, opts ++ body) end # Implements handle_info defp impl_defhandleinfo(msg, options) do quote bind_quoted: [ msg: Macro.escape(msg, unquote: true), options: Macro.escape(options, unquote: true) ] do options = Keyword.merge(options, @exactor_global_options) ExActor.Operations.implement_handler(:definfo, options, msg) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) end end @doc """ Defines a multicall operation. defmulticall my_request(x, y), do: ... ... # If the process is locally registered via `:export` option MyServer.my_request(2, 3) MyServer.my_request(nodes, 2, 3) # The process is not locally registered via `:export` option MyServer.my_request(:local_alias, 2, 3) MyServer.my_request(nodes, :local_alias, 2, 3) Request format is the same as in `defcall/3`. Timeout option works just like with `defcall/3`. """ defmacro defmulticall(req_def, options \\ [], body \\ []) do do_defmulticall(req_def, options ++ body) end @doc """ Same as `defmulticall/3` but the interface function is private. """ defmacro defmulticallp(req_def, options \\ [], body \\ []) do do_defmulticall(req_def, [{:private, true} | options] ++ body) end defp do_defmulticall(req_def, options) do quote bind_quoted: [ req_def: Macro.escape(req_def, unquote: true), options: Macro.escape(options, unquote: true) ] do options = Keyword.merge(options, @exactor_global_options) ExActor.Operations.implement_request(:defcall, req_def, options) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) ExActor.Operations.def_request(:multicall, req_def, Keyword.drop(options, [:do])) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) end end @doc """ Defines an abcast operation. defabcast my_request(x, y), do: ... ... # If the process is locally registered via `:export` option MyServer.my_request(2, 3) MyServer.my_request(nodes, 2, 3) # The process is not locally registered via `:export` option MyServer.my_request(:local_alias, 2, 3) MyServer.my_request(nodes, :local_alias, 2, 3) Request format is the same as in `defcall/3` """ defmacro defabcast(req_def, options \\ [], body \\ []) do do_defabcast(req_def, options ++ body) end @doc """ Same as `defabcast/3` but the interface function is private. """ defmacro defabcastp(req_def, options \\ [], body \\ []) do do_defabcast(req_def, [{:private, true} | options] ++ body) end defp do_defabcast(req_def, options) do quote bind_quoted: [ req_def: Macro.escape(req_def, unquote: true), options: Macro.escape(options, unquote: true) ] do options = Keyword.merge(options, @exactor_global_options) ExActor.Operations.implement_request(:defcast, req_def, options) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) ExActor.Operations.def_request(:abcast, req_def, Keyword.drop(options, [:do])) |> ExActor.Helper.inject_to_module(__MODULE__, __ENV__) end end end