plymio_enum v0.1.0 Plymio.Enum.Transform.Dictionary

Managing a Dictionary of Transform Functions for Enumerables.

This module support the creation and use of a dictionary of named transform functions.

It also supports the composition of higher level transforms from transforms in the dictionary tother and/or new pipelines. Composed transforms can themselves be saved in the dictionary.

It uses Plymio.Enum.Transform for support having peer functions build/2, transform/3 and realise/3 that layer dictionary management.

Building a Transform Dictionary

A transform dictionary can be built using build/1, and updated using build/2, from either a Map or Keyword where the keys are the transform function name.

Each value must be one or more elements that are either supported by Plymio.Enum.Transform.build/1 or an existing dictionary key. See the examplea for the range of options.

The returned transform dictionary is a struct i.e. %Plymio.Enum.Transform.Dictionary{}

iex> td_test1 = [
...>
...>   # v transforms
...>   v_f_number:  [filter: fn v -> is_number(v) end],
...>   v_f_gt_0:    [filter: fn v -> v > 0 end],
...>   v_m_squared: [map: fn v -> v * v end],
...>   v_m_plus_42: [map: fn v -> v + 42 end],
...>   v_r_lt_45:   [reject: fn v -> v < 45 end],
...>   v_r_gt_50:   [reject: fn v -> v > 50 end],
...>
...>   # {k,v} 2tuple transforms
...>   kv_f_v_number:   [filter: fn {_k,v} -> is_number(v) end],
...>   kv_f_v_gt_0:     [filter: fn {_k,v} -> v > 0 end],
...>   kv_m_v_squared:  &(Stream.map(&1, fn {k,v} -> {k, v * v} end)),
...>   kv_m_v_plus_42:  [map: fn {k,v} -> {k, v + 42} end],
...>   kv_r_v_lt_45:    &(Stream.reject(&1, fn {_k,v} -> v < 45 end)),
...>   kv_r_v_gt_50:    [reject: fn {_k,v} -> v > 50 end],
...>
...>   v_f_gt_0_m_cubed: [&(Stream.reject(&1, fn v -> v > 0 end)),
...>                      &(Stream.map(&1, fn v -> v * v * v end))],
...>
...>   v_f_lt_42_m_squared_and_sum: [
...>     [filter: fn v -> v < 42 end],
...>     &(Stream.map(&1, fn v -> v * v end)),
...>     :sum],
...>
...>   # composed from existing dictionary keys (transforms) and pipelines
...>   v_f_number_gt_0: [:v_f_number, :v_f_gt_0],
...>   v_f_number_gt_0_lt_10: [:v_f_number_gt_0, [filter: fn v -> v < 10 end]],
...>   v_m_squared_plus_42: [:v_m_squared, :v_m_plus_42],
...>   v_f_number_gt_0_m_squared_plus_42: [:v_f_number_gt_0, :v_m_squared_plus_42],
...>   v_f_number_gt_0_m_squared_plus_42_minus_7: [
...>     :v_f_number_gt_0_m_squared_plus_42,
...>     [map: fn v -> v - 7 end]],
...>
...>   # copy of an existing key
...>   ensure_only_numbers: :v_f_number,
...> ]
...> |> build
iex> match?(%Plymio.Enum.Transform.Dictionary{}, td_test1)
true

Notes:

  1. Each value can be one or more (List) of:

    • transform function (e.g. &(Stream.map(&1, fn {k,v} -> {k, v * v} end)))
    • transform pipeline (e.g. [filter: fn v -> is_number(v) end])
    • discrete transform (e.g. :sum),
    • existing dictionary transform (key) (e.g. : v_f_number)
  2. :v_m_squared and :v_r_lt_45 values are explicit transform functions.

  3. :v_f_gt_0_m_cubed is a list of transform_functions.

  4. :v_f_lt_42_m_squared_and_sum is a mix (pipeline) of valid values.

  5. :v_f_number_gt_0 is composed from existing transforms in the dictionary.

  6. :v_ensure_only_numbers is a copy of :v_f_number

  7. When the value is an Atom it could be a discrete transform (e.g. :sum) or an existing transform (e.g. :v_f_number) in the dictionary. Preference is given to existing dictionary transform.

  8. Transforms composed from other dictionary keys are “frozen” and do not track changes to the keys used to compose them.

  9. The dictionary is built one key at a time so “later” keys (e.g. :v_m_squared_plus_42) can be composed from “earlier” keys (:v_m_squared and :v_m_plus_42).

