lib/curry.ex

#
# MIT License
#
# Copyright (c) 2021 Matthew Evans
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

defmodule Curry do
  @moduledoc """

  A simple module to do currying and partial application using Variadic functions
  to start partial evaluation (i.e. no lists needed).

  ## Currying example:

      iex> import Curry

      iex> curry_fun = curry(&Curry.test3/3)
      #Function<0.51120925/1 in Curry.curry/1>

      iex> next_fun = curry_fun.(1)
      #Function<1.51120925/1 in Curry.do_generate_next/3>

      iex> next_fun = next_fun.(77)
      #Function<1.51120925/1 in Curry.do_generate_next/3>

      iex)> next_fun_or_result = next_fun.(10)
      {88, {1, 77, 10}}

      iex> info(curry_fun)
      [
        function: &Curry.test3/3,
        type: "Currying",
        function_arity: 3,
        args_still_needed: 3,
        args_collected: 0
      ]

  ## Partial application example:

      iex> partial_fun = partial(&Curry.test5/5, 1, 2)
      #Function<19.126501267/3 in :erl_eval.expr/5>

      iex> info(partial_fun)
      [
        function: &Curry.test5/5,
        type: "Partial application",
        function_arity: 5,
        args_still_needed: 3,
        args_collected: 2
      ]

      iex> partial_fun.(3, 4, 5)
      {15, {1, 2, 3, 4, 5}}

  """

  import Variadic

  @doc """
  Does currying of the supplied function (capture)

  ## Example:

      iex> curry_fun = curry(&Curry.test3/3)
      #Function<0.82106290/1 in Curry.curry/1>
      iex> curry_fun.(1).(2).(3)
      {6, {1, 2, 3}}

  ## Example:

      iex> curry_fun = Curry.~>(&Curry.test3/3)
      #Function<0.82106290/1 in Curry.curry/1>
      iex> last = curry_fun.(1).(2)
      #Function<1.82106290/1 in Curry.do_generate_next/3>
      iex> last.(3)
      {6, {1, 2, 3}}
  """
  def curry(fun), do:
    fn arg -> do_generate_next(fun, [arg], :curry) end

  @doc false
  def unquote(:~>)(fun), do: curry(fun)

  @doc """
  Does partial application

  ## Example:

      iex> partial_fun = Curry.partial(&Curry.test5/5, 1, 2)
      #Function<19.126501267/3 in :erl_eval.expr/5>
      iex> partial_fun.(3, 4, 5)
      {15, {1, 2, 3, 4, 5}}

  ## Example:

      iex> partial_fun = Curry.~>>(&Curry.test5/5, 1, 2)
      #Function<19.126501267/3 in :erl_eval.expr/5>
      iex> partial_fun.(3, 4, 5)
      {15, {1, 2, 3, 4, 5}}
  """

  def partial(args)

  @doc false
  defv :partial do
    [fun|arguments] = args_to_list(binding())
    do_generate_next(fun, arguments, :partial)
  end

  @doc false
  defv :~>> do
    [fun|arguments] = args_to_list(binding())
    do_generate_next(fun, arguments, :partial)
  end

  @doc """
  Generates the next lambda or the result

  """
  def do_generate_next(fun, args, type) do
    {_, arity} = Function.info(fun, :arity)

    case arity - length(args) do
      0 ->
        Kernel.apply(fun, args)

      val when val > 0 and type == :curry ->
        ## We are currying, return a function that takes a single argument
        fn arg -> do_generate_next(fun, args ++ [arg], type) end

      1 ->
        ## We are doing partial application and need 1 more argument
        fn arg1 -> do_generate_next(fun, args ++ [arg1], type) end

      2 ->
        ## We are doing partial application and need 2 more arguments
        fn arg1, arg2 -> do_generate_next(fun, args ++ [arg1, arg2], type) end

      arity when arity > 0 ->
        ## Still doing partial application, so we don't get a crazy long case statement
        ## we can actually make our own function. A bit slower to make than doing directly, but still pretty fast
        make_lambda(arity, fun, args, type)

      _ ->
        raise(%RuntimeError{message: "Bad arity. Should be #{arity} but got #{length(args)}"})
    end
  end

  @doc """
  Gets information about the lambda/fn

  ## Example:

      iex> Curry.info(partial_fun)
      [
        function: &Curry.test5/5,
        type: "Partial application",
        function_arity: 5,
        args_still_needed: 3,
        args_collected: 2
      ]
  """
  def info(fun) do
    {_, env} = Function.info(fun, :env)
    {_, module} = Function.info(fun, :module)

    {tfun, args, type} = case module do
      Curry ->
        [f|a] = env
        t = Enum.find(a, fn e -> is_atom(e) end)
        a = List.flatten(a) |> Enum.filter(fn e -> not is_atom(e) end)
        {f, a, t}

      _ ->
        [{plist, _, _, _}] = env
        [{_,a}|_] = Enum.filter(plist, fn {_,e} -> is_list(e) end)
        [{_,f}|_] = Enum.filter(plist, fn {_,e} -> is_function(e) end)
        [{_,t}|_] = Enum.filter(plist, fn {_,e} -> is_atom(e) end)
        {f, a, t}
    end

    type = if type == :partial do "Partial application" else "Currying" end

    {_, arity} = Function.info(tfun, :arity)
    args_collected = length(List.flatten(args))

    [
      function: tfun,
      type: type,
      function_arity: arity,
      args_still_needed: arity - args_collected,
      args_collected: args_collected
    ]
  end

  defp make_lambda(arity, fun, args, type) do
    arg_list = for arg <- 1..arity do
      last_arg = if arg != arity do ", " else "" end
      "arg" <> to_string(arg) <> last_arg
    end

    arg_list = List.to_string(arg_list)
    fun_cmd = "fn(" <> arg_list <> ") -> Curry.do_generate_next(fun, args ++ [" <> arg_list <> "], type) end"

    {lambda, _} = Code.eval_string(fun_cmd, fun: fun, args: args, type: type)

    lambda
  end

  ##
  ## Some test functions
  ##
  @doc false
  def test0(), do: :hello

  @doc false
  def test1(a), do: {a * 5, a}

  @doc false
  def test2(a, b), do: {a + b, {a, b}}

  @doc false
  def test3(a, b, c), do: {a + b + c, {a, b, c}}

  @doc false
  def test4(a, b, c, d), do: {a + b + c + d, {a, b, c, d}}

  @doc false
  def test5(a, b, c, d, e), do: {a + b + c + d + e, {a, b, c, d, e}}

end