defmodule Funx.List do @moduledoc """ The `Funx.List` module provides utility functions for working with lists while respecting `Eq` and `Ord` instances. This allows for set-like operations, uniqueness constraints, and sorted collections that align with functional programming principles. ## Features - **Equality-based Operations**: Use `Eq` instances to compare elements for uniqueness, intersection, and difference. - **Ordering Functions**: Leverage `Ord` instances to sort and enforce uniqueness in sorted collections. - **Set Operations**: Perform union, intersection, difference, and symmetric difference while preserving custom equality logic. - **Subset & Superset Checks**: Verify relationships between lists in terms of inclusion. - **Functional Constructs**: Implements `Monad` and `Foldable` protocols for lists, supporting mapping, binding, and folding. ### Usage Overview 1. **Deduplicate**: Use `uniq/1` to remove duplicates based on `Eq`. 2. **Combine**: Use `union/2` to merge lists without duplicates. 3. **Filter**: Use `intersection/2` or `difference/2` for set operations. 4. **Sort**: Use `sort/2` or `strict_sort/2` with `Ord` instances. 5. **Check Membership**: Use `subset?/2` or `superset?/2` to verify inclusion relationships. ### Equality-Based Operations - `uniq/1`: Removes duplicates using `Eq`. - `union/2`: Merges lists while preserving uniqueness. - `intersection/2`: Returns elements common to both lists. - `difference/2`: Returns elements from the first list not in the second. - `symmetric_difference/2`: Returns elements unique to each list. ### Ordering Functions - `sort/2`: Sorts a list using `Ord`. - `strict_sort/2`: Sorts while ensuring uniqueness. ### Set Operations - `subset?/2`: Checks if one list is a subset of another. - `superset?/2`: Checks if one list is a superset of another. ### Monad Implementation - `map/2`: Transforms list elements. - `bind/2`: Applies a function returning lists and flattens the result. - `ap/2`: Applies functions in a list to elements in another list. ### Foldable Implementation - `fold_l/3`: Performs left-associative folding. - `fold_r/3`: Performs right-associative folding. """ import Funx.Foldable, only: [fold_l: 3] import Funx.Filterable, only: [filter: 2] import Funx.Monoid.Utils, only: [m_concat: 2] alias Funx.Eq alias Funx.Monad.Maybe alias Funx.Monoid.ListConcat alias Funx.Ord @doc """ Removes duplicate elements from a list based on the given equality module. ## Examples iex> Funx.List.uniq([1, 2, 2, 3, 1, 4, 5]) [1, 2, 3, 4, 5] """ @spec uniq([term()], Eq.Utils.eq_t()) :: [term()] def uniq(list, eq \\ Funx.Eq) when is_list(list) do list |> fold_l([], fn item, acc -> if Enum.any?(acc, &Eq.Utils.eq?(item, &1, eq)), do: acc, else: [item | acc] end) |> :lists.reverse() end @doc """ Returns the union of two lists, removing duplicates. ## Examples iex> Funx.List.union([1, 2, 3], [3, 4, 5]) [1, 2, 3, 4, 5] """ @spec union([term()], [term()], Eq.Utils.eq_t()) :: [term()] def union(list1, list2, eq \\ Funx.Eq) when is_list(list1) and is_list(list2) do (list1 ++ list2) |> uniq(eq) end @doc """ Returns the intersection of two lists. ## Examples iex> Funx.List.intersection([1, 2, 3, 4], [3, 4, 5]) [3, 4] """ @spec intersection([term()], [term()], Eq.Utils.eq_t()) :: [term()] def intersection(list1, list2, eq \\ Funx.Eq) when is_list(list1) and is_list(list2) do list1 |> filter(fn item -> Enum.any?(list2, &Eq.Utils.eq?(item, &1, eq)) end) |> uniq(eq) end @doc """ Returns the difference of two lists. ## Examples iex> Funx.List.difference([1, 2, 3, 4], [3, 4, 5]) [1, 2] """ @spec difference([term()], [term()], Eq.Utils.eq_t()) :: [term()] def difference(list1, list2, eq \\ Funx.Eq) when is_list(list1) and is_list(list2) do list1 |> Enum.reject(fn item -> Enum.any?(list2, &Eq.Utils.eq?(item, &1, eq)) end) |> uniq(eq) end @doc """ Returns the symmetric difference of two lists. ## Examples iex> Funx.List.symmetric_difference([1, 2, 3], [3, 4, 5]) [1, 2, 4, 5] """ @spec symmetric_difference([term()], [term()], Eq.Utils.eq_t()) :: [term()] def symmetric_difference(list1, list2, eq \\ Funx.Eq) when is_list(list1) and is_list(list2) do (difference(list1, list2, eq) ++ difference(list2, list1, eq)) |> uniq(eq) end @doc """ Checks if the first list is a subset of the second. ## Examples iex> Funx.List.subset?([1, 2], [1, 2, 3, 4]) true iex> Funx.List.subset?([1, 5], [1, 2, 3, 4]) false """ @spec subset?([term()], [term()], Eq.Utils.eq_t()) :: boolean() def subset?(small, large, eq \\ Funx.Eq) when is_list(small) and is_list(large) do Enum.all?(small, fn item -> Enum.any?(large, &Eq.Utils.eq?(item, &1, eq)) end) end @doc """ Checks if the first list is a superset of the second. ## Examples iex> Funx.List.superset?([1, 2, 3, 4], [1, 2]) true iex> Funx.List.superset?([1, 2, 3, 4], [1, 5]) false """ @spec superset?([term()], [term()], Eq.Utils.eq_t()) :: boolean() def superset?(large, small, eq \\ Funx.Eq) when is_list(small) and is_list(large) do subset?(small, large, eq) end @doc """ Sorts a list using the given ordering module. ## Examples iex> Funx.List.sort([3, 1, 4, 1, 5]) [1, 1, 3, 4, 5] """ @spec sort([term()], Ord.Utils.ord_t()) :: [term()] def sort(list, ord \\ Funx.Ord) when is_list(list) do Enum.sort(list, Ord.Utils.comparator(ord)) end @doc """ Sorts a list while ensuring uniqueness. ## Examples iex> Funx.List.strict_sort([3, 1, 4, 1, 5]) [1, 3, 4, 5] """ @spec strict_sort([term()], Ord.Utils.ord_t()) :: [term()] def strict_sort(list, ord \\ Funx.Ord) when is_list(list) do list |> uniq(Ord.Utils.to_eq(ord)) |> sort(ord) end @doc """ Concatenates a list of lists from left to right. This uses the `ListConcat` monoid, preserving the original order of elements. ## Examples iex> Funx.List.concat([[1], [2, 3], [4]]) [1, 2, 3, 4] """ @spec concat([[term()]]) :: [term()] def concat(list) when is_list(list) do m_concat(%ListConcat{}, list) end @doc """ Returns the head of a list wrapped in `Maybe.Just`, or `Maybe.Nothing` if empty. This is a safe version of `hd/1` that returns `Maybe` instead of raising. ## Examples iex> Funx.List.maybe_head([1, 2, 3]) %Funx.Monad.Maybe.Just{value: 1} iex> Funx.List.maybe_head([]) %Funx.Monad.Maybe.Nothing{} iex> Funx.List.maybe_head("not a list") %Funx.Monad.Maybe.Nothing{} """ @spec maybe_head([a]) :: Maybe.t(a) when a: term() def maybe_head(list) do case list do [head | _] -> Maybe.just(head) _ -> Maybe.nothing() end end end defimpl Funx.Monad, for: List do @spec map([term], (term -> term)) :: [term] def map(list, func) do :lists.map(func, list) end @spec ap(list((term -> term)), list(term)) :: list(term) def ap(funcs, list) do :lists.flatten( for f <- funcs do :lists.map(f, list) end ) end @spec bind([term], (term -> [term])) :: [term] def bind(list, func) do :lists.flatmap(func, list) end end defimpl Funx.Foldable, for: List do @spec fold_l(list(term), term, (term, term -> term)) :: term def fold_l(list, acc, func), do: :lists.foldl(func, acc, list) @spec fold_r(list(term), term, (term, term -> term)) :: term def fold_r(list, acc, func), do: :lists.foldr(func, acc, list) end defimpl Funx.Filterable, for: List do def guard(list, true), do: list def guard(_list, false), do: [] def filter(list, predicate) do :lists.filter(predicate, list) end def filter_map(list, func) do :lists.foldl( fn x, acc -> case func.(x) do nil -> acc result -> [result | acc] end end, [], list ) |> :lists.reverse() end end