Outstanding

View Source

Outstanding: something not yet dealt with.

The outstanding protocol is for those times when you want to know if any or which expectations have not been sufficiently met, and equality doesn't actually do it for you.

We have two terms, expected and actual, apply outstanding(expected, actual) and see if nothing is outstanding nil, or we get back what is outstanding for us to work on.

Outstanding implementations are provided for most types, as well as helper functions to make it easy to add implementations for your structs. Outstanding implementations which accept expected Functions are provided, providing a point of extensibility. Convenience expected functions are also provided.

iex> import Outstanding
iex> outstanding(%{x: :a, y: :b}, %{})
%{y: :b, x: :a}
iex> outstanding(%{x: :a, y: :b}, %{y: :b})
%{x: :a}
iex> outstanding(%{x: :a, y: :b}, %{x: :a, y: :b})
nil
iex> outstanding(%{x: :a, y: :b}, %{x: :a, y: :b, z: :c})
nil

Installation

If available in Hex, the package can be installed by adding outstanding to your list of dependencies in mix.exs:

def deps do
  [
    {:outstanding, "~> 0.3.0"}
  ]
end

Tutorial

To get started you need a running instance of Livebook

Run in Livebook

Outstanding?

Outstanding.outstanding? simply calls Outstanding.outstanding, and is true if anything is outstanding, or false if nil outstanding.

iex> import Outstanding
iex> outstanding?(%{x: :a, y: :b}, %{y: :b})
true
iex> outstanding?(%{x: :a, y: :b}, %{x: :a, y: :b})
false

Supported Types

Out of the box we have outstanding protocol implementations for the following types:

Elixir Type ModuleType ExampleNotesResolving TypesRelated Expected Functions
Atom:anil is an AtomAtomany_atom, non_nil_atom
BitString"a"BitStringany_bitstring
BooleantrueBooleanany_boolean
Date~D[2025-02-25]Dateany_date, current_date, future_date, past_date
DateTimeU[2025-02-25 11:59:00.00Z]DateTimeany_date_time, current_date_time, future_date_time, past_date_time
Duration%Duration{minute: 60}Durationany_duration
Float1.1Float, Integerany_float, any_number
Function&Outstand.non_nil_atom/1actual is argumentAny-
Integer1Integer, Float, Rangeany_integer, any_number
Keyword[a: :a]handled by List(Keyword) Listnon_empty_keyword
List[:a]Listany_list, empty_list, non_empty_list
MapSetMapSet.new([:a])uses differenceMapSetany_map_set, empty_map_set, non_empty_map_set
Map{a: :b, c: :d}strictMap, any Structany_map, empty_map, non_empty_map
NaiveDateTime~N[2025-02-25 11:59:00]NaiveDateTimeany_naive_date_time, future_time, current_time, past_time
Range1Range, Integerany_range
Regex~r/foo/actual is argumentString.Chars implementations-
Time~T[11:59:00.000]Timeany_time, current_time, future_time, past_time
Tuple{a: :b}handled by AnyTupleany_tuple

Maps call outstanding on each element is expected, but allow extra elements in actual. Maps can also be resolved by Structs.

Keywords are Lists of Tuples but are handled like Maps.

Lists (other than Keywords) are strict in that they must be in order, so lists must have the same number of elements for outstanding to be nil. Outstanding is attempted on each pair of expected/actual elements, even when they have unequal number, in order to return a list of resolved (nil) or outstanding elements. If all expected elements are resolved however there are extra actual elements a list of nils is returned, of length expected.

MapSets are not strict in that actual may contain additional elements, however MapSet.difference is used on the elements (which uses equals not outstanding).

Tuples not part of Keywork List are matched using Any, which uses equals.

Date, Time, DateTime and NaiveDateTime are supported, where Expected Function current_date matches today's date, other current times are now +/- 1 min.

Function must be an outstanding function which takes a single argument actual, such as the expected functions.

Regex can be any regex operating on the actual BitString, uses Regex.match?

Of course you can easily implement the outstanding protocol for your own type (especially structs) using the defoutstanding macro.

Expected Functions

Sometimes our expectation is a bit vague, for instance in the example above we initially did not know the id. We can supply a function as an expectation, when not met this supplies a corresponding atom.

An expected function of arity 1 implicitly has actual as the argument.

iex> import Outstanding
Outstanding
iex> use Outstand
Outstand

iex> outstanding(&Outstand.any_integer/1, 546)
nil
iex> outstanding(&Outstand.any_integer/1, nil)
:any_integer

&Outstand.any_integer/1 is one of many convenience functions in Outstand.

  @spec any_integer(any()) :: :any_integer | nil
  def any_integer(actual) do
    if is_integer(actual) do
      nil
    else
      :any_integer
    end
  end

There are are number of included expected functions, see the table in Supported Types for the types they relate to.

Expected functions of arity 2 are also supported. These have the form of a tuple of function and term, where term is an argument or argument list.

Expected FunctionExpected TypeResolving TypesBehaviour
all_ofList, Map, Keyword ListList, Map, Keyword Listexpects all expected elements to be resolved by any actual element
any_ofList, Map, Keyword ListAnyexpects at least one expected element to be resolved by actual
none_ofList, Map, Keyword ListList, Map, Keyword Listexpects no expected elements to be resolved by any actual element
one_ofList, Map, Keyword ListList, Map, Keyword Listexpects exactly one expected element to be resolved by any actual element
less_thanDurationDurationexpects actual to be less than value
greater_thanDurationDurationexpects actual to be greater than value
bounded_byDurationDurationexpects actual to be bounded by [min_value, max_value]
unbounded_byDurationDurationexpects actual to be not bounded by [min_value, max_value]

