Pattern-matching in Elixir
Elixir is an Erlang-derived language, and as has carried over one of the nicest parts of Erlang, pattern-matching.
Pattern-matching is an old technique, and now exists in several languages 1, and allows you to split out handling of return-values from functions, and generally minimise the amount of code in functions and methods.
The implementation in Erlang and Elixir is a bit different, in that functions can directly match the args, and the underlying virtual-machine will pick the first matching function.
John Hamelink’s nice sms_blitz library for Elixir can be tidied up a bit with some pattern matching, that I think illustrates how to apply patterns to functions.
The original source for this file looks like this
def send_sms(%{uri: uri, auth: %{key: key, secret: secret}}, from: from, to: to, message: message) when is_binary(from) and is_binary(to) and is_binary(message) do
body = %{
from: from,
to: to,
text: message,
api_key: key,
api_secret: secret
} |> Poison.encode!
{:ok, %{headers: headers, body: resp, status_code: status_code}} = HTTPoison.post(
uri,
body,
[{"Content-Type", "application/json"}]
)
{:ok, %{"message-count" => msg_count, "messages" => [response_status]}} = Poison.decode(resp)
if response_status["status"] == "0" do
%{
id: response_status["message-id"],
result_string: "success",
status_code: response_status["status"]
}
else
{_key, trace_id} = Enum.find(headers, fn
({"X-Nexmo-Trace-Id", value}) -> true
(_) -> false
end)
%{
id: trace_id,
result_string: response_status["error-text"],
status_code: response_status["status"]
}
end
end
At 11 lines long (flattening the whitespace), this isn’t a huge function, it sets up a JSON body, then posts it using the HTTPoison library, and then handles the response, decoding and returning a standard response shared amongst all the various SMS adapters in the library.
But, I can hopefully clarify this a fair bit using pattern-matching…
def send_sms(
%Config{} = conf,
from: from,
to: to,
message: message
)
when is_binary(from) and is_binary(to) and is_binary(message) do
body =
%{
from: from,
to: to,
text: message,
api_key: conf.api_key,
api_secret: conf.api_secret
}
|> Poison.encode!()
HTTPoison.post(conf.uri, body, [{"Content-Type", "application/json"}])
|> handle_response!
end
This has brought the send_sms
function down to just 5 lines (again ignoring whitespace)
But what happened to the response handling?
defp handle_response!({:ok, %{headers: headers, body: resp, status_code: 200}}) do
handle_messages(headers, Poison.decode!(resp))
end
This only decodes HTTP 200 responses…note that any other response will not be caught by this, and the Elixir process will crash, this is the same behaviour as before.
The if
conditional in the original function can be replaced by two separate
paths, in two separate functions.
This handles the “success” case…the status codes are listed on the Nexmo site.
defp handle_messages(_, %{"messages" => [%{"status" => status, "message-id" => message_id}]})
when status == "0" do
respond(message_id, "success", status)
end
Note that the matching will handle picking out the response fields from the decoded JSON.
There are two handle_messages functions and the second must handle the results that the first doesn’t, or…the process will crash.
The second needs the headers to extract the Trace-Id header…the first doesn’t use the headers.
defp handle_messages(headers, %{"messages" => [%{"status" => status, "error-text" => error_text}]}) do
{_, trace_id} =
Enum.find(headers, fn
{"X-Nexmo-Trace-Id", _} -> true
_ -> false
end)
respond(trace_id, error_text, status)
end
Another example of pattern-matching here, picking out the value of the
X-Nexmo-Trace-Id
header if supplied, Enum.find
returns the first element in
the list that the predicate function returns true to, in this case, the headers are a list of tuples, with {header, value}
.
And finally, strip out a further function to generate the response…
defp respond(id, result, status) do
%{
id: id,
result_string: result,
status_code: status
}
end