improvement: remove :comparable as a dependency

It all compiles conditionally, so that if an explicit dependency exists
on `comp` it will still be used
This commit is contained in:
Zach Daniel 2024-08-05 16:04:47 -04:00
parent e94e484885
commit 58065c31cd
12 changed files with 493 additions and 22 deletions

View file

@ -499,9 +499,6 @@ defmodule Ash.Actions.Read do
end
end)
else
%Ash.Query{} = query ->
{{:error, query}, query}
{:ok, query} ->
{{:error, query}, query}

View file

@ -51,3 +51,9 @@ defmodule Ash.Type.Date do
Ecto.Type.dump(:date, value)
end
end
import Ash.Type.Comparable
defcomparable left :: Date, right :: Date do
Date.compare(left, right)
end

View file

@ -110,3 +110,9 @@ defmodule Ash.Type.DateTime do
Ecto.Type.dump(storage_type(constraints), value)
end
end
import Ash.Type.Comparable
defcomparable left :: DateTime, right :: DateTime do
DateTime.compare(left, right)
end

View file

@ -227,3 +227,21 @@ defmodule Ash.Type.Decimal do
def new(%Decimal{} = v), do: v
def new(v), do: Decimal.new(v)
end
import Ash.Type.Comparable
defcomparable left :: Decimal, right :: Integer do
Decimal.compare(left, Ash.Type.Decimal.new(right))
end
defcomparable left :: Decimal, right :: Decimal do
Decimal.compare(left, right)
end
defcomparable left :: Decimal, right :: Float do
Decimal.compare(Ash.Type.Decimal.new(left), right)
end
defcomparable left :: Decimal, right :: BitString do
Decimal.compare(left, Ash.Type.Decimal.new(right))
end

View file

@ -1,17 +1 @@
import Ash.Type.Comparable
defcomparable left :: Decimal, right :: Integer do
Decimal.compare(left, Ash.Type.Decimal.new(right))
end
defcomparable left :: Decimal, right :: Decimal do
Decimal.compare(left, right)
end
defcomparable left :: Decimal, right :: Float do
Decimal.compare(Ash.Type.Decimal.new(left), right)
end
defcomparable left :: Decimal, right :: BitString do
Decimal.compare(left, Ash.Type.Decimal.new(right))
end

View file

@ -51,3 +51,9 @@ defmodule Ash.Type.NaiveDatetime do
Ecto.Type.dump(:naive_datetime, value)
end
end
import Comp
defcomparable left :: NaiveDateTime, right :: NaiveDateTime do
NaiveDateTime.compare(left, right)
end

View file

@ -47,3 +47,9 @@ defmodule Ash.Type.Time do
Ecto.Type.dump(:time, value)
end
end
import Ash.Type.Comparable
defcomparable left :: Time, right :: Time do
Time.compare(left, right)
end

344
lib/comparable/comp.ex Normal file
View file

