-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/operator-simplification'
- Loading branch information
Showing
23 changed files
with
105 additions
and
531 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.