# # 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