@ -0,0 +1,344 @@
unless Code.ensure_loaded?(Comp) do
defmodule Comp do
@moduledoc """
Provides utilities to implement and work with `Comparable` types
"""
@type left :: term
@type right :: term
defmacro gt, do: :gt
defmacro lt, do: :lt
defmacro eq, do: :eq
@doc """
Helper to define ordering relation for pair of types,
accepts two `term :: type` pairs
and block of code where relation is described.
## Examples
```
iex> quote do
...> use Comp
...> defmodule Foo do
...> defstruct [:value, :meta]
...> end
...> defmodule Bar do
...> defstruct [:value, :meta]
...> end
...> defcomparable %Foo{value: left} :: Foo, %Foo{value: right} :: Foo do
...> Comp.compare(left, right)
...> end
...> defcomparable %Foo{value: left} :: Foo, %Bar{value: right} :: Bar do
...> Comp.compare(left, right)
...> end
...> defcomparable %Foo{value: left} :: Foo, right :: Integer do
...> Comp.compare(left, right)
...> end
...> end
...> |> Code.compile_quoted
iex> quote do
...> x = %Foo{value: 1, meta: 1}
...> y = %Foo{value: 1, meta: 2}
...> Comp.equal?(x, y) && Comp.equal?(y, x)
...> end
...> |> Code.eval_quoted
...> |> elem(0)
true
iex> quote do
...> x = %Foo{value: 1, meta: 1}
...> y = %Bar{value: 1, meta: 2}
...> Comp.equal?(x, y) && Comp.equal?(y, x)
...> end
...> |> Code.eval_quoted
...> |> elem(0)
true
iex> quote do
...> x = %Foo{value: 1, meta: 1}
...> y = 1
...> Comp.equal?(x, y) && Comp.equal?(y, x)
...> end
...> |> Code.eval_quoted
...> |> elem(0)
true
```
"""
defmacro defcomparable(
{:"::", _, [left_expression, quoted_left_type]},
{:"::", _, [right_expression, quoted_right_type]},
do: code
) do
{left_type, []} = Code.eval_quoted(quoted_left_type, [], __CALLER__)
{right_type, []} = Code.eval_quoted(quoted_right_type, [], __CALLER__)
lr_type =
[Comparable, Type, left_type, To, right_type]
|> Module.concat()
rl_type =
[Comparable, Type, right_type, To, left_type]
|> Module.concat()
lr_impl =
quote do
defmodule unquote(lr_type) do
@fields [:left, :right]
@enforce_keys @fields
defstruct @fields
end
defimpl Comparable, for: unquote(lr_type) do
def compare(%unquote(lr_type){
left: unquote(left_expression),
right: unquote(right_expression)
}) do
unquote(code)
end
end
end
if lr_type == rl_type do
lr_impl
else
quote do
unquote(lr_impl)
defmodule unquote(rl_type) do
@fields [:left, :right]
@enforce_keys @fields
defstruct @fields
end
defimpl Comparable, for: unquote(rl_type) do
def compare(%unquote(rl_type){
left: unquote(right_expression),
right: unquote(left_expression)
}) do
unquote(code)
end
end
end
end
end
@doc """
Is left term equal to right term?
## Examples
```
iex> Comp.equal?(1, 1)
true
iex> Comp.equal?(1, :hello)
false
```
"""
@spec equal?(left, right) :: boolean
def equal?(left, right) do
left
|> new(right)
|> Comparable.compare() == eq()
end
@doc """
Is left term not equal to right term?
## Examples
```
iex> Comp.not_equal?(1, 1)
false
iex> Comp.not_equal?(1, :hello)
true
```
"""
@spec not_equal?(left, right) :: boolean
def not_equal?(left, right) do
left
|> new(right)
|> Comparable.compare() != eq()
end
@doc """
Is left term greater than right term?
## Examples
```
iex> Comp.greater_than?(1, 1)
false
iex> Comp.greater_than?(1, 2)
false
iex> Comp.greater_than?(2, 1)
true
"""
@spec greater_than?(left, right) :: boolean
def greater_than?(left, right) do
left
|> new(right)
|> Comparable.compare() == gt()
end
@doc """
Is left term less than right term?
## Examples
```
iex> Comp.less_than?(1, 1)
false
iex> Comp.less_than?(1, 2)
true
iex> Comp.less_than?(2, 1)
false
"""
@spec less_than?(left, right) :: boolean
def less_than?(left, right) do
left
|> new(right)
|> Comparable.compare() == lt()
end
@doc """
Is left term greater or equal to right term?
## Examples
```
iex> Comp.greater_or_equal?(1, 1)
true
iex> Comp.greater_or_equal?(1, 2)
false
iex> Comp.greater_or_equal?(2, 1)
true
"""
@spec greater_or_equal?(left, right) :: boolean
def greater_or_equal?(left, right) do
left
|> new(right)
|> Comparable.compare() != lt()
end
@doc """
Is left term less or equal to right term?
## Examples
```
iex> Comp.less_or_equal?(1, 1)
true
iex> Comp.less_or_equal?(1, 2)
true
iex> Comp.less_or_equal?(2, 1)
false
"""
@spec less_or_equal?(left, right) :: boolean
def less_or_equal?(left, right) do
left
|> new(right)
|> Comparable.compare() != gt()
end
@doc """
Returns the biggest of the two given terms, if terms are equal - then the first one is returned
## Examples
```
iex> Comp.max(1, 1)
1
iex> Comp.max(1, 2)
2
iex> Comp.max(2, 1)
2
```
"""
@spec max(left, right) :: left | right
def max(left, right) do
left
|> new(right)
|> Comparable.compare()
|> case do
gt() -> left
lt() -> right
eq() -> left
end
end
@doc """
Returns the smallest of the two given terms, if terms are equal - then the first one is returned
## Examples
```
iex> Comp.min(1, 1)
1
iex> Comp.min(1, 2)
1
iex> Comp.min(2, 1)
1
```
"""
@spec min(left, right) :: left | right
def min(left, right) do
left
|> new(right)
|> Comparable.compare()
|> case do
gt() -> right
lt() -> left
eq() -> left
end
end
def type_of(v) when is_atom(v), do: Atom
def type_of(v) when is_bitstring(v), do: BitString
def type_of(v) when is_float(v), do: Float
def type_of(v) when is_function(v), do: Function
def type_of(v) when is_integer(v), do: Integer
def type_of(v) when is_pid(v), do: PID
def type_of(v) when is_port(v), do: Port
def type_of(v) when is_reference(v), do: Reference
def type_of(v) when is_tuple(v), do: Tuple
def type_of(v) when is_list(v), do: List
def type_of(%t{}), do: t
def type_of(v) when is_map(v), do: Map
@doc """
Compare left and right term
## Examples
```
iex> Comp.compare(1, 2)
:lt
iex> Comp.compare(2, 1)
:gt
iex> Comp.compare(1, 1)
:eq
```
"""
@spec compare(left, right) :: Comparable.ord()
def compare(left, right) do
left
|> new(right)
|> Comparable.compare()
end
defp new(left, right) do
lr_type =
try do
[Comparable, Type, type_of(left), To, type_of(right)]
|> Module.safe_concat()
rescue
ArgumentError ->
[Comparable, Type, Any, To, Any]
|> Module.safe_concat()
end
%{__struct__: lr_type, left: left, right: right}
end
end
end

