Skip to content

Commit

Permalink
Refactor README (#9)
Browse files Browse the repository at this point in the history
This PR updates the README with an eye toward clarity, brevity, and
consistency.

Notable changes include:

- moving Acknowledgements into this file
- aligning overall structure with our other open source projects like
[CargoSense/rubocop-cargosense](https://github.com/CargoSense/rubocop-cargosense)
- refactoring text for consistency, clarity, and correctness

Note that I didn't meaningfully changed the meaning or intent behind
anything and certainly didn't change the examples.
  • Loading branch information
jgarber623-cargosense authored Jun 14, 2024
2 parents 364c811 + 5517508 commit 3279c68
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 118 deletions.
5 changes: 0 additions & 5 deletions ACKNOWLEDGEMENTS.md

This file was deleted.

187 changes: 74 additions & 113 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
# CompareChain

## Description
**Chained semantic comparisons for Elixir.**

Provides convenience macros for comparisons which do:
[![Package](https://img.shields.io/hexpm/v/compare_chain?logo=elixir&style=for-the-badge)](https://hex.pm/packages/compare_chain)
[![Downloads](https://img.shields.io/hexpm/dt/compare_chain?logo=elixir&style=for-the-badge)](https://hex.pm/packages/compare_chain)
[![Build](https://img.shields.io/github/actions/workflow/status/CargoSense/compare_chain/ci.yml?branch=main&logo=github&style=for-the-badge)](https://github.com/CargoSense/compare_chain/actions/workflows/ci.yml)

* chained comparisons like: `a < b < c`
* semantic comparisons using the structural operators: `<`, `>`, `<=`, `>=`, `==`, `!=`, `===`, and `!==`
* combinations using: `and`, `or`, and `not`
## Key Features

### Examples
CompareChain provides convenience macros for:

- chained comparisons (`a < b < c`)
- semantic comparisons using structural operators (`<`, `>`, `<=`, `>=`, `==`, `!=`, `===`, and `!==`)
- combinations (`and`, `or`, and `not`)

## Installation

Add `compare_chain` to your project's dependencies in `mix.exs` and run `mix deps.get`:

```elixir
def deps do
[
{:compare_chain, "~> 0.5"}
]
end
```

## Usage

Import CompareChain to enable access to `CompareChain.compare?/1` and `CompareChain.compare?/2`:

```elixir
iex> import CompareChain
Expand All @@ -21,156 +41,103 @@ true
iex> compare?(~D[2017-03-31] < ~D[2017-04-01], Date)
true

# Chained, semantic comparisons
# 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

# More complex expressions
# Complex expressions
iex> compare?(%{a: ~T[16:00:00]}.a <= ~T[17:00:00], Time)
true
```

## Installation

Add `compare_chain` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:compare_chain, "~> 0.5"}
]
end
```

Documentation can be found at <https://hexdocs.pm/compare_chain>.
See [CompareChain on HexDocs](https://hexdocs.pm/compare_chain) for more.

## Usage
## Background

Once installed, you can add:

```elixir
import CompareChain
```

to your `defmodule` and you will have access to `CompareChain.compare?/1` and `CompareChain.compare?/2`.

## Background and motivation

`CompareChain` was originally motivated by the following situation:

> You have an interval of time bounded by a two `%Date{}` structs: `start_date` and `end_date`.
> You want to know if some third `date` falls in that interval.
> How do you write this?
In Elixir, we'd write this as follows:
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?

```elixir
Date.compare(start_date, date) == :lt and
Date.compare(date, end_date) == :lt
```

This is verbose and therefore a little hard to read.
It's also potentially incorrect, though not obviously so.
What if `date` is considered "within" the interval even if it equals `start_date` or `end_date`?
To include the bounds in our comparison, we'd instead write the expression like this:
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:

```elixir
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]
```

(We could have written `Date.compare(start_date, date) in [:lt, :eq]`, but `!= :gt` is faster.)
To spot the difference between these two cases, you must keep in mind:

In order to spot the difference between these two cases, you have to keep several things 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`).

* The order of the arguments passed to `Date.compare/2`
* The specific comparison operators for each clause: `==` vs. `!=`
* The specific comparison atoms for each clause: `:lt` vs. `:gt`
Contrast this example with equivalent Python code:

Since this is hard to read, it's easy to introduce bugs.
Contrast this with how you'd write the equivalent code in Python:
```python
# excluding bounds
start_date < date < end_date

```
start_date < date < end_date # excluding bounds
start_date <= date <= end_date # including bounds
# including bounds
start_date <= date <= end_date
```

This is much easier to read.
So why can't we write this in Elixir?
Two reasons:
Much easier to read! Why can't you write this in Elixir? Two reasons:

* Structural comparison operators
* Chained vs. nested comparisons
1. Structural comparison operators
2. Chained vs. nested comparisons

### Structural comparison operators
### Structural Comparison Operators

Operators like `<` do _structural_ comparison instead of _semantic_ comparison.
From the [`Kernel` docs](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
Operators like `<` do _structural_ comparison (instead of _semantic_ comparison). From the [`Kernel` docs](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):

> ... **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`, and `max/2`. For example:
> **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`, and `max/2`. For example:
>
> ```elixir
> ~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 a `compare/2` function, such as
`Date.compare/2`:
> 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 a `compare/2` function, such as `Date.compare/2`:
>
> ```elixir
> 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 perfectly valid code, it does _not_ tell you if `~D[2017-03-31]` is a later date than `~D[2017-04-01]` like you might expect.
Instead, you need to use `Date.compare/2`.
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`.
### Chained vs. nested comparisons
### Chained vs. Nested Comparisons
Additionally, even if `~D[2017-03-31] > ~D[2017-04-01]` did do semantic comparison, you still couldn't write the interval check like you do in Python.
This is because in Python, an expression like `1 < 2 < 3` is syntactic sugar for `(1 < 2) and (2 < 3)`, aka a series of "chained" expressions.
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 does not provide an equivalent syntactic sugar.
Instead, `1 < 2 < 3` is evaluated as `(1 < 2) < 3`, aka a series of "nested" expressions.
Since `(1 < 2) < 3` simplifies to `true < 3`, that's probably not what you want!
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!
Elixir will even warn you when you attempt an expression like that:
### A Solution!
> warning: Elixir does not support nested comparisons. Something like
>
> x < y < z
>
> is equivalent to
>
> (x < y) < z
>
> which ultimately compares z with the boolean result of (x < y). Instead, consider joining together each comparison segment with an "and", for example,
>
> x < y and y < z
### CompareChain
`CompareChain` attempts to address both of these issues with the macro `CompareChain.compare?/2`.
Its job is to take code similar to how you'd like to write it and rewriting it to be semantically correct.
For our motivating example, we'd write this:
CompareChain addresses these complexities with the macro `CompareChain.compare?/2`:
```elixir
import CompareChain
compare?(start_date < date < end_date, Date) # excluding bounds
compare?(start_date <= date <= end_date, Date) # including bounds
# excluding bounds
compare?(start_date < date < end_date, Date)
# including bounds
compare?(start_date <= date <= end_date, Date)
```
And at compile time, `CompareChain.compare?/2` rewrites those to be:
`CompareChain.compare?/2` rewrites these expressions as:
```elixir
# excluding bounds
Expand All @@ -182,26 +149,20 @@ Date.compare(start_date, date) != :gt and
Date.compare(date, end_date) != :gt
```
This way your code is more readable while still remaining correct.
Your code is more readable while remaining correct!
`CompareChain.compare?/1` is also available in case you only need chained comparison using the structural operators:
`CompareChain.compare?/1` also enables chained comparison using the structural operators:
```elixir
compare?(1 < 2 < 3)
```
Though I find this case comes up less often.
## Acknowledgements
### One last selling point
Thanks to [Ben Wilson](https://github.com/benwilson512) and [Michael Crumm](https://github.com/mcrumm) for the helpful discussions and their guidance!
As a happy accident, `CompareChain.compare?/2` always uses fewer characters than its `compare/2` counterpart:

```elixir
compare?(a <= b, Date)
# vs.
Date.compare(a, b) != :gt
```
Thanks as well to the folks who participated in the [elixir-lang-core](https://groups.google.com/g/elixir-lang-core) discussion, particularly Cliff whose [idea](https://groups.google.com/g/elixir-lang-core/c/W2TeQm5r1H4/m/ctVuN_woBgAJ) I shamelessly built off.
(Assuming you've already included `import CompareChain`, of course!)
## License
Because it's shorter _and_ more readable, these days I always use `CompareChain` for any semantic comparison, chained or not.
CompareChain is freely available under the [MIT License](https://opensource.org/licenses/MIT).

0 comments on commit 3279c68

Please sign in to comment.