lib/may_fail.ex

defmodule Hike.MayFail do
  alias Hike.MayFail, as: MayFail

  @typedoc """
  generic input type `<T>`.
  """

  @type t :: any()

  @typedoc """
  generic return type `<TR>`.
  """
  @type tr :: any()

  @typedoc """
  `func()` represent a function which take no argument and return value of type `<TR>`.
  """
  @type func :: (() -> tr)

  @typedoc """
  `func(t)` represent a function which take an argument of type `<T>`
  and return a value of type `<TR>`.
  """
  @type func(t) :: (t -> tr)

  @typedoc """
  `mapper()` represent a mapping function which take no argument and return
  a value of type `<TR>`.
  """
  @type mapper :: (() -> tr)

  @typedoc """
  `mapper(t)` represent a mapping function which take an argument of type `<T>`
  and return a value of type `<TR>`.
  """
  @type mapper(t) :: (t -> tr)

  @typedoc """
  `binder()` represent a binding(mapping) function which take no argument and
  return a Mayfail of type `<TR>`.

  ## Example
      iex> success_bind_func = fn () -> Hike.MayFail.success(:ok) end
      iex> failure_bind_func = fn () -> Hike.MayFail.failure(:error) end
  """
  @type binder :: (() -> mayfail_success(tr) | mayfail_failure(tr))

  @typedoc """
  `binder(t)` represent a binding(mapping) function which take an argument of type `<T>`
    and return a `Mayfail` of type `<TR>`.

  ## Example
      iex> success_bind_func = fn (x) -> Mayfail.success(x) end
      iex> failure_bind_func = fn (y) -> Mayfail.failure(y) end
  """
  @type binder(t) :: (t -> mayfail_success(tr) | mayfail_failure(tr))

  @typedoc """
    generic input type `<T_Failure>` represent a type of value on `Failure`state.
  """
  @type t_failure :: any()

  @typedoc """
  generic input type `<T_Success>`represent a type of value on `Success`state.
  """
  @type t_success :: any()

  @typedoc """
  represent a type of `Mayfail` that could be in either `failure` or `success` state
  with a value of given type.
  """
  @type mayfail(t_failure, t_success) :: %__MODULE__{
          failure: t_failure,
          success: t_success,
          is_success?: boolean()
        }
  @typedoc """
  represent a type of `Mayfail` that could be in either `Failure` or `Success` state
  """
  @opaque mayfail() :: %__MODULE__{
            failure: t_failure(),
            success: t_success(),
            is_success?: boolean()
          }
  @typedoc """
  Elevated data type of `Mayfail` struct that represents `Success` state.
  """
  @type mayfail_success :: %__MODULE__{
          failure: nil,
          success: t_success(),
          is_success?: true
        }
  @typedoc """
  Elevated data type of `Mayfail` struct that represents `Success` state and have a value of type `<T>`.
  """
  @type mayfail_success(t) :: %__MODULE__{
          failure: nil,
          success: t,
          is_success?: true
        }

  @typedoc """
  represent a type of `Mayfail` in `Failure` state.
  """
  @type mayfail_failure :: %__MODULE__{
          failure: t_failure(),
          success: nil,
          is_success?: false
        }
  @typedoc """
  represent a type of `Mayfail` in `Failure` state.
  """
  @type mayfail_failure(t) :: %__MODULE__{
          failure: t,
          success: nil,
          is_success?: false
        }
  @doc """
  `%Hike.Mayfail{failure: t_failure(), success: t_success(), is_success?:boolean()}` is a struct that represents an "either/or" value.
  It can contain either a `Failure` value or a `Success` value, but not both.

  * `failure`: the failure value (if `is_success?` is false)
  * `success`: the success value (if `is_success?` is true)
  * `is_success?`: a boolean flag indicating whether the value is a success value (`true`) or a failure value (`false`)
  """
  defstruct [:failure, :success, :is_success?]

  @spec success(t_success) :: mayfail_success(t_success)
  def success(value) when value != nil,
    do: %__MODULE__{success: value, failure: nil, is_success?: true}

  @spec failure(t_failure) :: mayfail_failure(t_failure)
  def failure(error) when error != nil,
    do: %__MODULE__{success: nil, failure: error, is_success?: false}

  @doc """

  ## Example

      iex> apply_func = fn x -> (x + 2) end
      iex> MayFail.failure(1) |> MayFail.apply_success(apply_func)
      %Hike.MayFail{failure: 1, success: nil, is_success?: false}

      iex> MayFail.success(1) |> MayFail.apply_success(apply_func)
      %Hike.MayFail{failure: nil, success: 3, is_success?: true}

  """
  @spec apply_success(mayfail_success(t_success()), func(t_success())) :: mayfail_success(tr())
  def apply_success(%__MODULE__{success: value, is_success?: true} = _mayfail, func)
      when is_function(func, 1) do
    func.(value) |> success
  end

  @spec apply_success(mayfail_failure(t_failure()), func(t_success())) ::
          mayfail_failure(t_failure())
  def apply_success(%__MODULE__{failure: error, is_success?: false} = _mayfail, _func),
    do: failure(error)

  @doc """

  ## Example

      iex> apply_func = fn x -> x + 2 end
      iex> MayFail.failure(1) |> MayFail.apply_failure(apply_func)
      %Hike.MayFail{failure: 3, success: nil, is_success?: false}

      iex> MayFail.success(1) |> MayFail.apply_failure(apply_func)
      %Hike.MayFail{failure: nil, success: 1, is_success?: true}


  """
  @spec apply_failure(mayfail_failure(t_failure()), func(t_failure())) :: mayfail_failure(tr())
  def apply_failure(%__MODULE__{failure: error, is_success?: false} = _mayfail, func)
      when is_function(func, 1),
      do: func.(error) |> failure

  @spec apply_failure(mayfail_success(t_success()), func(t_failure())) ::
          mayfail_success(t_success())
  def apply_failure(%__MODULE__{success: value, is_success?: true} = _mayfail, _func),
    do: success(value)

  @doc """

  ## Example

      iex> mapper = fn x -> (x + 2) end
      iex> MayFail.failure(1) |> MayFail.map_success(mapper)
      %Hike.MayFail{failure: 1, success: nil, is_success?: false}

      iex> MayFail.success(1) |> MayFail.map_success(mapper)
      %Hike.MayFail{failure: nil, success: 3, is_success?: true}

  """
  @spec map_success(mayfail_success(t_success()), mapper()) :: mayfail_success(tr())
  def map_success(%__MODULE__{success: value, is_success?: true} = _mayfail, mapper)
      when is_function(mapper, 1) do
    mapper.(value) |> success
  end

  @spec map_success(mayfail_failure(t_failure()), mapper(t_success())) ::
          mayfail_failure(t_failure())
  def map_success(%__MODULE__{failure: error, is_success?: false} = _mayfail, _mapper),
    do: failure(error)

  @doc """

  ## Example

      iex> mapper = fn x -> x + 2 end
      iex> MayFail.failure(1) |> MayFail.map_failure(mapper)
      %Hike.MayFail{failure: 3, success: nil, is_success?: false}

      iex> MayFail.success(1) |> MayFail.map_failure(mapper)
      %Hike.MayFail{failure: nil, success: 1, is_success?: true}


  """
  @spec map_failure(mayfail_failure(t_failure()), mapper(t_failure())) :: mayfail_failure(tr())
  def map_failure(%__MODULE__{failure: error, is_success?: false} = _mayfail, mapper)
      when is_function(mapper, 1),
      do: mapper.(error) |> failure

  @spec map_failure(mayfail_failure(t_success()), mapper(t_failure())) ::
          mayfail_failure(t_success())
  def map_failure(%__MODULE__{success: value, is_success?: true} = _mayfail, _mapper),
    do: success(value)

  @doc """
  Binds a function that returns a `MayFail` value for an `MayFail` in the `Failure` state.
  If the input is in the `Success` state, the function is not executed and the input is returned as it is.

  ## Example

      iex> binder = fn x -> MayFail.success(x + 2) end
      iex> MayFail.failure(1) |> MayFail.bind_success(binder)
      %Hike.MayFail{failure: 1, success: nil, is_success?: false}

      iex> MayFail.success(1) |> MayFail.bind_success(binder)
      %Hike.MayFail{failure: nil, success: 3, is_success?: true}

      iex> binder = fn x -> MayFail.failure(x + 2) end
      iex>  MayFail.success(1) |> MayFail.bind_success(binder)
      %Hike.MayFail{failure: 3, success: nil, is_success?: false}

  """
  @spec bind_success(mayfail_success(t_success()), binder(t_success())) :: mayfail_success(tr())
  def bind_success(%__MODULE__{success: value, is_success?: true} = _mayfail, binder)
      when is_function(binder, 1),
      do: binder.(value)

  @spec bind_success(mayfail_failure(t_failure()), binder(t_success())) ::
          mayfail_failure(t_failure)
  def bind_success(%__MODULE__{failure: error, is_success?: false} = _mayfail, _binder),
    do: failure(error)

  @doc """
  Binds a function that returns a `MayFail` value for an `MayFail` in the `Failure` state.
  If the input is in the `Success` state, the function is not executed and the input is returned as it is.

  ## Example

      iex> binder = fn x -> MayFail.success(x + 2) end
      iex> MayFail.failure(1) |> MayFail.bind_failure(binder)
      %Hike.MayFail{failure: nil, success: 3, is_success?: true}

      iex> binder = fn x -> MayFail.failure(x + 2) end
      iex>  MayFail.failure(1) |> MayFail.bind_failure(binder)
      %Hike.MayFail{failure: 3, success: nil, is_success?: false}

      iex> MayFail.success(1) |> MayFail.bind_failure(binder)
      %Hike.MayFail{failure: nil, success: 1, is_success?: true}


  """

  @spec bind_failure(mayfail_failure(t_failure()), binder(t_failure())) :: mayfail_failure(tr())
  def bind_failure(%__MODULE__{failure: error, is_success?: false} = _mayfail, binder)
      when is_function(binder, 1),
      do: binder.(error)

  @spec bind_failure(mayfail_failure(t_success()), binder(t_failure())) ::
          mayfail_failure(t_success())
  def bind_failure(%__MODULE__{success: value, is_success?: true} = _mayfail, _binder),
    do: success(value)

  @doc """

  Matches an `Mayfail` value and applies the corresponding function.

  ## Example

      iex> Hike.MayFail.success(4) |> Hike.MayFail.match(fn x -> x + 3 end, fn y -> y + 2 end)
      6
  """

  @spec match(
          MayFail.mayfail(t_failure(), t_success()),
          (t_failure() -> tr()),
          (t_success() -> tr())
        ) :: tr()
  def match(%__MODULE__{failure: x, is_success?: false} = _mayfail, failure_fn, _)
      when is_function(failure_fn, 1),
      do: failure_fn.(x)

  def match(%__MODULE__{success: x, is_success?: true} = _mayfail, _, success_fn)
      when is_function(success_fn, 1),
      do: success_fn.(x)

  @doc """
  Check whether MayFail is in `Success` state or not.

  ## Example
      iex> Hike.MayFail.success(4) |> Hike.MayFail.is_success?
      true
      iex> Hike.MayFail.failure("fail") |> Hike.MayFail.is_success?
      false
  """

  @spec is_success?(mayfail_success()) :: true
  def is_success?(%__MODULE__{is_success?: true} = _mayfail), do: true
  @spec is_success?(mayfail_failure()) :: false
  def is_success?(_), do: false

  @doc """
  Check whether MayFail is in `Failure` state or not.

  ## Example

      iex> Hike.MayFail.success(4) |> Hike.MayFail.is_failure?
      false
      iex> Hike.MayFail.failure("fail") |> Hike.MayFail.is_failure?
      true
  """
  @spec is_failure?(mayfail_failure()) :: true
  def is_failure?(%__MODULE__{is_success?: false} = _mayfail), do: true
  @spec is_failure?(mayfail_success()) :: false
  def is_failure?(_), do: false
end