Proof-of-concept Elixir Contracts

I was toying around to see if I could possibly get something similar to Clojure-style pre/post enforcement. This is what I came up with and would like your input. Thanks.


Example Usage

  defmodule Example do
    require Contract

    def reverse_abc(x) do
      # Contract pre processing
      pre_must_be_abc = fn -> x == "abc" end
      pre = [
        pre_must_be_abc,
      ]

      # Contract post processing
      post_must_be_cba = fn x -> x == "cba" end
      post_must_be_string = fn x -> is_binary x end
      post = [
        post_must_be_cba,
        post_must_be_string,
      ]

      # Contract
      # The arguments here are `[x]` and do not represent the current value of `x`
      reverse_abc_contract = Contract.new([x], [pre: pre, post: post], String.reverse(x))

      # Encorce Contract
      reverse_abc_contract.(x)
    end
  end

Basic Implementation

    defmodule Contract do

    defmodule EnforcePreError do
      defexception message: "Argument(s) Failed Pre Processing Requrements."
    end

    defmacro enforce(:pre, list, expr) do
      quote do
        valid? =
          Enum.map(unquote(list), fn f -> f.() end)
          |> Enum.all?(&(&1 == true))

        case valid? do
          true -> unquote(expr)
          _ -> raise EnforcePreError
        end
      end
    end

    defmodule EnforcePostError do
      defexception message: "Return value Failed Post Processing Requrements."
    end

    defmacro enforce(:post, list, value) do
      quote do
        valid? =
          Enum.map(unquote(list), fn f -> f.(unquote(value)) end)
          |> Enum.all?(&(&1 == true))

        case valid? do
          true -> unquote(value)
          _ -> raise EnforcePostError
        end
      end
    end

    defmodule EnforceNothingError do
      defexception message: "No enforcements are defined."
    end

    defmacro enforce(:prepost, pre, post, expr) do
      quote do
        pre = unquote(pre) || []
        post = unquote(post) || []
        no_enforcements? = (pre == [] && post == [])
        get_value = fn ->
          unquote(__MODULE__).enforce(:pre, unquote(pre), unquote(expr))
        end

        cond do
          no_enforcements? -> raise EnforceNothingError
          pre && post -> unquote(__MODULE__).enforce(:post, unquote(post), get_value.())
          (pre == []) && post -> unquote(__MODULE__).enforce(:post, unquote(post), unquote(expr))
          (post == []) && pre -> get_value.()
        end
      end
    end

    defmacro new(args, opts, expr) do
      quote do
        fn (unquote_splicing(args)) ->
          unquote(__MODULE__).enforce(
            :prepost, unquote(opts[:pre]), unquote(opts[:post]), unquote(expr)
          )
        end
      end
    end
  end
1 Like