Directed acyclic graph computations for Clojure.
You can find the namespace for this example here
(ns quick-example
(:require [bortexz.graphcom :as g]))
(defn latest-n-vals-node
"Node that will emit as value latest up to n numbers received from input"
[input n]
(g/compute-node
{:input input}
(fn [val {:keys [input]}]
(vec (take-last n (conj (or val []) input))))))
(defn sum-values-node
"Node that sums all values received"
[vals-node]
(g/compute-node
{:vals vals-node}
(fn [_ {:keys [vals]}]
(reduce + 0 vals))))
(defn mean-node
"Node that computes the mean of the numbers of numbers-node"
[numbers-node]
(g/compute-node
{:numbers numbers-node
:summed (sum-values-node numbers-node)}
(fn [_ {:keys [numbers summed]}]
(/ summed (count numbers)))))
(let [;; Node to introduce new values into the computation
latest-val (g/input-node)
;; Node that returns latest 2 values introduced from latest-val
latest-2 (latest-n-vals-node latest-val 2)
;; Node that computes the mean of values
mean-node (mean-node latest-2)
;; Only nodes we are interested need to be labeled when creating the graph,
;; either to input a value or to retrieve their value.
;; Intermediary hidden nodes are recursively added without label
graph (g/graph {:latest-val latest-val :mean mean-node})
;; A graph needs to be wrapped in a context to be executed
ctx (g/context graph)]
[(-> ctx
(g/process {:latest-val 3})
(g/process {:latest-val 7})
(g/process {:latest-val 9})
(g/value :mean)) ; => 8
; Immutable context
(-> ctx
(g/process {:latest-val 3})
(g/value :mean))] ; => 3
) ; => [8 3]
A node of the graph. Nodes can be sources (dependencies) of other nodes when building the graph.
There are two types of nodes:
A node without sources, also called root
nodes. Their use is to introduce new values into the graph for downstream computations.
(g/input-node)
A node that has dependencies on other nodes, and computes it's value from its sources. Sources can be other compute-nodes or input-nodes. In the case of input-nodes, the values will be nil if the inputs are not specified as part of the current graph computation.
(g/compute-node {:a-source a-source-node}
(fn [current-value {:keys [a-source] :as _sources-values}]
;; Return new value of this node,
;; a-source contains the current value of a-source-node
))
For nodes to be used, they must be added to a graph. Nodes are added to a graph with a certain label, that can be later used to retrieve their values (compute-nodes) or to introduce new values into the graph (input-nodes).
When adding a node to a graph with a label, all sources are also added recursively, without a label. This allows compute-nodes to create their own internal nodes, when needed.
(def input (g/input-node))
(def compute (g/compute-node {:input1 input1} handler))
(def graph (g/graph {:input input :compute compute}))
;; Same as:
(def graph (-> (g/graph)
(g/add :input input)
(g/add :compute compute)))
;; The graph is immutable, so every step returns the updated graph
To be able to execute a graph, we need a graph processor. Two default processors are offered:
(g/sequential-processor)
(g/parallel-processor) ;; Only CLJ
sequential-processor
Will process a flattened version of the topological sort of the graph sequentially.
parallel-processor
Uses a topological sort where each step consists of nodes that can be executed in parallel (all their sources have already been executed), and executes each step using pmap
.
The context glues the graph with a specific processor, the current values, and processor compilations. A context is also immutable, and functions that change it's value return an updated context.
;; Create context
(g/context graph) ; Uses sequential-processor by default
(g/context graph processor)
;; Process the context with given inputs
(g/process context {:input1 value1 :input2 value2})
;; Get current graph values as a map of {<label> <value>}
(g/values context)
;; Get current value for node node under <label>
;; (faster than (-> (g/values context) label) )
(g/value context label)
;; Adds a compilation to context to process graph when :input1 and :input2 are specified in `process`. If a compilation doesn't exist for the input set when executing `process`, it will be added automatically and stored in the context for future `process` calls.
(g/precompile context #{:input1 :input2})
All nodes implement the g/Node protocol:
(defprotocol Node
(-id [this] "Returns id of node. Must be unique inside a graph.))
Nodes created with g/input-node
or g/compute-node
have an id randomly created.
For input nodes, a protocol marker exists, although no function needs to be implemented. When you process the context or precompile it, the inputs must satisfy Input.
(defprotocol Input)
Compute protocol:
(defprotocol Compute
(-sources [this] "Returns map of {<source-label> <source-node>}")
(-compute [this current-value sources-values] "Returns new value of node"))
A processor implements the g/Processor protocol. This protocol consists of two functions:
(defprotocol Processor
(-compile [this graph inputs])
(-process [this graph compilation values input-map]))
-compile
generates a processor-specific data structure that will allow to traverse the graph, for a certain set of input ids. Usually it will be some kind of topological sort. A -topological-sort
function is offered in case it might be useful for custom Processor implementation.
-process
must return a new map of {<compute-id> <value>}
, that must contain all nodes of the graph (Not all nodes need to be executed all the time, depending on the input set, but -process
must return all values either way. Usually you would merge new executed values to current values). input-map
is a map of {<input-id> <value>}
, and compilation
is the return value of -compile
for the inputs that are going to be propagated through the graph.
Copyright © 2022 Alberto Fernández
Distributed under the Eclipse Public License version 1.0.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close