editor/doc/defnode.md
The g/defnode macro is used for defining new node types: the inputs,
properties and outputs of corresponding nodes, internal dependencies
and meta data about the node type.
Every node instance in the system has a reference to a certain node
type, and this node type is consulted whenever you do g/node-value
on the node, or when for example the output of a node is used as input
to another node we're doing g/node-value on. There are also
introspection functions that allow us for example to query a node if
it has a certain output or in what order its properties should be
displayed.
g/defnode (which should maybe be called g/defnodetype) is
complicated for a number of reasons, but foremost:
g/node-value to be as efficient as possibleThe reloading is handled by an indirection between the node instances
and corresponding node type. The node type of a node instance is not
an assoc'ed immutable value, but rather a reference that when deref'd
will look up the actual "current revision" of the node type in a
global registry of node types. g/defnode will emit code to update
this registry.
The efficiency of g/node-value is acheived by having g/defnode
inspect the dependencies (arguments) of all production functions in
the node definition and generate code at compile time (macro expand
time) for argument collection, error checking, schema validation etc
and finally calling the actual production function. Essentially every
production function will become a def'd function ("behavior
function") in the namespace of the node definition, and g/node-value
eventually dispatches to that. Initial implementations of the graph
system did most of this at runtime by inspecting the node type
information but this was too inefficient.
So we have a couple of concepts now, where are they in the code?
g/defnode is in /src/clj/dynamo/graph.clj (dynamo.graph), but the
actual meat of it - which will be discussed shortly - is in
process-node-type-forms in /src/clj/internal/node.clj (internal.node).
The node types are all NodeTypeImpl records, definition in internal.node
For node instances there are two record types:
NodeImpl for normal nodesOverrideNode for override nodesBoth of these are in internal.node. The concept of overrides will not be
covered here, but we'll touch on some of OverrideNode.
The node type indirection is handled by the record NodeTypeRef in
internal.node. You can see that NodeImpl has a node-type field -
this is actually a NodeTypeRef. derefing a NodeTypeRef will
do a lookup in the node type registry.
The node type registry is a simple (def node-type-registry-ref (atom {})) in internal.node
g/node.value is in dynamo.graph, which eventually dispatches to
the produce-value method on a NodeImpl or OverrideNode.
defnode forms to a map representation, via
internal.node/process-node-type-forms. In this representation we
have user supplied production functions and generated behavior
functions as (fn [...] ...) forms.def's for the functions, and setup code
implementing the node type.Note that when the bundled editor is started, there is no macro
expansion going on - only the emitted def's and setup code remain of
the defnode forms.
In addition to reloading node type definitions, we also want to be
able to reload the definitions of types we mention in the schemas in
the node definition. For this reason, there is actually yet another
"registry" and supporting ref types (value-type-registry-ref,
ValueTypeRef in internal.node). Types created with g/deftype end
up here and support reloading. But we also support using java types
directly. These are not expected to be redefined, but must still
follow the same interface as the g/deftype types. To support this we
emit a list of calls to in/register-value-type, one for each
relevant type mentioned in the schemas.
Next, we emit a list of def's for all the functions we found in the
map representation. The name of these def's match the name we use to
replace the corresponding forms.
Next, I think is an attempt to reduce the size of the finally
generated namespace init class (gui__init.class for the gui
namespace for instance) - we emit a "runtime definer" function that
simply returns the node type map as a literal.
Then we emit a call to internal.node/register-node-type. This
registers a map->NodeTypeImpl'd version of the runtime definer's
return value in the global node type registry. The result of the
registration is a NodeTypeRef which we def using the node type name,
as given in the g/defnode form. This def is what we use whenever we
use the node type name in code.
Finally, we have support for a form of inheritance between node
types. As part of that we emit clojure.core/derive calls matching
the inherits clauses of the node definition.
internal.node/process-node-type-formsinherits clause). The _output-jammers property
is used when marking a node defective. The _overridden-properties
output is used for OverrideNodes.inherits, property, output,
input, display-order). This step translates the former node type
forms into a nested node type map, which will be further
refined. We will also collect any non-deftype types used in
schemas. Note that property clauses automatically create a
corresponding output with the same name. That output can however
be overridden.def'd function name.g/fnk or g/defnk'd symbol) you can also put a
constant, like 1, and this step will automatically wrap it in a
(g/fnk [] 1). So we don't need g/constantly.property's appear, the display-order clauses, and any
inherited node types.(g/fnk [...] ...) forms this is a
matter of parsing the form. For (g/defnk [...] ...)'d symbols,
like produce-scene, we need to resolve the symbol and then check
it's meta :arguments. See internal.util/inputs-needed and
dynamo.graph/fnk for details.dynamic's and
any value clause to the dependencies of the property.
_properties gets transitively dependent on the
dependencies of dynamicsinternal.node/apply-property-dependencies-to-outputs.:cached output _declared-properties (used by the
intrinsic _properties output), that depends on all declared
properties.g/input-dependencies) is a key part of figuring out what
(cached) outputs might have been invalidated by a change to the
graph (see internal.graph/update-graph-successors). The
"lifting" of property dependencies to outputs with the same name
could make this map contain irrelevant entries - we could be
invalidating outputs unnecessarily.g/node-value on an input, property or output. We also generate
special behavior for _declared-properties. We'll discuss behaviors
in detail below.:cascade-delete - every node
connected to these will be deleted together with the owning node.g/declared-property-labels. This is used for instance for
reflection-like initialisation of nodes "for every declared
property label set the property to some value from a map".Intrinsic outputs and properties:
(property _node-id g/NodeID :unjammable)
(property _output-jammers g/KeywordMap :unjammable)
(output _properties g/Properties (g/fnk [_declared-properties] _declared-properties))
(output _overridden-properties g/KeywordMap (g/fnk [_this _basis] (gt/overridden-properties _this _basis)))
"Behavior" roughly means what happens whenever you do
g/node-value. If you look at internal.node/NodeImpl
produce-value you see that a behavior for the label is looked up
in the node's corresponding node type, and then we call the :fn of
this behavior, passing the node instance, the label and evaluation
context as arguments. The process is the same regardless if you ask
for the value of an input, property or output. So what needs to
happen?
For an input we trace arcs backwards to find what is connected to the
input, and ask the source node to produce the output. If it's an
:array input, we do this for all connected arcs. For inputs you can
also specify a :substitute function. The idea is that if a
:substitute function is specified, and the produced value (or any
value in the case of :array) is an error, the :substitute function
gets a chance to repair the result (see
internal.node/node-input-value-function-form). In fact, this
substitution is still commented with a TODO in the code. No
substitution happens if you do g/node-value on an input, but it does
happen if you refer to an input in the argument list of another
production function
For a property, we mentioned that a corresponding output was created
automatically. The production function for this output is essentially
what was supplied as a (value ...) clause. If no such clause was
given, a special case in the generation of output behavior will kick
in. This turns into a get-property on the node instance, which in
the case of NodeImpl is just a map lookup on the instance itself.
For outputs, both automatically generated from properties and
explicitly provided, there is a whole lot going on (see
internal.node/node-output-value-function-form).
mark-defective
on a node, we populate the intrinsic _output-jammers map property
with all the jammable outputs paired with the supplied error
value. If the requested output appears in _output-jammers the
corresponding error value is immediately returned.(value ...) clause, and if so, we do a get-property.:cached outputs being produced
during this node-value call (to be added to the system cache), and
finally a local temp cache with the result of all
non-:cached outputs produced during this node-value. The local
temp cache is there to prevent recalculating the same value several
times.produce-value calls if the argument is another
output on the same node, or an input from an output on another
node. Also there are special considerations for what argument names
mean, this will be described below.g/fnk, g/defnk) of the output,
passing the arguments.:cached, or the local temp
cache otherwise.At relevant points, we do callbacks for tracing (for debugging and/or
progress reporting during node-value) and also check if this
evaluation is a :dry-run - meaning we want to do essentially
everything except calling the actual production functions.
Keep in mind that all of this is purely syntactical. At this point
we're generating forms implementing the wanted behavior
function. These forms become part of the node type map (under the
:behavior key), but there is no actual fn to call until the
g/defnode macro extracts them and emits a list of def's.
The arguments of a production function (property (value ...) clause,
(dynamic ...) clause or explicit output function) refer to inputs,
outputs or properties in the node definition. The declaration order of
these have no impact.
Declaring an explicit output with the same name as a property
(overriding the property) requires special care. If you refer to the
shared name in the argument list of the output production function,
that argument is assumed to refer to the property. For any other
production function, using that name in the argument list will refer
to the output - not the property.
Similarly you can declare an explicit output with the same name as
an input. Here, the name will refer to the input when used in the
output production function, but everywhere else it will refer to the
output.
There is also a special case for properties with (value ...)
clause. The value should produce the current value of the property
(sort of the inverse of the presumed (set ...) clause), but in fact,
any set-property transaction will also store the value via
set-property on the node instance. For this reason, you can in your
(value ...) argument list refer to the same property by name - and
as value you will via get-property get this implicitly set value. So
this will not lead to infinite recursion. This feels a bit sketchy,
the value from (value ...) and this magic property can easily get
out of sync during resource "refactoring" for instance, where the
value clause essentially returns the value of an input
See internal.node/fnk-argument-form for details.
_declared-propertiesFor _declared-properties there is no production function. Instead we
create a custom behavior that collects the property values and
associated dynamics. There is not much magic going on otherwise, see
internal.node/declared-properties-function-form.
In the implementation for the update-property transaction, we need
to produce the current value of the property. In this case, we cannot
use g/node-value mainly for one reason: Any overriding output with
the same name as the property will effectively hide the value of the
actual property. Also, I think we didn't want the semantics of
jamming
To solve this there is a separate set of behaviors for properties,
called "property behaviors". The property behavior essentially either
does the get-property shortcut, or the argument collection, argument
error check and call of the production function (see
internal.node/node-property-value-function-form,
internal.node/collect-property-value-form).
The only place this is invoked is from the update-property
transaction, through internal.node/node-property-value*. This
function will look up the behavior in the :property-behavior map and
call that instead.
Some tips for inspecting what happens during g/defnode and dig
further into this:
node-type-def before and after the call
to util/update-paths.->
pipe in internal.node/process-node-type-formsmacroexpand-1 of very simple node types, or see the examples belowLet's see what happens with an empty node type.
(g/defnode EmptyNodeType)
Even this simple node type declaration creates a flood of
definitions. g/defnode quickly dispatches to
internal.node/process-node-type-forms so first let have a look at
what happens there.
We receive a fully qualified (namespaced) node type symbol "internal.defnode-test/EmptyNodeType" and an empty list of forms.
Our forms are now:
((property _node-id :dynamo.graph/NodeID :unjammable)
(property _output-jammers :dynamo.graph/KeywordMap :unjammable)
(output _properties :dynamo.graph/Properties (dynamo.graph/fnk [_declared-properties] _declared-properties))
(output _overridden-properties :dynamo.graph/KeywordMap (dynamo.graph/fnk [_this _basis] (internal.graph.types/overridden-properties _this _basis))))
The properties _node-id and _output-jammers are :unjammable
meaning if we mark this node defective, we still get sane values if we
do g/node-value for these properties.
{:register-type-info nil, ; we don't refer to any non-g/deftype types in a schema
:property ; Map of all properties
{:_node-id
{:value-type {:k :dynamo.graph/NodeID},
:flags #{:unjammable}, ; :unjammable ends up here. This is the only allowed flag for properties.
:options {}}, ; For inputs, there is also an "option" :substitute that takes a function as a value.
; Both properties and outputs always have an empty `:options` map.
:_output-jammers
{:value-type {:k :dynamo.graph/KeywordMap},
:flags #{:unjammable},
:options {}}},
:property-order-decl [], ; there are no non-intrinsic properties, so the property order list is empty
:output ; Map of all outputs
{:_node-id ; Auto generated output for the _node-id property
{:value-type {:k :dynamo.graph/NodeID},
:flags #{:unjammable}, ; :unjammable also ends up here: this is actually the :unjammable flag that gets respected.
:options {},
:fn :internal.node/default-fn, ; Since this property/output does not have a custom (value ...) clause, there is no need to emit
; a separate production function. We can just use get-property. :internal.node/default-fn is filtered out
; when extracting functions.
:default-fn-label :_node-id}, ; Implementation detail. Need to know the original label when later on extracting function arguments.
:_output-jammers
{:value-type {:k :dynamo.graph/KeywordMap},
:flags #{:unjammable},
:options {},
:fn :internal.node/default-fn,
:default-fn-label :_output-jammers},
:_properties
{:value-type {:k :dynamo.graph/Properties},
:flags #{:explicit}, ; the :explicit flag means this output was not auto generated from a property
:options {},
:fn (dynamo.graph/fnk [_declared-properties] _declared-properties)}, ; Supplied (intrinsic) production function forms
:_overridden-properties
{:value-type {:k :dynamo.graph/KeywordMap},
:flags #{:explicit},
:options {},
:fn (dynamo.graph/fnk [_this _basis] (internal.graph.types/overridden-properties _this _basis))}}}
After this step, nothing really exciting happens until we extract the used argument names and lift the property dependencies.
The relevant difference is that arguments to the fnks have now been
pulled out to :argument and :dependencies sets next to the
:fn. We also have :key, :name, :supertypes, :input and
:display-order-decl entries. The :supertypes, :input and
:display-order-decl comes from the implementation of
inheritance. Slightly sloppy in merge-supertypes, no real problem
though
{:property-display-order (internal.node/merge-display-order nil []),
:key :internal.defnode-test/EmptyNodeType,
:property
{:_node-id
{...
:dependencies #{}},
:_output-jammers
{...
:dependencies #{}}},
...
:name "internal.defnode-test/EmptyNodeType",
:output
{:_node-id
{...
:default-fn-label :_node-id,
:arguments #{:_node-id}, ; <-
:dependencies #{:_node-id}},
:_output-jammers
{...
:default-fn-label :_output-jammers,
:arguments #{:_output-jammers}, ; <-
:dependencies #{:_output-jammers}},
:_properties
{...
:fn (dynamo.graph/fnk [_declared-properties] _declared-properties),
:arguments #{:_declared-properties}, ; <-
:dependencies #{:_declared-properties}},
:_overridden-properties
{...
:fn (dynamo.graph/fnk [_this _basis] (internal.graph.types/overridden-properties _this _basis)),
:arguments #{:_basis :_this}, ; <-
:dependencies #{:_basis :_this}}},
...
:supertypes nil,
:input nil,
:display-order-decl nil}
_declared-properties, collected transitive dependencies {...
:input-dependencies
{:_node-id #{:_node-id},
:_output-jammers #{:_output-jammers},
:_declared-properties #{:_properties},
:_basis #{:_overridden-properties}},
...
:output
{...
:_declared-properties
{:value-type {:k :dynamo.graph/Properties},
:flags #{:cached}, ; _declared-properties is cached
:arguments #{},
:dependencies #{}}} ; note there is no user supplied :fn here, behavior will be added later
}
The code looks a bit wonky with unnecessary do's etc because of how
the different steps that need to be performed are broken up into
different helper functions.
Explanation in code comments.
{...
:behavior
{:_overridden-properties
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
;; Check if the output is jammed: if so, return jam value.
(or (internal.node/output-jammer node node-id label (:basis evaluation-context))
;; Add the current output to the set of "in production" outputs in order to find/prevent circular dependencies
(let [evaluation-context (internal.node/mark-in-production node-id label evaluation-context)]
;; This (intrinsic) output is not :cached. We still check if it has already been produced during this node-value
;; by looking in the temp cache
(if-some [result (internal.node/check-local-temp-cache node-id label evaluation-context)]
;; If it was found, we trace that it was found in the :cache
(internal.node/trace-expr node-id label evaluation-context :cache
(fn [] (if (= result :internal.node/cached-nil) nil result)))
;; Otherwise, we trace that an output is being produced
(internal.node/trace-expr node-id label evaluation-context :output
(fn []
;; Collect the arguments. Looking at the _overridden-properties definition in node-intrinsics,
;; it takes _this (the node) and _basis. All production functions are always passed _node-id and _basis.
(let [arguments {:_basis (:basis evaluation-context),
:_this node,
:_node-id node-id}]
;; The result is either
;; * an aggregated error from the arguments - not likely here :)
;; * nil, if this was a :dry-run (this skips calling the actual production functions)
;; * the result of calling the dollar-name'd definition of the _overridden-properties
;; production function, passing the argument map
(let [result (or (internal.node/argument-error-aggregate node-id label arguments)
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/EmptyNodeType$output$_overridden-properties
arguments)))]
;; Check that the result is either an error value, or conforms to the schema (KeywordMap)
(do (internal.node/schema-check-result node-id label evaluation-context
(schema.core/maybe
(schema.core/conditional internal.graph.error-values/error-value?
internal.graph.error_values.ErrorValue
:else {Keyword Any}))
result)
;; The output is not :cached, but we add it to the local temp cache
(do (internal.node/update-local-temp-cache! node-id label evaluation-context result)
result)))))))))))},
:_output-jammers
{:fn
(fn [node label evaluation-context]
;; Since _output-jammers is an unjammable property, we don't do any jam checking
(let [node-id (internal.graph.types/node-id node)]
;; _output-jammers does not have a custom (value ...) clause, and is traced as
;; :raw-property **Naming? :default-property?**
(internal.node/trace-expr node-id label evaluation-context :raw-property
(fn []
;; Unless this is a :dry-run, call get-property on the node directly. There is no need to
;; generate a separate production function / dollar-name'd def for every simple property.
(when-not (:dry-run evaluation-context)
(internal.graph.types/get-property node (:basis evaluation-context) label))))))},
:_properties
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
;; Check if jammed
(or (internal.node/output-jammer node node-id label (:basis evaluation-context))
;; Add to "in production" outputs
(let [evaluation-context (internal.node/mark-in-production node-id label evaluation-context)]
;; not :cached, but check temp cache
(if-some [result (internal.node/check-local-temp-cache node-id label evaluation-context)]
(internal.node/trace-expr node-id label evaluation-context :cache
(fn [] (if (= result :internal.node/cached-nil) nil result)))
(internal.node/trace-expr node-id label evaluation-context :output
(fn []
;; Collect arguments. Looking at the production function, _declared-properties is an argument.
;; Since this is another output, we effectively recurse and produce it.
(let [arguments {:_declared-properties (internal.graph.types/produce-value node :_declared-properties evaluation-context),
:_node-id node-id,
:_basis (:basis evaluation-context)}]
;; Note! We treat the '_properties' output specially and don't do any argument error checking.
;; If you provide an overriding definition of _properties and refer to something that produces
;; an error, the corresponding argument will be an error value. This is different from all other
;; production functions. Not sure why we do this, or even if we should.
(let [result (when-not (:dry-run evaluation-context)
;; Call the dollar-name'd def of the _properties production function.
(#'internal.defnode-test/EmptyNodeType$output$_properties arguments))]
(do
;; Schema check result
(internal.node/schema-check-result node-id label evaluation-context
(schema.core/maybe
(schema.core/conditional
internal.graph.error-values/error-value?
internal.graph.error_values.ErrorValue
:else
{:properties
{Keyword {:node-id Int,
{:k :validation-problems} Any,
:value Any,
:type Any,
Keyword Any}},
{:k :node-id} Int,
{:k :display-order}
[(conditional
vector?--5399 ; <- Why this strange form?
[(one Str "category") Keyword]
keyword?
Keyword)]}))
result)
(do
;; Not :cached, but add to local temp cache
(internal.node/update-local-temp-cache! node-id label evaluation-context result)
result)))))))))))},
:_node-id
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
;; This implementation is slightly embarrassing since we already have the node-id here.
;; Instead it's being treated as any other :unjammable property with no (value ...) clause.
(internal.node/trace-expr node-id label evaluation-context :raw-property
(fn []
(when-not (:dry-run evaluation-context)
(internal.graph.types/get-property node (:basis evaluation-context) label))))))},
:_declared-properties
{:fn
(fn [node label evaluation-context]
;; _declared-properties gets a custom behavior. Since this sample node type does not have any properties, the value-map is empty.
(let [node-id (internal.graph.types/node-id node)
display-order (internal.node/property-display-order (internal.graph.types/node-type node (:basis evaluation-context)))
value-map {}]
(when-not (:dry-run evaluation-context)
{:properties value-map,
:display-order display-order,
:node-id node-id})))}},
...
:property-behavior ; Here are the custom behaviors used for update-property.
{:_output-jammers
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
(internal.node/trace-expr node-id :_output-jammers evaluation-context :raw-property
(fn []
(when-not (:dry-run evaluation-context)
(internal.graph.types/get-property node (:basis evaluation-context) :_output-jammers))))))},
:_node-id
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
(internal.node/trace-expr node-id :_node-id evaluation-context :raw-property
(fn []
(when-not (:dry-run evaluation-context)
(internal.graph.types/get-property node (:basis evaluation-context) :_node-id))))))}}
...}
g/defnodeAfter internal.node/process-node-type-forms has massaged the
defnode ... clauses into a map, defnode needs to turn it into a
list of def's and setup calls. We'll go through this below.
fn-paths (in/extract-def-fns node-type-def)
This will find all {:fn ...} maps under the [:output <label>],
[:property <label> :dynamics], [:property <label> :value],
[:property <label> :default], [:behavior <label>] and
[:property-behavior <label>]keys. fn-paths is a list of pairs with
the path to the {:fn ...} and the fn forms from the map.
fn-defs (for [[path func] fn-paths]
(list `def (in/dollar-name symb path) func))
Turn the fn-paths into a list of fn def's. For instance, the
behavior clauses for _node-id will turn into:
(def EmptyNodeType$behavior$_node-id
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
(internal.node/trace-expr node-id label evaluation-context :raw-property
(fn []
(when-not (:dry-run evaluation-context)
(internal.graph.types/get-property node (:basis evaluation-context) label)))))))
Next,
node-type-def (util/update-paths node-type-def fn-paths
(fn [path func curr]
(assoc curr :fn (list `var (in/dollar-name symb path)))))
Here, we update the {:fn ...}'s, replacing the function clauses with
a reference to the def'd version. Now the behavior for _node-id in the map is simply:
{:fn #'EmptyNodeType$behavior$_node-id}
Next,
node-key (:key node-type-def)
derivations (for [tref (:supertypes node-type-def)]
`(when-not (contains? (descendants ~(:key (deref tref))) ~node-key)
(derive ~node-key ~(:key (deref tref)))))
node-type-def (update node-type-def :supertypes #(list `quote %))
This translates the inherits clauses to derive's so isa? etc
works as expected.
runtime-definer (symbol (str symb "*"))
This is just a name to be use for the function that returns the final form of the node type map, the "runtime definer". In this case "EmptyNodeType*".
type-regs (for [[key-form value-type-form] (:register-type-info node-type-def)]
`(in/register-value-type ~key-form ~value-type-form))
node-type-def (dissoc node-type-def :register-type-info)]
This generates calls to register the non-deftype'd types we've used,
using register-value-type. We have no other use for this
information so dissoc it.
`(do
~@type-regs
~@fn-defs
(defn ~runtime-definer [] ~node-type-def)
(def ~symb (in/register-node-type ~node-key (in/map->NodeTypeImpl (~runtime-definer))))
~@derivations))
Finally, we emit all these def's and function calls.
Here we'll have a look at the different ways of specifying a production function.
(g/defnk produce-defnk-output [] "defnk output")
(g/defnode ProductionDefs
(output fnk-output g/Str (g/fnk [] "fnk output"))
(output defnk-output g/Str produce-defnk-output)
(output constant-output g/Str "constant output"))
After process-node-type-forms, relevant parts of the node type map looks like this:
{...
:output
{:fnk-output
{:value-type {:k :dynamo.graph/Str},
:flags #{:explicit},
:options {},
:fn (g/fnk [] "fnk output"),
:arguments #{},
:dependencies #{}},
:defnk-output
{:value-type {:k :dynamo.graph/Str},
:flags #{:explicit},
:options {},
:fn produce-defnk-output,
:arguments #{},
:dependencies #{}},
:constant-output
{:value-type {:k :dynamo.graph/Str},
:flags #{:explicit},
:options {},
:fn (dynamo.graph/fnk [] "constant output"),
:arguments #{},
:dependencies #{}},
...}
:behavior
{
:fnk-output
{:fn
(fn [node label evaluation-context]
...
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/ProductionDefs$output$fnk-output arguments))
...)
}
:defnk-output
{:fn
(fn [node label evaluation-context]
...
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/ProductionDefs$output$defnk-output arguments)))]
...)
}
:constant-output
{:fn
(fn [node label evaluation-context]
...
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/ProductionDefs$output$constant-output arguments))
...)
}
...
}
In the :output section we see that the produce-defnk-output
remains, and the "constant output" string has been wrapped in a
fnk. In the behavior section, we see that all behavior functions
contain a call to a dollar-name'd production function.
Checking what g/defnode emits we see:
(def ProductionDefs$output$fnk-output (g/fnk [] "fnk output"))
(def ProductionDefs$output$defnk-output produce-defnk-output)
(def ProductionDefs$output$constant-output (dynamo.graph/fnk [] "constant output"))
Here we demonstrate
(value ...) clauses referring to itself:property-behavior and how it differs from the usual :behaviordynamics and the default value function end up_properties, and property dynamics (g/defnode CustomProperty
(property simple-property g/Str)
(property custom-property g/Str
(default (fn [] "fruit"))
(value (g/fnk [simple-property custom-property]
(str simple-property custom-property)))
(dynamic matches-input (g/fnk [custom-property simple-input]
(= custom-property simple-input))))
(input simple-input g/Str))
(value ...) clauses referring to itselfThe (value ...) clause for :custom-property ends up as an
production function in the :output section:
:custom-property
{:value-type {:k :dynamo.graph/Str},
:flags #{},
:options {},
:fn (g/fnk [simple-property custom-property] (str simple-property custom-property)),
:arguments #{:simple-property :custom-property},
:dependencies #{:simple-property :custom-property}}
And the corresponding behavior in the :behavior section:
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
(or (internal.node/output-jammer node node-id label (:basis evaluation-context))
...
;; Here we see that the "self reference" to custom-property became a direct property access via get-property,
;; instead of a (failing) recursive call to produce-value for the same label.
(let [arguments {:simple-property (internal.graph.types/produce-value node :simple-property evaluation-context),
:custom-property (internal.node/trace-expr node-id :custom-property evaluation-context :raw-property
(fn []
(when-not (:dry-run evaluation-context)
(internal.graph.types/get-property node (:basis evaluation-context) :custom-property)))),
:_node-id node-id,
:_basis (:basis evaluation-context)}]
(let [result (or (internal.node/argument-error-aggregate node-id label arguments)
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/CustomProperty$output$custom-property arguments)))]
...
:property-behavior and how it differs from the usual :behaviorThe :property-behavior for custom-property (used during update-property), looks like this:
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
;; No jam checking, no check for cycles
(internal.node/trace-expr node-id :custom-property evaluation-context :property
(fn []
;; The reference to custom-property will use get-property. Using produce-value might invoke the behavior of an
;; overriding an explicit output, and we can't invoke the property-behaviour recursively.
(let [arguments__11811__auto__ {:simple-property (internal.graph.types/produce-value node :simple-property evaluation-context),
:custom-property (internal.graph.types/get-property node (:basis evaluation-context) :custom-property)}]
(or (internal.node/argument-error-aggregate node-id :custom-property arguments__11811__auto__)
;; **Small wart: if :dry-run, we will call (constantly nil) with the argument.
;; This was a simple way to compose the code generating functions**
((if-not (:dry-run evaluation-context)
;; Note that we call ...$property$custom-property$value instead of ...$output$...
;; This prevents us from calling the production function of an explicit
;; overriding output.
#'internal.defnode-test/CustomProperty$property$custom-property$value (constantly nil)) arguments__11811__auto__)))))))
dynamics and the default value function end upProperty dynamics and the default function end up under the
corresponding property in the :property section of the node type
map.
As you can see, the production function for (value ...) also ends up
here in the :property map. This version will be used for
_properties later on.
:property
{...
:custom-property
{:value-type {:k :dynamo.graph/Str},
:flags #{},
:options {},
:default {:fn (fn* ([] "fruit")), :arguments #{}, :dependencies #{}},
:value
{:fn (g/fnk [simple-property custom-property] (str simple-property custom-property)),
:arguments #{:simple-property :custom-property},
:dependencies #{:simple-property :custom-property}},
:dynamics
{:matches-input
{:fn (g/fnk [custom-property simple-input] (= custom-property simple-input)),
:arguments #{:simple-input :custom-property},
:dependencies #{:simple-input :custom-property}}},
:dependencies #{:simple-property :simple-input}}}
All these fn's will be emitted as def's by defnode and the node
type map updated with the corresponding vars.
The :default function will by called when constructing a new node to
provide values if none were supplied.
_properties, and property dynamicsThe intrinsic version of _properties just passes through
_declared-properties. Looking at the _declared-properties behavior
for this node type:
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)
display-order (internal.node/property-display-order (internal.graph.types/node-type node (:basis evaluation-context)))
value-map {:simple-property {:value (internal.node/trace-expr node-id :simple-property evaluation-context :raw-property
(fn []
(when-not (:dry-run evaluation-context)
;; The simple-property has no custom (value ...), so we just do get-property
(internal.graph.types/get-property node (:basis evaluation-context) :simple-property)))),
:type {:k :dynamo.graph/Str},
:node-id node-id},
:custom-property {:value (internal.node/trace-expr node-id :custom-property evaluation-context :property
(fn []
;; Below gets generated pretty much the same way as the :property-behavior above
(let [arguments__11811__auto__ {:simple-property (internal.graph.types/produce-value node :simple-property evaluation-context),
:custom-property (internal.graph.types/get-property node (:basis evaluation-context) :custom-property)}]
(or (internal.node/argument-error-aggregate node-id :custom-property arguments__11811__auto__)
((if-not (:dry-run evaluation-context)
#'internal.defnode-test/CustomProperty$property$custom-property$value (constantly nil)) arguments__11811__auto__))))),
:type {:k :dynamo.graph/Str},
;; Here comes our `dynamic`
:matches-input (internal.node/trace-expr node-id [:custom-property :matches-input] evaluation-context :dynamic
(fn []
;; Collect arguments for the dynamic. Note that the reference to custom-property will prioritise
;; an overriding explicit output with the same name, since we use produce-value.
(let [arguments__11811__auto__ {:simple-input (internal.node/error-checked-input-value node-id :simple-input (internal.node/pull-first-input-value node :simple-input evaluation-context)),
:custom-property (internal.graph.types/produce-value node :custom-property evaluation-context)}]
(or (internal.node/argument-error-aggregate node-id :matches-input arguments__11811__auto__)
((if-not (:dry-run evaluation-context)
;; The dynamic gets a slightly longer dollar-name :)
#'internal.defnode-test/CustomProperty$property$custom-property$dynamics$matches-input (constantly nil)) arguments__11811__auto__))))),
:node-id node-id}}]
(when-not (:dry-run evaluation-context)
{:properties value-map,
:display-order display-order,
:node-id node-id}))
Lets have a look what happens when you have an output with the same name as a property.
(g/defnode OverriddenProperty
(property data g/Str (value (g/fnk [] 1)))
(input nonsense-input g/Str)
(output data g/Str (g/fnk [data nonsense-input]
(str data nonsense-input))))
In the :output section, only the explicit output production function
remains, but the definition in :property remains.
{:output
{:data
{:value-type {:k :dynamo.graph/Str},
:flags #{:explicit},
:options {},
:fn (g/fnk [data nonsense-input] (str data nonsense-input)),
:arguments #{:nonsense-input :data},
:dependencies #{:nonsense-input :data}}
...
}
...
:property
{...
:data
{:value-type {:k :dynamo.graph/Str},
:flags #{},
:options {},
:value {:fn (g/fnk [] 1), :arguments #{}, :dependencies #{}},
:dependencies #{}}}
}
If you look into the code generated for the data output behavior, you
would find that it calls
#'internal.defnode-test/OverriddenProperty$property$data$value to get the property value.
Same thing for the _declared-properties output and the
:property-behavior for data.
We generate behaviors for inputs also, These are somewhat
broken in that they do not perform error substitution. They are
used in a handful places in the editor (we do (g/node-value ... :some-input)) and are sometimes useful for debugging, but we could
probably do without them.
(g/defnode InputVarieties
(input single-input g/Str)
(input array-input g/Str :array)
(input subst-single-input g/Str :substitute (fn [err] "error!"))
(input subst-array-input g/Str :array :substitute (fn [inputs] (map #(if (g/error? %) "error!" %) inputs))))
The behavior (in :behavior) looks like this:
{:array-input {:fn internal.node/pull-input-values},
:subst-single-input
{:fn (clojure.core/partial internal.node/pull-first-input-with-substitute (fn [err] "error!"))},
:subst-array-input
{:fn (clojure.core/partial internal.node/pull-input-values-with-substitute
(fn [inputs]
(map
(fn* [p1__26210#] (if (g/error? p1__26210#) "error!" p1__26210#))
inputs)))},
:single-input {:fn internal.node/pull-first-input-value}}
The inputs do not need individual production functions - these are the four varieties possible.
Sadly both -with-substitute varieties say "todo - invoke substitute"
If, however we use these inputs as argument to a production function:
(g/defnode InputVarietiesUsed
(input single-input g/Str)
(input array-input g/Str :array)
(input subst-single-input g/Str :substitute (fn [err] "error!"))
(input subst-array-input g/Str :array :substitute (fn [inputs] (map #(if (g/error? %) "error!" %) inputs)))
(output output-single-input g/Str (g/fnk [single-input] single-input))
(output output-array-input g/Str (g/fnk [array-input] array-input))
(output output-subst-single-input g/Str (g/fnk [subst-single-input] subst-single-input))
(output output-subst-array-input g/Str (g/fnk [subst-array-input] subst-array-input)))
The behavior for :output-subst-array-input for instance will, when
collecting arguments, perform the error substitution:
(let [arguments {:subst-array-input (internal.node/error-substituted-array-input-value
(internal.node/pull-input-values node :subst-array-input evaluation-context)
(fn [inputs]
(map (fn* [p1__26365#]
(if (g/error? p1__26365#) "error!" p1__26365#))
inputs))),
:_node-id node-id,
:_basis (:basis evaluation-context)}]
How does the :cached flag affect the behavior function? The
difference is only what caches we use for lookup and update.
(g/defnode CachedOutputs
(property data g/Str)
(output non-cached g/Str (g/fnk [data] data))
(output cached g/Str :cached (g/fnk [data] data)))
{...
:behavior
{...
:cached
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
(or (internal.node/output-jammer node node-id label (:basis evaluation-context))
(let [evaluation-context (internal.node/mark-in-production node-id label evaluation-context)]
;; internal.node/check-caches! will look in two caches:
;; * The "global" copied from the system when creating the evaluation context
;; * The "local" which contains the results of newly produced :cached outputs.
;; These will typically be added to the system cache at the end of the `node-value` call.
;; internal.node/check-caches! will do trace-expr internally
(if-some [[result] (internal.node/check-caches! node-id label evaluation-context)]
result
(internal.node/trace-expr node-id label evaluation-context :output
(fn []
(let [arguments {:data (internal.graph.types/produce-value node :data evaluation-context),
:_node-id node-id,
:_basis (:basis evaluation-context)}]
(let [result (or (internal.node/argument-error-aggregate node-id label arguments)
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/CachedOutputs$output$cached
arguments)))]
(do (internal.node/schema-check-result node-id label evaluation-context
(schema.core/maybe
(schema.core/conditional
internal.graph.error-values/error-value?
internal.graph.error_values.ErrorValue
:else
java.lang.String))
result)
;; Here internal.node/update-local-cache! will, well, add the result to the local cache
(do (internal.node/update-local-cache! node-id label evaluation-context result)
result)))))))))))},
:non-cached
{:fn
(fn [node label evaluation-context]
(let [node-id (internal.graph.types/node-id node)]
(or (internal.node/output-jammer node node-id label (:basis evaluation-context))
(let [evaluation-context (internal.node/mark-in-production node-id label evaluation-context)]
;; Non-:cached outputs will, as we've already seen, only use the local temp cache
(if-some [result (internal.node/check-local-temp-cache node-id label evaluation-context)]
(internal.node/trace-expr node-id label evaluation-context :cache
(fn []
(if (= result :internal.node/cached-nil)
nil
result)))
(internal.node/trace-expr node-id label evaluation-context :output
(fn []
(let [arguments {:data (internal.graph.types/produce-value node :data evaluation-context),
:_node-id node-id,
:_basis (:basis evaluation-context)}]
(let [result (or (internal.node/argument-error-aggregate node-id label arguments)
(when-not (:dry-run evaluation-context)
(#'internal.defnode-test/CachedOutputs$output$non-cached
arguments)))]
(do (internal.node/schema-check-result node-id label evaluation-context
(schema.core/maybe
(schema.core/conditional
internal.graph.error-values/error-value?
internal.graph.error_values.ErrorValue
:else
java.lang.String))
result)
;; And here, we only update the local temp cache
(do (internal.node/update-local-temp-cache! node-id label evaluation-context result)
result)))))))))))}
}
}
When using inherits, be careful not to unintentionally override a
property from the base node type. The semantics of inherits is
almost "merge the node type map of the base node type with the
current" / copy-paste the definition.
(g/defnode BaseNode
(property surprise g/Str
(dynamic dynamic-value (g/fnk [surprise] surprise)))
(output use-surprise g/Str (g/fnk [surprise] surprise)))
(g/defnode DerivedNode
(inherits BaseNode)
(output surprise g/Str (g/fnk [] "DerivedNode/surprise")))
With this definition, the behavior for use-surprise will call
produce-value to get the argument for surprise. So on a
DerivedNode, use-surprise will actually return
"DerivedNode/surprise". The _declared-properties behavior will
correctly call the production function for the property surprise to
get the value, but dynamic-value will use produce-value and get
"DerivedNode/surprise". This might or might not be surprising.
For reference, the derive calls emitted by defnode are:
(when-not (contains? (descendants :internal.defnode-test/BaseNode) :internal.defnode-test/DerivedNode)
(derive :internal.defnode-test/DerivedNode :internal.defnode-test/BaseNode))