Chained semantic comparisons for Elixir.
CompareChain provides convenience macros for:
- chained comparisons (
a < b < c
) - semantic comparisons using structural operators (
<
,>
,<=
,>=
,==
,!=
,===
, and!==
) - combinations (
and
,or
, andnot
)
Add compare_chain
to your project's dependencies in mix.exs
and run mix deps.get
:
def deps do
[
{:compare_chain, "~> 0.5"}
]
end
Import CompareChain to enable access to CompareChain.compare?/1
and CompareChain.compare?/2
:
iex> import CompareChain
# Chained comparisons
iex> compare?(1 < 2 < 3)
true
# Semantic comparisons
iex> compare?(~D[2017-03-31] < ~D[2017-04-01], Date)
true
# Chained semantic comparisons
iex> compare?(~D[2017-03-31] < ~D[2017-04-01] < ~D[2017-04-02], Date)
true
# Semantic comparisons with logical operators
iex> compare?(~T[16:00:00] <= ~T[16:00:00] and not (~T[17:00:00] <= ~T[17:00:00]), Time)
false
# Complex expressions
iex> compare?(%{a: ~T[16:00:00]}.a <= ~T[17:00:00], Time)
true
See CompareChain on HexDocs for more.
Say you have an interval of time bounded by two %Date{}
structs, start_date
and end_date
, and you want to know whether or not a third date
falls within that interval. How would you write this in Elixir?
Date.compare(start_date, date) == :lt and
Date.compare(date, end_date) == :lt
The above code is verbose, somewhat hard to read, and potentially incorrect (though not obviously so). What if date
is considered "within" the interval inclusive of the start_date
or end_date
? To include the bounds in the comparison, you'd instead write the expression like this:
Date.compare(start_date, date) != :gt and
Date.compare(date, end_date) != :gt
# …or, even more verbosely:
Date.compare(start_date, date) in [:lt, :eq] and
Date.compare(date, end_date) in [:lt, :eq]
To spot the difference between these two cases, you must keep in mind:
- the order of the arguments passed to
Date.compare/2
, - the specific comparison operators for each clause (
==
vs.!=
), and - the specific comparison atoms for each clause (
:lt
vs.:gt
).
Contrast this example with equivalent Python code:
# excluding bounds
start_date < date < end_date
# including bounds
start_date <= date <= end_date
Much easier to read! Why can't you write this in Elixir? Two reasons:
- Structural comparison operators
- Chained vs. nested comparisons
Operators like <
do structural comparison (instead of semantic comparison). From the Kernel
docs:
…comparisons in Elixir are structural, as it has the goal of comparing data types as efficiently as possible to create flexible and performant data structures. This distinction is specially important for functions that provide ordering, such as
>/2
,</2
,>=/2
,<=/2
,min/2
, andmax/2
. For example:~D[2017-03-31] > ~D[2017-04-01]will return
true
because structural comparison compares the:day
field before:month
or:year
. In order to perform semantic comparisons, the relevant data-types provide acompare/2
function, such asDate.compare/2
:iex> Date.compare(~D[2017-03-31], ~D[2017-04-01]) :lt
In other words, although ~D[2017-03-31] > ~D[2017-04-01]
is valid code, it does not tell you if ~D[2017-03-31]
is a later date than ~D[2017-04-01]
as you might expect.
Instead, you'd use Date.compare/2
.
Additionally, even if ~D[2017-03-31] > ~D[2017-04-01]
did semantic comparison, you still couldn't write the interval check like you do in Python. In Python, an expression like 1 < 2 < 3
is syntactic sugar for (1 < 2) and (2 < 3)
(a series of "chained" expressions).
Elixir doesn't provide an equivalent syntactic sugar. Instead, 1 < 2 < 3
is evaluated as (1 < 2) < 3
(a series of "nested" expressions). (1 < 2) < 3
evaluates to true < 3
which is probably not what you want!
CompareChain addresses these complexities with the macro CompareChain.compare?/2
:
import CompareChain
# excluding bounds
compare?(start_date < date < end_date, Date)
# including bounds
compare?(start_date <= date <= end_date, Date)
CompareChain.compare?/2
rewrites these expressions as:
# excluding bounds
Date.compare(start_date, date) == :lt and
Date.compare(date, end_date) == :lt
# including bounds
Date.compare(start_date, date) != :gt and
Date.compare(date, end_date) != :gt
Your code is more readable while remaining correct!
CompareChain.compare?/1
also enables chained comparison using the structural operators:
compare?(1 < 2 < 3)
Thanks to Ben Wilson and Michael Crumm for the helpful discussions and their guidance!
Thanks as well to the folks who participated in the elixir-lang-core discussion, particularly Cliff whose idea I shamelessly built off.
CompareChain is freely available under the MIT License.