To make the following tests less cluttered, the above dictionary has been extracted into the helper function helper_dictionary_build_test1.

The two functions transform/3 and realise/3 complement Plymio.Enum.Transform.transform/2 and Plymio.Enum.Transform.realise/2.

Using a Transform Dictionary

This example selects (filters) just the numbers in the enumerable, returning a lazy enumerable. Normally transform returns a lazy enumerable (as does its peer Plymio.Enum.Transform.transform/2).

iex> td_test1 = helper_dictionary_build_test1()
 ...> result = [-1, make_ref(), 1, :atom, 2, "string", 3, &(&1), 4.25]
 ...> |> transform(td_test1, :v_f_number)
 ...> match?(%Stream{}, result)
 true

Calling realise/3 will return the actual result:

iex> td_test1 = helper_dictionary_build_test1()
 ...> [-1, make_ref(), 1, :atom, 2, "string", 3, &(&1), 4.25]
 ...> |> realise(td_test1, :v_f_number)
 [-1, 1, 2, 3, 4.25]

This example uses one of the composed transforms (:v_f_number_gt_0_m_squared_plus_42_minus_7) to filter just the numbers, square them, add 42 and subtract 7:

iex> td_test1 = helper_dictionary_build_test1()
 ...> [-1, make_ref(), 1, :atom, 2, "string", 3, &(&1), 4.25]
 ...> |> realise(td_test1, :v_f_number_gt_0_m_squared_plus_42_minus_7)
 [36, 39, 44, 53.0625]

Multiple transforms can be given as a list. (The last transform — :sum — always returns a real value)

iex> td_test1 = helper_dictionary_build_test1()
 ...> [-1, make_ref(), 1, :atom, 2, "string", 3, &(&1)]
 ...> |> transform(td_test1, [:v_f_number, :v_f_lt_42_m_squared_and_sum])
 15

A mix of transform forms (keys, functions, pipeline, discrete) can be given.

iex> td_test1 = helper_dictionary_build_test1()
 ...> [-1, make_ref(), 1, :atom, 2, "string", 3, &(&1)]
 ...> |> transform(td_test1, [:v_f_number, :v_f_gt_0, [map: fn v -> v*v*v end], :sum])
 36

Note a single pipeline must be inside another list

iex> td_test1 = helper_dictionary_build_test1()
 ...> [1,2,3] |> realise(td_test1, [[map: fn v -> v*v end]])
 [1,4,9]

Summary

Functions

build/1 builds a transform dictionary and build/2 will update it

The count function works as expected

The delete function works as expected

The fetch! accessor can take one or more keys, returning one or more (List) transform_functions. Unknown keys raise a KeyError

The get function takes one or more keys, returning a transform_function or list of transform_functions

The has_key? function works as expected

The keys accessor works as expected

The put function supports either a transform_function or a pipeline of discrete transforms

realise/3 applies transforms from the tranform dictionary to an enumerable

transform/3 applies transform functions from the transform dictionary to an enumerable

The values function works as expected

Types

transform_build_key()
transform_build_key() :: transform_name
transform_build_map()
transform_build_map() :: %{required(transform_build_key) => transform_build_value}
transform_build_opt()
transform_build_opt() :: {transform_build_key, transform_build_value}
transform_build_opts()
transform_build_opts() :: [transform_build_opt]
transform_build_value()
transform_build_value() :: transform_function | transform_pipeline
transform_dictionary()
transform_dictionary() :: %Plymio.Enum.Transform.Dictionary{transforms: term}
transform_function()
transform_functions()
transform_functions() :: [transform_function]
transform_name()
transform_name() :: atom
transform_names()
transform_names() :: transform_name | [transform_name]
transform_pipeline()

Functions

build(td \\ nil, opts \\ [])

build/1 builds a transform dictionary and build/2 will update it.

See the main example at the top.

Examples

To create a transform_dictionary, call build/ with a Map or Keyword:

iex> td = [
...>   f1: [filter: [fn v -> is_number(v) end, fn v -> v > 0 end]],
...>   m1: [map: [fn v -> v * v end, fn v -> v + 42 end]],
...>   r1: [reject: [fn v -> v < 45 end, fn v -> v > 50 end]],
...> ]
...> |> build
iex> match?(%Plymio.Enum.Transform.Dictionary{}, td)
true

To update a transform_dictionary, call build/2 with the transform_dictionary and a Map or Keyword:

iex> opts1 = [
...>   f1: [filter: [fn v -> is_number(v) end, fn v -> v > 0 end]],
...>   m1: [map: [fn v -> v * v end, fn v -> v + 42 end]],
...>   r1: [reject: [fn v -> v < 45 end, fn v -> v > 50 end]],
...> ]
...> td1 = opts1 |> build
...>
...> # build new transform dictionary updating :f1 and adding three new transforms.
...> opts2 = [
...>   f1: [filter: [fn v -> is_atom(v) end]],
...>   m2: [map: [fn v -> v * v * v end, fn v -> v - 99 end]],
...>   r2: &(Stream.reject(&1, fn v -> v < 0 end)),
...>   s1: :sum
...> ]
...> td2 = td1 |> build(opts2)
...> td2 |> keys
[:f1, :m1, :m2, :r1, :r2, :s1]
count(td)
count(transform_dictionary) :: integer

The count function works as expected:

iex> helper_dictionary_build_test1() |> count
 20

The delete function works as expected:

iex> helper_dictionary_build_test1()
 ...> |> delete(:v_r_gt_50)
 ...> |> has_key?(:v_r_gt_50)
 false
fetch!(td, keys)

The fetch! accessor can take one or more keys, returning one or more (List) transform_functions. Unknown keys raise a KeyError.

iex> helper_dictionary_build_test1()
...> |> fetch!(:v_f_gt_0)
...> |> is_function(1)
true

iex> helper_dictionary_build_test1()
...> |> fetch!([:v_f_gt_0, :v_m_plus_42])
...> |> Enum.all?(fn value -> is_function(value, 1) end)
true

The get function takes one or more keys, returning a transform_function or list of transform_functions.

The default must be nil or a transform_function.

iex> helper_dictionary_build_test1()
...> |> get(:v_f_gt_0)
...> |> is_function(1)
true

iex> helper_dictionary_build_test1()
...> |> get(
...>      [:v_f_gt_0, :missing_x, :v_m_plus_42, nil],
...>      nil)
...> |> Enum.all?(fn
...>      value when is_function(value, 1) -> true
...>      value when is_nil(value) -> true
...>      _ -> false
...>    end)
true
has_key?(td, key)
has_key?(transform_dictionary, transform_name) :: boolean

The has_key? function works as expected:

iex> helper_dictionary_build_test1()
...> |> has_key?(:v_r_gt_50)
true

The keys accessor works as expected:

iex> helper_dictionary_build_test1() |> keys
 [:ensure_only_numbers, :kv_f_v_gt_0, :kv_f_v_number, :kv_m_v_plus_42,
  :kv_m_v_squared, :kv_r_v_gt_50, :kv_r_v_lt_45, :v_f_gt_0,
  :v_f_gt_0_m_cubed, :v_f_lt_42_m_squared_and_sum, :v_f_number,
  :v_f_number_gt_0, :v_f_number_gt_0_lt_10,
  :v_f_number_gt_0_m_squared_plus_42,
  :v_f_number_gt_0_m_squared_plus_42_minus_7, :v_m_plus_42,
  :v_m_squared, :v_m_squared_plus_42, :v_r_gt_50, :v_r_lt_45]

The put function supports either a transform_function or a pipeline of discrete transforms.

iex> helper_dictionary_build_test1()
...> |> put(:key_x, fn x -> x end)
...> |> has_key?(:key_x)
true

iex> helper_dictionary_build_test1()
...> |> put(:map_v_cubed, [map: fn x -> x * x * x end])
...> |> get(:map_v_cubed)
...> |> is_function(1)
true
realise(enum, td, transforms \\ [])

realise/3 applies transforms from the tranform dictionary to an enumerable.

It calls transform/2 to apply the transforms.

If the result is a lazy enumerable (e.g. Stream), it is realised (e.g. Enum.to_list/1).

transform(enum, td, transforms \\ [])

transform/3 applies transform functions from the transform dictionary to an enumerable.

The transforms are first converted to a list if necessary, flattened and any nils deleted.

The result may be anything including a lazy enumerable (e.g. Stream).

See the examples at the top.

The values function works as expected:

iex> values = helper_dictionary_build_test1() |> values
 ...> values |> Enum.all?(fn fun -> is_function(fun,1) end)
 true