Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions lib/eq.ex
Original file line number Diff line number Diff line change
Expand Up @@ -266,26 +266,6 @@ defmodule Funx.Eq do
}
end

@doc """
Converts an Eq DSL result or projection to an eq_map.

If passed a plain map with `eq?/2` and `not_eq?/2` functions (the result
of `eq do ... end`), returns it directly. Otherwise, delegates to `contramap/2`.

Used internally by `Funx.Macros.eq_for/3` to support both projection-based
and DSL-based equality definitions.
"""
@spec to_eq_map_or_contramap(any(), eq_t()) :: eq_map()
# Plain map with eq?/not_eq? keys (DSL result)
def to_eq_map_or_contramap(%{eq?: eq?, not_eq?: not_eq?} = map, _eq)
when is_function(eq?, 2) and is_function(not_eq?, 2) and not is_struct(map) do
map
end

def to_eq_map_or_contramap(projection, eq) do
contramap(projection, eq)
end

@doc """
Checks equality of two values by applying a projection before comparison.

Expand Down
165 changes: 120 additions & 45 deletions lib/macros.ex
Original file line number Diff line number Diff line change
Expand Up @@ -270,28 +270,31 @@ defmodule Funx.Macros do
defmacro eq_for(for_struct, projection, opts \\ []) do
or_else = Keyword.get(opts, :or_else)
custom_eq = Keyword.get(opts, :eq)
projection_ast = normalize_projection(projection, or_else)
{projection_ast, projection_type} = normalize_projection(projection, or_else)
eq_module_ast = custom_eq || quote(do: Funx.Eq.Protocol)

eq_map_ast = build_eq_map_ast(projection_ast, eq_module_ast, projection_type)

quote do
alias Funx.Eq
alias Funx.Optics.Prism

defimpl Funx.Eq.Protocol, for: unquote(for_struct) do
# Private function to build the eq_map once at module compile time
defp __eq_map__ do
Funx.Eq.to_eq_map_or_contramap(unquote(projection_ast), unquote(eq_module_ast))
end
defp __eq_map__, do: unquote(eq_map_ast)

def eq?(a, b)
when is_struct(a, unquote(for_struct)) and is_struct(b, unquote(for_struct)) do
__eq_map__().eq?.(a, b)
end

def eq?(%unquote(for_struct){}, b) when is_struct(b), do: false

def not_eq?(a, b)
when is_struct(a, unquote(for_struct)) and is_struct(b, unquote(for_struct)) do
__eq_map__().not_eq?.(a, b)
end

def not_eq?(%unquote(for_struct){}, b) when is_struct(b), do: true
end
end
end
Expand Down Expand Up @@ -362,18 +365,17 @@ defmodule Funx.Macros do
defmacro ord_for(for_struct, projection, opts \\ []) do
or_else = Keyword.get(opts, :or_else)
custom_ord = Keyword.get(opts, :ord)
projection_ast = normalize_projection(projection, or_else)
{projection_ast, projection_type} = normalize_projection(projection, or_else)
ord_module_ast = custom_ord || quote(do: Funx.Ord.Protocol)

ord_map_ast = build_ord_map_ast(projection_ast, ord_module_ast, projection_type)

quote do
alias Funx.Optics.Prism
alias Funx.Ord

defimpl Funx.Ord.Protocol, for: unquote(for_struct) do
# Private function to build the ord_map once at module compile time
defp __ord_map__ do
Funx.Ord.to_ord_map_or_contramap(unquote(projection_ast), unquote(ord_module_ast))
end
defp __ord_map__, do: unquote(ord_map_ast)

def lt?(a, b)
when is_struct(a, unquote(for_struct)) and is_struct(b, unquote(for_struct)) do
Expand Down Expand Up @@ -410,22 +412,80 @@ defmodule Funx.Macros do
end
end

# ============================================================================
# AST BUILDERS (PRIVATE)
# ============================================================================

# For known projections, directly call contramap
defp build_eq_map_ast(projection_ast, eq_module_ast, :projection) do
quote do
Funx.Eq.contramap(unquote(projection_ast), unquote(eq_module_ast))
end
end

# For function calls that might return an eq_map, do runtime check
defp build_eq_map_ast(projection_ast, eq_module_ast, :maybe_map) do
quote do
projection = unquote(projection_ast)

case projection do
%{eq?: eq_fun, not_eq?: not_eq_fun}
when is_function(eq_fun, 2) and is_function(not_eq_fun, 2) ->
projection

_ ->
Funx.Eq.contramap(projection, unquote(eq_module_ast))
end
end
end

# For known projections, directly call contramap
defp build_ord_map_ast(projection_ast, ord_module_ast, :projection) do
quote do
Funx.Ord.contramap(unquote(projection_ast), unquote(ord_module_ast))
end
end

# For function calls that might return an ord_map, do runtime check
defp build_ord_map_ast(projection_ast, ord_module_ast, :maybe_map) do
quote do
projection = unquote(projection_ast)

case projection do
%{lt?: lt_fun, le?: le_fun, gt?: gt_fun, ge?: ge_fun}
when is_function(lt_fun, 2) and is_function(le_fun, 2) and
is_function(gt_fun, 2) and is_function(ge_fun, 2) ->
projection

_ ->
Funx.Ord.contramap(projection, unquote(ord_module_ast))
end
end
end

# ============================================================================
# PROJECTION NORMALIZATION (PRIVATE)
# ============================================================================
# Returns {normalized_ast, type} where type is :projection or :maybe_map

# Atom without or_else - convert to Prism.key (safe for nil values, Nothing < Just semantics)
defp normalize_projection(atom, nil) when is_atom(atom) do
quote do
Prism.key(unquote(atom))
end
ast =
quote do
Prism.key(unquote(atom))
end

{ast, :projection}
end

# Atom with or_else - convert to {Prism.key, default}
defp normalize_projection(atom, or_else) when is_atom(atom) and not is_nil(or_else) do
quote do
{Prism.key(unquote(atom)), unquote(or_else)}
end
ast =
quote do
{Prism.key(unquote(atom)), unquote(or_else)}
end

{ast, :projection}
end

# Lens.key(...) - cannot use or_else with Lens
Expand All @@ -434,7 +494,7 @@ defmodule Funx.Macros do
or_else
) do
if is_nil(or_else) do
lens_ast
{lens_ast, :projection}
else
raise ArgumentError, Errors.or_else_with_lens()
end
Expand All @@ -446,7 +506,7 @@ defmodule Funx.Macros do
or_else
) do
if is_nil(or_else) do
lens_ast
{lens_ast, :projection}
else
raise ArgumentError, Errors.or_else_with_lens()
end
Expand All @@ -458,11 +518,14 @@ defmodule Funx.Macros do
or_else
) do
if is_nil(or_else) do
prism_ast
{prism_ast, :projection}
else
quote do
{unquote(prism_ast), unquote(or_else)}
end
ast =
quote do
{unquote(prism_ast), unquote(or_else)}
end

{ast, :projection}
end
end

Expand All @@ -472,11 +535,14 @@ defmodule Funx.Macros do
or_else
) do
if is_nil(or_else) do
prism_ast
{prism_ast, :projection}
else
quote do
{unquote(prism_ast), unquote(or_else)}
end
ast =
quote do
{unquote(prism_ast), unquote(or_else)}
end

{ast, :projection}
end
end

Expand All @@ -486,17 +552,20 @@ defmodule Funx.Macros do
or_else
) do
if is_nil(or_else) do
traversal_ast
{traversal_ast, :projection}
else
raise ArgumentError, Errors.or_else_with_traversal()
end
end

# {Prism, default} tuple - cannot have additional or_else (redundant)
defp normalize_projection({_prism_ast, _or_else_ast} = tuple, nil) do
quote do
unquote(tuple)
end
ast =
quote do
unquote(tuple)
end

{ast, :projection}
end

defp normalize_projection({_prism_ast, _or_else_ast}, _extra_or_else) do
Expand All @@ -506,7 +575,7 @@ defmodule Funx.Macros do
# Captured function &fun/1 - cannot use or_else
defp normalize_projection({:&, _, _} = fun_ast, or_else) do
if is_nil(or_else) do
fun_ast
{fun_ast, :projection}
else
raise ArgumentError, Errors.or_else_with_captured_function()
end
Expand All @@ -515,7 +584,7 @@ defmodule Funx.Macros do
# Anonymous function fn ... end - cannot use or_else
defp normalize_projection({:fn, _, _} = fun_ast, or_else) do
if is_nil(or_else) do
fun_ast
{fun_ast, :projection}
else
raise ArgumentError, Errors.or_else_with_anonymous_function()
end
Expand All @@ -524,34 +593,40 @@ defmodule Funx.Macros do
# Struct literal (e.g., %Lens{...}) - cannot use or_else with Lens struct
defp normalize_projection({:%, _, _} = struct_ast, or_else) do
if is_nil(or_else) do
struct_ast
{struct_ast, :projection}
else
raise ArgumentError, Errors.or_else_with_struct_literal()
end
end

# Remote function call (Module.function()) - can use or_else (runtime check)
# Remote function call (Module.function()) - might return ord/eq map
defp normalize_projection({{:., _, _}, _, _} = call_ast, or_else) do
if is_nil(or_else) do
call_ast
{call_ast, :maybe_map}
else
# Runtime: if helper returns Lens, contramap will raise
quote do
{unquote(call_ast), unquote(or_else)}
end
# If or_else is provided, it's definitely a projection (wrapped in tuple)
ast =
quote do
{unquote(call_ast), unquote(or_else)}
end

{ast, :projection}
end
end

# Local function call (function_name()) - pass through (already handled by remote call pattern or atom)
# Local function call (function_name()) - might return ord/eq map
defp normalize_projection({function_name, _, args} = call_ast, or_else)
when is_atom(function_name) and is_list(args) do
if is_nil(or_else) do
call_ast
{call_ast, :maybe_map}
else
# Runtime: if helper returns Lens, contramap will raise
quote do
{unquote(call_ast), unquote(or_else)}
end
# If or_else is provided, it's definitely a projection (wrapped in tuple)
ast =
quote do
{unquote(call_ast), unquote(or_else)}
end

{ast, :projection}
end
end
end
23 changes: 0 additions & 23 deletions lib/ord.ex
Original file line number Diff line number Diff line change
Expand Up @@ -222,29 +222,6 @@ defmodule Funx.Ord do
}
end

@doc """
Converts an Ord DSL result or projection to an ord_map.

If passed a plain map with `lt?/2`, `le?/2`, `gt?/2`, and `ge?/2` functions
(the result of `ord do ... end`), returns it directly. Otherwise, delegates
to `contramap/2`.

Used internally by `Funx.Macros.ord_for/3` to support both projection-based
and DSL-based ordering definitions.
"""
@spec to_ord_map_or_contramap(any(), ord_t()) :: ord_map()

# Plain map with ord keys (DSL result)
def to_ord_map_or_contramap(%{lt?: lt?, le?: le?, gt?: gt?, ge?: ge?} = map, _ord)
when is_function(lt?, 2) and is_function(le?, 2) and is_function(gt?, 2) and
is_function(ge?, 2) and not is_struct(map) do
map
end

def to_ord_map_or_contramap(projection, ord) do
contramap(projection, ord)
end

@doc """
Returns the maximum of two values, with an optional custom `Ord`.

Expand Down
Loading