defmodule Sky do @moduledoc """ Collection of higher-oder functions not present in the standard library. """ @doc """ Curries the given function, starting with an optional list of given params. ## Examples The order of the arguments is respected. iex> Sky.curry(fn(a, b) -> a - b end).(5).(4) 1 You can also pass it a list of predefined parameters. iex> Sky.curry(fn(a, b, c) -> a + b + c end, [1, 2]).(3) 6 """ def curry(f, given \\ []) when is_function(f) do n = arity(f) if length(given) == n do apply(f, given) else fn x -> curry(f, Enum.reverse([x|given])) end end end @doc """ Given a function *f* with arity *n*, return a function that receives a single tuple of *n* elements and applies them to the original *f*. ## Example iex> Sky.tupleize(fn(a, b) -> a + b end).({1, 2}) 3 It might seem less useful for functions of one argument, but you might gain composability with other functions from the module. """ def tupleize(f) when is_function(f) do n = arity(f) fn tuple when is_tuple(tuple) and tuple_size(tuple) == n -> apply(f, Tuple.to_list(tuple)) end end @doc """ Given a *single-argument* function *f*, return a new function that receives either a tuple in the form `{:ok, value}` which would execute `f.(value)`, or any other term, which would be returned without change. In case of success, the return value of the returned function is *always* in the form `{:ok, value}` even if the result of *f* was in the same form. This means that any nesting is eliminated. ## Example iex> inc = Sky.lift_ok(fn n -> n + 1 end) iex> inc.({:ok, 1}) {:ok, 2} iex> inc.({:error, :bad_input}) {:error, :bad_input} Note that the value is always a two-element tuple `{:ok, v}`. iex> inc = Sky.lift_ok(fn n -> n + 1 end) iex> inc.(inc.({:ok, 1})) {:ok, 3} """ def lift_ok(f) when is_function(f) do fn {:ok, value} -> flatten(f.(value)) error -> error end end defp flatten({:ok, value}), do: flatten(value) defp flatten(value), do: {:ok, value} @doc """ Given a two argument functions, swap the order in wich the arguments are received. ## Examples iex> Sky.swap(&rem/2).(7, 5) 5 iex> Sky.swap(fn(a, b) -> a - b end).(2, 1) -1 """ def swap(f) when is_function(f) do fn(a, b) -> f.(b, a) end end @doc """ Creates a function that receives an argument and always returns the original given value. ## Example iex> one = Sky.constant(1) iex> one.(2) 1 iex> one.(nil) 1 """ def constant(value) do fn _ -> value end end @doc ~S""" Given a *one-argument* function that raises an exception, return a function that instead returns any of the following tuples: * {:ok, value} when no exception is raised * {:error, exception} when an exception is raised. ## Examples iex> safe_float_div = Sky.noraise(fn ({a, b}) -> a / b end) iex> safe_float_div.({1, 0}) {:error, %ArithmeticError{message: "bad argument in arithmetic expression"}} iex> safe_float_div.({1, 2}) {:ok, 0.5} """ def noraise(f) when is_function(f) do fn x -> try do {:ok, f.(x)} rescue exception -> {:error, exception} end end end @doc """ Given two one-argument functions, a subject *f* and a predicate *p*, return a function that takes an argument *x*. Such function returns `{:ok, f.(x)}` only if `p.(x)` is truthy, :error otherwise. ## Example iex> f = fn x -> x - 1 end iex> p = fn x -> x > 0 end iex> Sky.reject_if(f, p).(1) {:ok, 0} iex> Sky.reject_if(f, p).(0) :error """ def reject_if(f, p) when is_function(f) and is_function(p) do fn x -> if p.(x), do: {:ok, f.(x)}, else: :error end end @doc """ Given two one-argument functions, a subject *f* and a predicate *p*, return a function that takes an argument *x*. However, in contrast to `Sky.reject_if/2`, if the predicate is not satisfied, the parameter *x* is returned. Otherwise the return value is `f.(x)`. ## Example iex> trim = (&tl/1) iex> non_empty? = fn list -> length(list) > 0 end iex> safetrim = Sky.apply_if(trim, non_empty?) iex> safetrim.([1,2]) [2] iex> safetrim.([]) [] """ def apply_if(f, p) when is_function(f) and is_function(p) do fn x -> if p.(x), do: f.(x), else: x end end @doc """ Given a predicate *p*, which receives a single argument and returns a boolean value, return a function which is equivalent to `not p.(x)` ## Example iex> negative? = fn x -> x < 0 end iex> no_negative? = Sky.negate(negative?) iex> no_negative?.(1) true """ def negate(p) when is_function(p) do fn v -> not p.(v) end end @doc """ Get the arity of the given function. ## Examples iex> Sky.arity(&Enum.map/2) 2 iex> Sky.arity(fn(_, _, _) -> nil end) 3 """ def arity(f) when is_function(f) do :erlang.fun_info(f)[:arity] end end