You can supply your own functions where needed.

Exceeds and Difference Operators

For convenient use in expressions we've implemented operators.

The 'exceeds' operator tells us whether our expectations exceed our actual. expected >>> actual is equivalent to Outstanding.outstanding?(expected, actual)

The 'difference' operator tells us what expectations remain unmet. expected --- actual is equivalent to Outstanding.outstanding(expected, actual)

Example of infix shortcuts usage:

iex> use Outstand
Outstand

iex> [:a, :b] --- [:a, :b]
nil
iex> [:a, :b] >>> [:a, :b]
false
iex> %{x: :a, y: :b} >>> %{y: :b}
true
iex> %{x: :a, y: :b} --- %{y: :b}
%{x: :a}

Expecting Anything or Nothing

We've taken a nil expectation to mean that we have no expectations, so are satisified by actual anything.

However we often need to expect actual nothing, say we managed something that shouldn't exist now, and we want to check for this. We have two alternatives for this. In the first we can expect :explicit_nil which is only satisfied with :explicit_nil:

iex> Outstanding.outstanding(:explicit_nil, :explicit_nil)
nil
iex> Outstanding.outstanding(:explicit_nil, "a")
:explicit_nil
iex> Outstanding.outstanding(:explicit_nil, nil)
:explicit_nil

There is a helper function &Outstand.explicit_nil/1, but it behaves identically. The disadvantage here is that actual needs to be coded with :explicit_nil, rather than nil or simply missing keys, which requires transformation of actual ahead of differencing with outstanding.

Another alternative is the :no_value atom, which is only used as an expected value, where the expectation is a key-value, such as a Map or Keyword List element. It expects that there is no value, either due to there being no key, or the key having a value of nil. This can be very useful as we can avoid an actual transformation. The no_value expectation can be resolved by actual :no_value or nil.

iex> Outstanding.outstanding(%{a: :no_value}, %{})
nil
iex> Outstanding.outstanding(%{a: :no_value}, %{a: nil})
nil
iex> Outstanding.outstanding(%{a: :no_value}, %{a: "a"})
%{a: :no_value}
iex> Outstanding.outstanding([a: :no_value], [])
nil
iex> Outstanding.outstanding([a: :no_value], [a: nil])
nil
iex> Outstanding.outstanding([a: :no_value], [a: "a"])
[a: :no_value]
iex> Outstanding.outstanding(:no_value, :no_value)
nil
iex> Outstanding.outstanding(:no_value, nil)
nil
iex> Outstanding.outstanding(:no_value, "a")
:no_value

Implementing Outstanding for other types

This requires some though as to what it means to 'resolve' your expected type with actual.

derive for Structs

Outstanding implements the __deriving__/3 callback so you can simply derive an Outstanding implementation when you define your struct. By default this performs outstanding on all fields, and requires the actual struct to be of the same type.

defmodule ABC do
  @derive Outstanding
  defstruct [:a, :b, :c]
end

You can also exclude fields with the except option

  defmodule AB do
    @derive {Outstanding, except: [:c]}
    defstruct [:a, :b, :c]
  end

defoutstanding macro

More flexibly, the defoutstanding macro can be used to implement outstanding on other types, including your own structs.

use Outstand

defmodule XYZ do
  defstruct [:x, :y, :z]
end

defoutstanding expected :: XYZ, actual :: Any do
  case {expected, actual} do
    {nil, nil} ->
      nil
    {_, ^expected} ->
      nil
    {%name{}, %name{}} ->
      expected
      |> Map.from_struct()
      |> Outstanding.outstanding(Map.from_struct(actual))
      |> Outstand.map_to_struct(name)
    {_, _} ->
      # not an exact match so default to outstanding
      expected
  end
end

If you are using Ash, then consider using the ash_outstanding extension which enables you to implement Outstanding protocol on your Ash Resources with a simple DSL.

Testing

use Outstand expression also provides 3 utilities which can auto-generate ExUnit tests for implementation of Outstanding protocol for your types:

iex>use Outstand

iex> gen_something_outstanding_test("value outstanding", "a", "b")
iex> gen_nothing_outstanding_test("realized", "a", "a")
iex> gen_result_outstanding_test("value result", "a", "b", "a")

Example

We often have minimum expectations that must be met, which when not met by actuality are outstanding. However we may also be happy for these expectations to be exceeded.

We may expect something to exist, but we may not know its identifier yet. If we have a list of managed child services, and imagine a scenario where while our backup feature is enabled, we should have a backup child service, however before we acquire it we won't know its identity.

scenarioexpectedactualoutstanding?outstanding
no backup[][]falsenil
enable backup - commmenced[%{alias: :backup, state: :ok}][]true[%{alias: :backup, state: :ok}]
enable backup - backup created[%{alias: :backup, state: :ok}][%{alias: :backup, id: 453, state: :starting}]true[%{alias: :backup, state: :ok}]
enable backup - backup bound[%{alias: :backup, id: 453, state: :ok}][%{alias: :backup, id: 453, state: :ok}]falsenil

Once we've created a backup child we want to keep track of it, so we refine the expectation to include it's specific id. We also monitor its behaviour and apply corrective action, just like a real child.

An application using outstanding would update expected, then do work based on what is outstanding given actual.Outstanding can be further processed (by your code) to detemine next action based on your priority of goals not met, constraints, business rules, etc.

Acknowledgements

Thanks to Ilja Tkachuk for comparable which was an exemplar.

Kudos to the Elixir Core Team for elixir 🚀

Diffo.dev

ash_outstanding

documentation