defmodule Bio.Enum do @moduledoc """ Implements a wrapper around the `Enum` module's public interface. The semantics of the `Enum` module don't always match up with what I would think is best for certain cases. The best example of this is the `slide/3` function. Because of the `Enum` implementation, there is no way to coerce the return value back into a struct. So for example, given a `Bio.Sequence.DnaStrand` it would return a list of graphemes. This is not what I want users to expect. That said, there are other functions that _do_ behave well. Or at the very least, their semantics seem meaningfully useful. So in order to preserve the maximum utility, I wrap the module. The expectation should be as follows: `Enum` functions will return bare data. `Bio.Enum` functions will return the closest thing to the struct as is reasonable. There are cases where it doesn't make much sense to return more than is required. For example, the `Bio.Enum.at/2` function will return a binary grapheme. I have a hard time imagining a case where the user would want a struct with a sequence of a single character instead of the character itself. Contrast that with the `Enum.at/2` function, which will return a raw char. """ @type acc() :: any() @type default() :: any() @type element() :: any() @type index() :: integer() @type t() :: Enumerable.t() # def all?(enumerable), do: Enum.all?(enumerable) # def all?(enumerable, func), do: Enum.all?(enumerable, func) # @spec any?(t()) :: boolean() # def any?(enumerable), do: Enum.any?(enumerable) # TODO: add a check for `func` in `any?/2` is_binary to allow # `any?(seq, "char")` as a valid approach. # @spec any?(t(), (element() -> as_boolean(term()))) :: boolean() # def any?(enumerable, func), do: Enum.any?(enumerable, func) def at(enumerable, index) when is_integer(index) do Enum.at(enumerable, index) |> then(&[&1]) |> List.to_string() end def at(enumerable, index, default) when is_integer(index) do Enum.at(enumerable, index, default) |> then(&[&1]) |> List.to_string() end # def chunk_by(enumerable, func) do # Enum.chunk_by(enumerable, func) # |> Enum.map(&Enum.join/1) # |> Enum.map(&apply(enumerable.__struct__, :new, [&1, [label: enumerable.label]])) # end def chunk_every(enumerable, count), do: Enum.chunk_every(enumerable, count) |> Enum.map(&Enum.join/1) |> Enum.map(&new(&1, enumerable)) def chunk_every(enumerable, count, step), do: Enum.chunk_every(enumerable, count, step) |> Enum.map(&Enum.join/1) |> Enum.map(&new(&1, enumerable)) def chunk_every(enumerable, count, step, options), do: Enum.chunk_every(enumerable, count, step, options) |> Enum.map(&Enum.join/1) |> Enum.map(&new(&1, enumerable)) def chunk_while(enumerable, acc, chunk_fun, after_fun), do: Enum.chunk_while(enumerable, acc, chunk_fun, after_fun) |> Enum.map(&Enum.join/1) |> Enum.map(&new(&1, enumerable)) # TODO: figure out the semantics for concatenation with non-sequence # enumerables # def concat(a), do: {a} # def concat(a, b), do: {a, b} # def count(enumerable), do: Enum.count(enumerable) # def count(enumerable, fun), do: Enum.count(enumerable, fun) # def count_until(a), do: {a} # def count_until(a, b), do: {a, b} # def dedup(), do: {} # def dedup_by(), do: {} # def drop(), do: {} # # def drop_every(), do: {} # # def drop_while(), do: {} # # def each(), do: {} # # def empty?(), do: {} # # def fetch!(), do: {} # # def fetch(), do: {} # # def filter(), do: {} # # def find(a), do: {a} # def find(a, b), do: {a, b} # # def find_index(), do: {} # # def find_value(a), do: {a} # def find_value(a, b), do: {a, b} # # def flat_map(), do: {} # # def flat_map_reduce(), do: {} # # def frequencies(), do: {} # # def frequencies_by(), do: {} # # def group_by(a), do: {a} # def group_by(a, b), do: {a, b} # # def intersperse(), do: {} # # def into(a), do: {a} # def into(a, b), do: {a, b} # # def join(a), do: {a} # def join(a, b), do: {a, b} def map(enumerable, func), do: Enum.map(enumerable, func) |> Enum.join() |> new(enumerable) # def map_every(), do: {} # # def map_intersperse(), do: {} # # def map_join(a), do: {a} # def map_join(a, b), do: {a, b} # # def map_reduce(), do: {} # # def max(), do: {} # # def max_by(a), do: {a} # def max_by(a, b), do: {a, b} # # def member?(), do: {} # # def min(), do: {} # # def min_by(a), do: {a} # def min_by(a, b), do: {a, b} # # def min_max(a), do: {a} # def min_max(a, b), do: {a, b} # # def min_max_by(a), do: {a} # def min_max_by(a, b), do: {a, b} # # def product(), do: {} # # def random(), do: {} # # def reduce(a), do: {a} # def reduce(a, b), do: {a, b} # # def reduce_while(), do: {} # # def reject(), do: {} def reverse(enumerable), do: Enum.reverse(enumerable) |> Enum.join() |> new(enumerable) # TODO: what type is tail? def reverse(enumerable, tail), do: Enum.reverse(enumerable, tail) |> Enum.join() |> new(enumerable) # def reverse_slice(), do: {} # # def scan(a), do: {a} # def scan(a, b), do: {a, b} # # def shuffle(), do: {} def slice(enumerable, index_range), do: Enum.slice(enumerable, index_range) |> List.to_string() |> new(enumerable) def slice(enumerable, start_index, amount), do: Enum.slice(enumerable, start_index, amount) |> List.to_string() |> new(enumerable) # def slide(), do: {} # # def sort(a), do: {a} # def sort(a, b), do: {a, b} # # def sort_by(a), do: {a} # def sort_by(a, b), do: {a, b} # # def split(), do: {} # # def split_while(), do: {} # # def split_with(), do: {} # # def sum(), do: {} # # def take(), do: {} # # def take_every(), do: {} # # def take_random(), do: {} # # def take_while(), do: {} # # def to_list(), do: {} # # def uniq(), do: {} # # def uniq_by(), do: {} # # def unzip(), do: {} # def zip(a), do: {a} # def zip(a, b), do: {a, b} # # def zip_reduce(a), do: {a} # def zip_reduce(a, b), do: {a, b} # # def zip_with(a), do: {a} # def zip_with(a, b), do: {a, b} defp new(seq, enumerable) do data = enumerable |> Map.from_struct() |> Map.drop([:sequence, :length]) |> Map.to_list() apply(enumerable.__struct__, :new, [seq, data]) end end