Skip to content

Commit

Permalink
Merge branch 'feature/operator-simplification'
Browse files Browse the repository at this point in the history
  • Loading branch information
alexocode committed Aug 17, 2018
2 parents d18d16e + 7e3969e commit 8bfb12b
Show file tree
Hide file tree
Showing 23 changed files with 105 additions and 531 deletions.
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.6.3-otp-20
erlang 20.1
erlang 21.0.5
elixir 1.7.0-otp-21
46 changes: 22 additions & 24 deletions lib/brex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ defmodule Brex do
# Operators
Also, as you might have noticed, I used an `all/1` function in the examples
above. This is the __compose__ part of `Brex`: it allows you to link rules
using boolean logic.
above. It's called a `Brex.Operator` and represents the __compose__ part of
`Brex`: it allows you to link rules using boolean logic.
It currently supports:
Expand Down Expand Up @@ -132,7 +132,7 @@ defmodule Brex do
iex> result = Brex.evaluate(rules, [])
iex> match? %Brex.Result{
...> evaluation: {:ok, [%Brex.Result{evaluation: true}, %Brex.Result{}]},
...> rule: %Brex.Operator.All{clauses: _},
...> rule: %Brex.Operator{clauses: _},
...> value: []
...> }, result
true
Expand Down Expand Up @@ -201,20 +201,8 @@ defmodule Brex do
@spec number_of_clauses(Types.rule()) :: non_neg_integer()
defdelegate number_of_clauses(rule), to: Rule

for operator <- Operator.default_operators() do
short_name =
operator
|> Macro.underscore()
|> Path.basename()
|> String.to_atom()

@doc "Shortcut for `Brex.#{operator}([rule1, rule2])`."
@spec unquote(short_name)(rule1 :: Types.rule(), rule2 :: Types.rule()) :: Operator.t()
def unquote(short_name)(rule1, rule2) do
unquote(short_name)([rule1, rule2])
end

