lib/combinators/stream.ex

defmodule Formulae.Combinators.Stream do
  @moduledoc """
  Helper to calculate all the combinations / permutations of the enumerable given.

  Similar to `Formulae.Combinators` but returns a stream.

  _Examples_

      iex> ~w|a b c d|a
      ...> |> Formulae.Combinators.Stream.combinations(2)
      ...> |> elem(0)
      ...> |> Enum.to_list()
      [[:a, :b], [:a, :c], [:a, :d], [:b, :c], [:b, :d], [:c, :d]]
      iex> ~w|a b c d|a
      ...> |> Formulae.Combinators.Stream.permutations(2)
      ...> |> elem(0)
      ...> |> Enum.to_list()
      [[:a, :b], [:a, :c], [:a, :d], [:b, :a], [:b, :c], [:b, :d],
       [:c, :a], [:c, :b], [:c, :d], [:d, :a], [:d, :b], [:d, :c]]
      iex> ~w|a b c d|a
      ...> |> Formulae.Combinators.Stream.repeated_permutations(2)
      ...> |> elem(0)
      ...> |> Enum.to_list()
      [[:a, :a], [:a, :b], [:a, :c], [:a, :d], [:b, :a], [:b, :b],
       [:b, :c], [:b, :d], [:c, :a], [:c, :b], [:c, :c], [:c, :d],
       [:d, :a], [:d, :b], [:d, :c], [:d, :d]]

      iex> (for c <- ?a..?z, do: <<c>>)
      ...> |> Formulae.Combinators.Stream.combinations(12)
      ...> |> elem(0)
      ...> |> Stream.take_every(26)
      ...> |> Enum.take(2)
      [["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
       ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "l", "x"]]
      iex> l = for c <- ?a..?z, do: <<c>>
      iex> with {stream, :ok} <- Formulae.Combinators.Stream.permutations(l, 12),
      ...>   do: stream |> Stream.take_every(26) |> Enum.take(2)
      [["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
       ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "l", "w"]]
      iex> (for c <- ?a..?z, do: <<c>>)
      ...> |> Formulae.Combinators.Stream.repeated_permutations(12)
      ...> |> elem(0)
      ...> |> Enum.take(5)
      [["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"],
       ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "b"],
       ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "c"],
       ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "d"],
       ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "e"]]
  """
  import Formulae.Combinators.H

  @typep stream_fun :: (any(), any() -> any())
  @typep stream ::
           stream_fun()
           | %{
               __struct__: Stream,
               enum: Enum.t(),
               funs: [stream_fun()],
               accs: [stream_fun()],
               done: nil | boolean()
             }

  @doc """
  Lazily calculates combinations of the list, given as the first parameter,
  returns `{%Stream{}, :ok}` tuple.

  See `Formulae.Combinators.combinations/2` for greedy version.
  """
  @spec combinations(list :: list(), count :: non_neg_integer()) :: {stream(), :ok}

  defmacro combinations(l, n) do
    Enum.reduce(n..1, {[mapper(1, n, &var/1)], :ok}, fn i, body ->
      stream_combination_transform_clause(i, l, body)
    end)
  end

  @doc """
  Lazily calculates permutations of the list, given as the first parameter,
  returns `{%Stream{}, :ok}` tuple.

  See `Formulae.Combinators.permutations/2` for greedy version.
  """
  @spec permutations(list :: list(), count :: non_neg_integer()) :: {stream(), :ok}

  defmacro permutations(l, n) do
    Enum.reduce(n..1, {[mapper(1, n, &var/1)], :ok}, fn i, body ->
      stream_permutation_transform_clause(i, l, body)
    end)
  end

  @doc """
  Lazily calculates repeated permutations of the list, given as the first parameter,
  returns `{%Stream{}, :ok}` tuple.

  See `Formulae.Combinators.repeated_permutations/2` for greedy version.
  """
  @spec repeated_permutations(list :: list(), count :: non_neg_integer()) :: {stream(), :ok}

  defmacro repeated_permutations(l, n) do
    Enum.reduce(n..1, {[mapper(1, n, &var/1)], :ok}, fn i, body ->
      stream_transform_clause(i, l, body)
    end)
  end
end