lib/lazy_for.ex

defmodule LazyFor do
  @moduledoc """
  The stream-based implementation of `Kernel.SpecialForms.for/1`.

  Has the same syntax as its ancestor, returns a stream.

  Currently supports `Enum.t()` as an input. Examples are gracefully stolen
    from the ancestor’s docs.

  _Examples:_

      iex> import LazyFor
      iex> # A list generator:
      iex> result = stream n <- [1, 2, 3, 4], do: n * 2
      iex> Enum.to_list(result)
      [2, 4, 6, 8]

      iex> # A comprehension with two generators
      iex> result = stream x <- [1, 2], y <- [2, 3], do: x * y
      iex> Enum.to_list(result)
      [2, 3, 4, 6]

      iex> # A comprehension with a generator and a filter
      iex> result = stream n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
      iex> Enum.to_list(result)
      [2, 4, 6]

      iex> users = [user: "john", admin: "meg", guest: "barbara"]
      iex> result = stream {type, name} when type != :guest <- users do
      ...>   String.upcase(name)
      ...> end
      iex> Enum.to_list(result)
      ["JOHN", "MEG"]

      iex> Enum.to_list(stream <<c <- "a|b|c">>, c != ?|, do: c)
      'abc'
  """
  defmacrop a(), do: quote(do: {:acc, [], Elixir})

  defmacrop __s__(any \\ {:_, [], nil}),
    do: quote(do: {:., [], [{:__aliases__, [alias: false], [:Stream]}, unquote(any)]})

  defmacrop stransf(), do: quote(do: __s__(:transform))
  defmacrop sfilter(), do: quote(do: __s__(:filter))

  ##############################################################################

  defp reduce_clauses(clauses, block, acc \\ []) do
    clauses
    |> Enum.reverse()
    |> Enum.reduce({acc, block}, fn outer, {acc, inner} ->
      {acc, clause(outer, inner, acc)}
    end)
  end

  # simple comprehension, outer, guards
  defp clause(
         {:<-, _meta, [{:when, _inner_meta, [var, conditions]}, source]},
         {__s__(), _, _} = inner,
         acc
       ),
       do: do_stransf_clause(source, acc, do_fn_body(inner, var, conditions))

  # simple comprehension, inner, guards
  defp clause({:<-, _meta, [{:when, _inner_meta, [var, conditions]}, source]}, inner, acc),
    do: do_stransf_clause(source, acc, do_fn_body([inner], var, conditions))

  # simple comprehension, outer, no guards
  defp clause({:<-, _meta, [var, source]}, {__s__(), _, _} = inner, acc),
    do: do_stransf_clause(source, acc, do_fn_body(inner, var))

  # simple comprehension, inner, no guards
  defp clause({:<-, _meta, [var, source]}, inner, acc),
    do: do_stransf_clause(source, acc, do_fn_body([inner], var))

  # expression
  defp clause({:=, meta, [var, expression]}, inner, acc),
    do: clause({:<-, meta, [var, [expression]]}, inner, acc)

  # TODO
  # stream <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
  # {{:<<>>, _,
  #   [
  #     {:"::", _, [{:r, _, nil}, 8]},
  #     {:"::", _, [{:g, _, nil}, 8]},
  #     {:<-, _, [{:"::", _, [{:b, _, nil}, 8]}, {:pixels, _, nil}]}
  #   ]}, {:{}, _, [{:r, _, nil}, {:g, _, nil}, {:b, _, nil}]}}

  # binary string
  defp clause({:<<>>, outer_meta, [{:<-, meta, [{:<<>>, _, [var]}, source]}]}, inner, acc),
    do: clause({:<<>>, outer_meta, [{:<-, meta, [var, source]}]}, inner, acc)

  defp clause({:<<>>, _, [{:<-, meta, [var, source]}]}, inner, acc),
    do:
      clause(
        {:<-, meta, [var, {{:., [], [:erlang, :bitstring_to_list]}, [], [source]}]},
        inner,
        acc
      )

  # condition
  defp clause(guard, {__s__(), _, _} = inner, _acc),
    do: {sfilter(), [], [inner, {:fn, [], [{:->, [], [[{:_, [], Elixir}], guard]}]}]}

  defp clause(guard, inner, _acc),
    do: {sfilter(), [], [[inner], {:fn, [], [{:->, [], [[{:_, [], Elixir}], guard]}]}]}

  ##############################################################################

  defp do_stransf_clause(source, acc, fn_body),
    do: {stransf(), [], [source, acc, {:fn, [], fn_body}]}

  defp do_fn_body(inner, {var_name, _, ctx} = var) when is_atom(var_name) and is_atom(ctx),
    do: [{:->, [], [[var, a()], {inner, a()}]}]

  defp do_fn_body(inner, var),
    do: [
      {:->, [], [[var, a()], {inner, a()}]},
      {:->, [], [[{:_, [], Elixir}, a()], {[], a()}]}
    ]

  defp do_fn_body(inner, var, conditions),
    do: [
      {:->, [], [[{:when, [], [var, a(), conditions]}], {inner, a()}]},
      {:->, [], [[{:_, [], Elixir}, a()], {[], a()}]}
    ]

  ##############################################################################

  defp do_apply_opts(ast, opts) do
    ast =
      if opts[:uniq],
        do: {{:., [], [{:__aliases__, [alias: false], [:Stream]}, :uniq]}, [], [ast]},
        else: ast

    into = opts[:into]

    ast =
      if into,
        do: {{:., [], [{:__aliases__, [alias: false], [:Stream]}, :into]}, [], [ast, into]},
        else: ast

    case opts[:take] do
      :all ->
        {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :to_list]}, [], [ast]}

      i when is_integer(i) ->
        {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :take]}, [], [ast, i]}

      _ ->
        ast
    end
  end

  @clauses Application.compile_env(:lazy_for, :clause_limit, 18)
  @args for i <- 1..(@clauses + 1),
            into: %{},
            do: {i, Enum.map(1..i, &Macro.var(:"arg_#{&1}", nil))}

  for i <- 1..@clauses do
    @doc false
    [last | rest] = :lists.reverse(@args[i])
    rest = :lists.reverse(rest)

    defmacro stream(unquote_splicing(rest), unquote(last), do: block)
             when not is_list(unquote(last)),
             do: with({_, s} <- reduce_clauses(unquote(@args[i]), block), do: s)
  end

  for i <- 2..@clauses do
    @doc false
    [last | rest] = :lists.reverse(@args[i])
    rest = :lists.reverse(rest)

    defmacro stream(unquote_splicing(rest), unquote(last), do: block)
             when is_list(unquote(last)) do
      with {_, ast} <- reduce_clauses(unquote(rest), block), do: do_apply_opts(ast, unquote(last))
    end

    defmacro stream(unquote_splicing(rest), unquote(last)) do
      {block, opts} = Keyword.pop(unquote(last), :do)
      with {_, ast} <- reduce_clauses(unquote(rest), block), do: do_apply_opts(ast, opts)
    end
  end
end