@doc """
operator_doc = fn operator ->
"""
Links the given rules in a boolean fashion, similar to the `Enum` functions.
- `all` rules have to pass (`and` / `&&`)
Expand All @@ -223,19 +211,29 @@ defmodule Brex do
# Examples
iex> Brex.#{short_name} &is_list/1, &is_map/1
%#{inspect(operator)}{
iex> Brex.#{operator} &is_list/1, &is_map/1
%Brex.Operator{
aggregator: &Brex.Operator.Aggregator.#{operator}?/1,
clauses: [&:erlang.is_list/1, &:erlang.is_map/1]
}
iex> Brex.#{short_name} [&is_list/1, &is_map/1, &is_binary/1]
%#{inspect(operator)}{
iex> Brex.#{operator} [&is_list/1, &is_map/1, &is_binary/1]
%Brex.Operator{
aggregator: &Brex.Operator.Aggregator.#{operator}?/1,
clauses: [&:erlang.is_list/1, &:erlang.is_map/1, &:erlang.is_binary/1]
}
"""
@spec unquote(short_name)(list(Types.rule())) :: Operator.t()
def unquote(short_name)(rules) do
Operator.new(unquote(operator), rules)
end

for operator <- [:all, :any, :none] do
@doc "Shortcut for `Brex.#{operator}([rule1, rule2])`."
@spec unquote(operator)(rule1 :: Types.rule(), rule2 :: Types.rule()) :: Operator.t()
def unquote(operator)(rule1, rule2) do
unquote(operator)([rule1, rule2])
end

@doc operator_doc.(operator)
@spec unquote(operator)(list(Types.rule())) :: Operator.t()
defdelegate unquote(operator)(rules), to: Brex.Operator.Defaults
end
end
156 changes: 36 additions & 120 deletions lib/brex/operator.ex
Original file line number Diff line number Diff line change
@@ -1,135 +1,51 @@
defmodule Brex.Operator do
@moduledoc """
Contains the `Aggregatable` root protocol for operators, provides some helpers
for Operators and is `use`able to define ...
A struct which represents a rule linking two or more other rules together. It
does this by accepting a list of `clauses` and an `aggregator`, being an arity
1 function which reduces a list of booleans into a single boolean.
# Custom Operators
**TL;DR**
Creating a custom operator is merely a case of wrapping your rules into the
`Brex.Operator` struct and providing your custom `aggregator` alongside.
1. `use Brex.Operator`
2. define a struct with a `clauses` key (`defstruct [:clauses]`)
3. define an `aggregator/1` function and return the aggregating function
## Example
There are various `use` options to control this behaviour and to make your
life easier.
## Options
### `aggregator`
This controls the aggregator definition, it can receive:
- a function reference: `&Enum.all?/1`
- an anonymous function: `&Enum.all(&1)` or `fn v -> Enum.all(v) end`
- an atom, identifying a function in this module: `:my_aggregator`
- a tuple, identifying a function in a module: `{MyModule, :my_aggregator}`
### `clauses`
Allows to override the expected default key (`clauses`) for contained
"sub-rules".
# How does this magic work?
Brex operators are based on the `Brex.Operator.Aggregatable` protocol. When
calling `use Brex.Operator` Brex tries to define a number of functions for you
which it then uses to implement the protocol. The protocol calls then simply
delegate to the functions in your custom operator module.
Furthermore it defines an `evaluate/2` function which is necessary to actually
use this operator as a Brex rule. This might change in the future, to make
implementing the `Aggregatable` protocol sufficient for defining custom operators.
To do all of this it calls the `Brex.Operator.Builder.build_from_use/1`
function, which does a number of things.
1. it defines an `aggregator/1` function, if an `aggregator` option has been given
2. it defines a `clauses/1` function, which extracts the clauses from the struct
3. it defines a `new/2` function, which news an operator with some clauses
After that it tries to define the implementation of the `Aggregatable`
protocol, which simply delegates it's calls to the using module.
Due to that it checks if the necessary functions (`aggregator/1` and
`clauses/1`) exist. In case they don't exist, a `CompileError` is being raised.
%Brex.Operator{
rules: [my_rule1, my_rule2],
aggregator: my_aggregation_function # For example &Enum.all?/1
}
"""

alias Brex.Types

# A struct implementing this behaviour
@type t :: struct()

@type clauses :: list(Types.rule())

defprotocol Aggregatable do
@type clauses :: list(Brex.Types.rule())

@spec aggregator(t()) :: (list(boolean()) -> boolean())
def aggregator(aggregatable)

@spec clauses(t()) :: clauses()
def clauses(aggregatable)

@spec new(t(), clauses()) :: t()
def new(aggregatable, clauses)
use Brex.Rule.Struct

@type aggregator :: (list(boolean()) -> boolean())
@type clauses :: list(Brex.Types.rule())
@type t :: %__MODULE__{
aggregator: aggregator(),
clauses: clauses()
}
defstruct [:aggregator, :clauses]

def evaluate(%__MODULE__{} = rule, value) do
results = evaluate_clauses(rule, value)

if aggregate(rule, results) do
{:ok, results}
else
{:error, results}
end
end

defmacro __using__(opts) do
Brex.Operator.Builder.build_from_use(opts)
defp evaluate_clauses(%{clauses: clauses}, value) do
Enum.map(clauses, &Brex.evaluate(&1, value))
end

@spec default_operators() :: list(module())
def default_operators do
[
Brex.Operator.All,
Brex.Operator.Any,
Brex.Operator.None
]
defp aggregate(%{aggregator: aggregator}, results) do
results
|> Enum.map(&Brex.passed?/1)
|> aggregator.()
end

@doc """
Returns a new instance of an operator; not meant to be used directly but is
instead used internally when calling the operator shortcut functions on `Brex`.
"""
@spec new(operator :: module(), rules :: clauses()) :: t()
def new(operator, rules) do
operator
|> struct()
|> Aggregatable.new(rules)
end

@doc """
Returns the rules contained in the Operator. Raises a `Protocol.UndefinedError`
if the given value does not implement `Brex.Operator.Aggregatable`.
## Examples
iex> Brex.Operator.clauses!(Brex.all([&is_list/1, &is_map/1]))
[&is_list/1, &is_map/1]
"""
@spec clauses!(t()) :: clauses()
defdelegate clauses!(operator), to: Aggregatable, as: :clauses

@doc """
Returns `{:ok, list(t())}` if the rule implements Brex.Operator.Aggregatable
and `:error` otherwise.
## Examples
iex> Brex.Operator.clauses(Brex.all([&is_list/1, &is_map/1]))
{:ok, [&is_list/1, &is_map/1]}
iex> Brex.Operator.clauses(&is_list/1)
:error
iex> Brex.Operator.clauses("foo_bar")
:error
"""
@spec clauses(t()) :: {:ok, clauses()} | :error
def clauses(operator) do
case Aggregatable.impl_for(operator) do
nil -> :error
impl -> {:ok, impl.clauses(operator)}
end
end
def clauses(%__MODULE__{clauses: clauses}), do: {:ok, clauses}
def clauses(_), do: :error
end
8 changes: 8 additions & 0 deletions lib/brex/operator/aggregator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Brex.Operator.Aggregator do
@moduledoc false

defdelegate all?(enum), to: Enum
defdelegate any?(enum), to: Enum

def none?(enum), do: not all?(enum)
end
6 changes: 0 additions & 6 deletions lib/brex/operator/all.ex

This file was deleted.

6 changes: 0 additions & 6 deletions lib/brex/operator/any.ex

This file was deleted.

114 changes: 0 additions & 114 deletions lib/brex/operator/builder.ex

This file was deleted.

Loading

0 comments on commit 8bfb12b

Please sign in to comment.