Skip to content

Instantly share code, notes, and snippets.

@danielgracia
Last active December 7, 2017 17:13
Show Gist options
  • Save danielgracia/094a24b39b75b58c4bc69fa4128b8dfa to your computer and use it in GitHub Desktop.
Save danielgracia/094a24b39b75b58c4bc69fa4128b8dfa to your computer and use it in GitHub Desktop.
Coalesce macro
defmodule Coalesce do
@moduledoc """
Provides a "coalesce" macro.
This macro is partly inspired by the COALESCE function in the SQL-92 standard.
The original COALESCE function is a function that returns the first non-null
computed expression passed as one of its argument, or NULL itself if no such
expression is found.
The "coalesce" macro, by comparison, accepts a compile-time list of expressions to evaluate,
and returns the first one with a truthy value - which means that the macro will skip both
`nil` and `false`.
Ultimately, this is just a cleaner way to write a sequence of `||` operations. Where you would to this:
var = do_operation(:a) || do_operation(:b) || fallback(:a) || fallback(:b) || [...]
Or this:
var = do_operation(:a)
var = var || do_operation(:b)
var = var || fallback(:a)
[...]
You can instead do this:
import Coalesce
var = coalesce [do_operation(:a), do_operation(:b), fallback(:a), ...]
The coalesce macro is short-circuiting, and won't evaluate any expressions beyond the first truthy one.
"""
@doc """
Selects and returns the first truthy expression in a list.
The argument must be a compile-time list. In other words, it needs to be a list literal, although
the values of the list themselves do not need to be literal values.
If you need to return the first truthy value from a list created at run time,
you can just use `Enum.find(& &1)`.
## Examples
iex> Coalesce.coalesce([false == true, 3, IO.puts("lol")])
3
"""
defmacro coalesce([_|_] = list) do
do_coalesce(list)
end
defmacro coalesce(any) do
string = Macro.to_string(any)
raise "coalesce/1 must receive a compile-time list of expressions, got: #{string}"
end
defp do_coalesce([expr]) do
quote do: unquote(expr)
end
defp do_coalesce([expr | tail]) do
quote do
value = unquote(expr)
if value do
value
else
unquote(do_coalesce(tail))
end
end
end
end
defmodule CoalesceTest do
use ExUnit.Case
doctest Coalesce
import Coalesce
test "uses a long coalesce" do
assert coalesce([
1 + 1 == 3,
Map.get(%{a: :b}, :c),
[
1 + 1,
2 + 2,
3 + 3,
Map.get(%{a: :d}, :a)
]
]) == [2, 4, 6, :d]
end
test "short-circuiting" do
assert coalesce([
true,
raise("coalesce ended")
]) == true
end
test "still runs through" do
assert_raise RuntimeError, "coalesce ended", fn ->
coalesce([
false,
raise("coalesce ended")
])
end
end
test "as captured function" do
list = [
[1, false, false],
[false, 2, false],
[false, false, 3],
[false, false, false]
]
pass_along = fn fun ->
Enum.map(list, &apply(fun, &1))
end
assert pass_along.(&coalesce([&1, &2, &3])) == [1, 2, 3, false]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment