PhStTransform
A Protocol Implementation for transforming arbitrary Elixir data Structures
Documentation
The PhStTransform
protocol is a way to create dynamic protocols
on the fly. It can transform any Elixir data structure by applying
functions to each specific Elixir data type in the way that makes
sense for that data type.
The transform/3
function takes the data structure and
a map of transformation functions and a depth level. It
then does a depth-first recursion through the structure,
applying the tranformation functions for all
data types found in the data structure.
The transform map has data types as keys and anonymous functions as values. The anonymous functions have the data item and recursion depth list as inputs and can return anything. These maps of types and functions are referred to as potions.
Examples
iex> potion = %{ Atom => fn(atom) -> Atom.to_string(atom) end }
iex> data = %{:a => [a: :a], :b => {:c, :d}, "f" => [:e, :g]}
iex> PhStTransform.transform(data, potion)
%{:a => [a: "a"], :b => {"c", "d"}, "f" => ["e", "g"]}
Note that only the values of any data structure are transformed. If an Atom is used as a key, it is not transformed by the Atom function. If we wanted to tranform the keys of a data structure, that would be done in the function for Keyword or Map, rather than Atom.
Using PhStTransform
The potion map should have Elixir Data types as keys and anonymous functions
of either fn(x)
or fn(x, depth)
arity. You can supply nearly any kind of map
as an argument however, since the PhStTransform.Potion.brew
function will strip
out any invalid values. The valid keys are all of the standard Protocol types:
[Atom, Integer, Float, BitString, Regexp, PID, Function, Reference, Port, Tuple, List, Map]
plus Keyword
and the name of any defined Structs (e.g. Range
).
There is also the special type Any
, this is the default function applied
when there is no function for the type listed in the potion. By default
this is set to the identity function fn(x, _d) -> x end
, but can be overridden
in the initial map.
The depth argument should always be left at the default value when using this protocol. The anonymous functions in the potion map can use the depth list to know which kind of data structure contains the current data type.
For example: Capitalize all strings in the UserName
struct, normalize all other strings.
user_potion = %{ BitString =>
fn(str, depth) -> if(List.first(depth) == UserName , do: String.capitalize(str) , else: String.downcase(str)) end}
PhStTransform.transform(data, user_potion)
There is also an experimental PhStTransform.transmogrify
function that allows the maps
to change the potions as the transformation proceeds. This is still a work in progress.
Limitations
Clearly there are some transformations that would be difficult or impossible
to duplicate in a single potion. The tranformations can be easily composed,
but this has a performance cost in that each tranform
iterates through
the entire data structure and applies a transform function to both the
items in the data structure and the entire data structure itself.
Also, since transforms are implemented as a Protocol, the transforms will be relatively slow during development since Protocols are not consolidated for development compilations. Protocol consolidation will improve the speed in production, but like any general purpose tool, this module emphasizes utility over performance.
This module is intended as a quick and easy interface to the benefits of creating a Protocol. Once performance becomes an issue, it’s straightforward to convert a potion to a customized Protocol implementation that can be tuned for the specific task.
Installation
If available in Hex, the package can be installed as:
Add transform to your list of dependencies in
mix.exs
:def deps do [{:phst_transform, "~> 0.9.0"}] end
Ensure transform is started before your application:
def application do [applications: [:phst_transform]] end