String Processing with FunctionNode and PipeNode¶
Introduction¶
Simple examples of FunctionNode
and PipeNode
can be provided with string processing functions. While not serving any practical purpose, these examples demonstrate core features. Other usage examples will provide more practical demonstrations.
Importing function-pipe¶
Throughout these examples function-pipe will be imported as follows.
This assumes the function_pipe.py module has been installed in site-packages
or is otherwise available via sys.path
.
FunctionNodes for Function Composition¶
FunctionNodes wrap callables. These callables can be lambdas, functions, or instances of callable classes. We can wrap them directly by calling FunctionNode
or use FunctionNode
as a decorator.
Using lambda
callables for brevity, we can start with a number of simple functions that concatenate a string to an input string.
With or without the FunctionNode
decorator, we can call and compose these in Python with nested calls, such that the return of the inner function is the argument to the outer function.
This approach does not return a new function we can use repeatedly with different inputs. To do so, we can wrap the same nested calls in a lambda
. The initial input is the input provided to the resulting composed function.
While this works, it can be hard to maintain. By using FunctionNodes, we can make this composition more readable through it’s linear >>
or <<
operators.
Both of these operators return a FunctionNode
that, when called, pipes inputs to outputs (>>
: left to right, <<
: left to right). As with the lambda
example above, we can reuse the resulting FunctionNode
with different inputs.
Depending on your perspective, a linear presentation from left to right may not map well to the nested presentation initially given. The <<
operator can be used to process from right to left:
And even though it is ill-advised on grounds of poor readability and unnecessary conceptual complexity, you can do bidirectional composition too:
The FunctionNode
overloads standard binary and unary operators to produce new FunctionNodes
that encapsulate operator operations. Operators can be mixed with composition to create powerful expressions.
We can create multiple FunctionNode expressions and combine them with operators and other compositions. Notice that the initial input “*” is made available to both innermost expressions, p
and q
.
In the preceeding examples the functions took only the value of the predecessor return as their input. Each function thus has only one argument. Functions with additional arguments are much more useful.
As is common in approaches to function composition, we can partial multi-argument functions so as to compose them in a state where they only require the predecessor return as their input.
The FunctionNode
exposes a partial
method that simply calls functools.partial
on the wrapped callable, and returns that new partialed function re-wrapped in a FunctionNode
.
PipeNodes for Extended Function Composition¶
At higher level of complexity, FunctionPipe
can start to become difficult to understand or maintain. The PipeNode
class (a subclass of FunctionNode
) and its associated decorators makes extended function composition practical, readable, and maintainable. Rather than using the >>
or <<
operators used by FunctionNode
, PipeNode
uses only the |
operator to express left-to-right composition.
We will build on the tutorial from earlier (LINK NEEDED), and now explore more complex string processing functions using PipeNode
.
Using the function a
from before, we will instead create it as a PipeNode
, using the pipe_node
decorator.
Recall that PNs that receive fpn.PREDECESSOR_RETURN
must have a preceding PN. In our case, we want an initial PN that receives an initial input from the user. We will do this by positionally binding fpn.PN_INPUT
to the first argument.
Finally, we can generalize string concatenation with a cat
function that, given an arbitrary string, concatenates it to its predecessor return value. Since this function takes an expresion-level argument, we must use the pipe_node_factory
decorator.
Now we can create a pipeline expression that evaluates to a single function f
. In order to evaluate the pipeline, recall we must the __getitem__
syntax with some initial input.
Each node in a PipeNode
expression has access to the fpn.PN_INPUT
. This can be used for many applications. A trivial application below replaces initial input characters found in the predecessor return with characters provided with the expression-level argument chars
.
As already shown, a callable decorated with pipe_node_factory
can take expression-level arguments. With a PipeNode
expression, these arguments can be PipeNode
expressions. The following function interleaves expression-level arguments with those of the predecessor return value.
We can break PipeNode
expressions into pieces by storing and recalling results. This requires that the initial input is a PipeNodeInput
or a subclass. The following PNI
class exposes the __init__
based chars
argument as an instance attribute. Alternative designs for PipeNodeInput
subclasses can provide a range of input data preparation. Since our initial input has changed, we need a new innermost node. The input_init
node defined below simply returns the chars
attribute from the PNI
instance passed as key-word argument fpn.PN_INPUT
.
The function-pipe module provides store
and recall
nodes. The store
node stores a predecessor value. The recall
node returns a stored value as an output later in the expression. A recall
node, for example, can be used as an argument to pipe_node_factory
functions. The call
PipeNode
, also provided in the function-pipe module, will call any number of passed PipeNode
expressions in sequence.
While these string processors do not do anything useful, they demonstrate common approaches in working with FunctionNode
and PipeNode
.
Conclusion¶
After going through this tutorial, you should now have an understanding of:
How to use
fpn.FunctionNode
for function compositionThe directionality of
fpn.FunctionPipe
(i.e.>>
and<<
)How to partial expression-level arguments into
fpn.FunctionPipe
The
fpn.pipe_node
decorator, and when to use itThe
fpn.pipe_node_factory
decorator, and when to use itHow to use
fpn.PipeNode
for function composition
Here is all of the code examples we have seen so far: