defmodule Monad.Maybe do
  @moduledoc """
  A monad that represents something or nothing.

  The concept of having something vs. nothing is similar to having a value vs.
  `nil`.
  """

  @typedoc """
  The possible types of values that can occur (i.e. something and nothing).
  """
  @type maybe_type :: :some | :none

  use Monad.Behaviour

  @opaque t :: %__MODULE__{type: maybe_type, value: term}
  @doc false
  defstruct type: :none, value: nil

  @doc """
  Returns a "nothing" state.
  """
  defmacro none, do: quote(do: %Monad.Maybe{})

  @doc """
  Wraps the value into a maybe monad.

      iex> some 42
      %Monad.Maybe{type: :some, value: 42}
  """
  @spec some(term) :: t
  def some(value), do: %Monad.Maybe{type: :some, value: value}

  @doc """
  An alias for `some/1`.
  """
  @spec pure(term) :: t
  def pure(value), do: some(value)

  @doc """
  Unwraps the value from a maybe monad.

  Does not work with `none` values, since they contain nothing.

      iex> m = some 42
      iex> unwrap! m
      42
  """
  @spec unwrap!(t) :: term
  def unwrap!(%Monad.Maybe{type: :some, value: value}), do: value

  @doc """
  Macro that indicates if the maybe monad contains nothing.

  This macro may be used in guard clauses.

      iex> none? none()
      true
      iex> none? some 42
      false
  """
  defmacro none?(maybe), do: quote(do: unquote(maybe) == none())

  @doc """
  Macro that indicates if the maybe monad contains something.

  This macro may be used in guard clauses.

      iex> some? none()
      false
      iex> some? some 42
      true
  """
  defmacro some?(maybe), do: quote(do: not none?(unquote(maybe)))

  @doc """
  Callback implementation of `Monad.Behaviour.return/1`.

  Wraps the value in a maybe monad.

      iex> return 42
      %Monad.Maybe{type: :some, value: 42}
  """
  @spec return(term) :: t
  def return(value), do: pure(value)

  @doc """
  Callback implementation of `Monad.Behaviour.bind/2`.

  If the monad contains a value, then the value is unwrapped and applied to
  `fun`.

  For `none` monads, `none` is returned without evaluating `fun`.

      iex> m = some 42
      iex> bind m, (& some &1 * 2)
      %Monad.Maybe{type: :some, value: 84}

      iex> bind none(), (& some &1 * 2)
      %Monad.Maybe{type: :none, value: nil}
  """
  @spec bind(t, (term -> t)) :: t
  def bind(maybe, fun) when none?(maybe) and is_function(fun, 1), do: maybe

  def bind(maybe, fun) when some?(maybe) and is_function(fun, 1) do
    maybe |> unwrap! |> fun.()
  end
end