View file

@ -0,0 +1,16 @@
unless Code.ensure_loaded?(Comparable) do
defprotocol Comparable do
@moduledoc """
Protocol which describes ordering relation for pair of types
"""
@type t :: Comparable.t()
@type ord :: :gt | :lt | :eq
@doc """
Accepts struct with fields :left and :right and returns ord value
"""
@spec compare(t) :: ord
def compare(left_and_right)
end
end

View file

@ -0,0 +1,91 @@
import Comp
defcomparable left :: Any, right :: Any do
case {left, right} do
{_, _} when left == right ->
Comp.eq()
{%name{}, %name{}} ->
left
|> Map.from_struct()
|> Comp.compare(Map.from_struct(right))
{_, _} when left > right ->
Comp.gt()
{_, _} when left < right ->
Comp.lt()
end
end
defcomparable left :: List, right :: List do
left
|> Stream.zip(right)
|> Enum.reduce_while(Comp.eq(), fn {lx, rx}, Comp.eq() ->
lx
|> Comp.compare(rx)
|> case do
res when res in [Comp.gt(), Comp.lt()] -> {:halt, res}
Comp.eq() = res -> {:cont, res}
end
end)
|> case do
res when res in [Comp.gt(), Comp.lt()] ->
res
Comp.eq() ->
left_length = length(left)
right_length = length(right)
cond do
left_length > right_length -> Comp.gt()
left_length < right_length -> Comp.lt()
true -> Comp.eq()
end
end
end
defcomparable left :: Map, right :: Map do
left_length = map_size(left)
right_length = map_size(right)
cond do
left_length > right_length ->
Comp.gt()
left_length < right_length ->
Comp.lt()
true ->
left
|> Map.keys()
|> Comp.compare(right |> Map.keys())
|> case do
res when res in [Comp.gt(), Comp.lt()] ->
res
Comp.eq() ->
left
|> Map.values()
|> Comp.compare(right |> Map.values())
end
end
end
defcomparable left :: Tuple, right :: Tuple do
left_length = tuple_size(left)
right_length = tuple_size(right)
cond do
left_length > right_length ->
Comp.gt()
left_length < right_length ->
Comp.lt()
true ->
left
|> Tuple.to_list()
|> Comp.compare(right |> Tuple.to_list())
end
end

View file

@ -343,7 +343,6 @@ defmodule Ash.MixProject do
{:ets, "~> 0.8"},
# Data & types
{:decimal, "~> 2.0"},
{:comparable, "~> 1.0"},
{:jason, ">= 1.0.0"},
# Observability
{:telemetry, "~> 1.1"},

View file

@ -1,7 +1,6 @@
%{
"benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
@ -48,7 +47,6 @@
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"ucwidth": {:hex, :ucwidth, "0.2.0", "1f0a440f541d895dff142275b96355f7e91e15bca525d4a0cc788ea51f0e3441", [:mix], [], "hexpm", "c1efd1798b8eeb11fb2bec3cafa3dd9c0c3647bee020543f0340b996177355bf"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},