Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added unknown docs #6839

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelogs/unreleased/6056-docs-unknowns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: Added documentation about unknowns
change-type: patch
sections:
minor-improvement: "{{description}}"
issue-nr: 6056
destination-branches:
- master
- iso6
4 changes: 4 additions & 0 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ Glossary
a cloud provider will not be known upfront. Inmanta marks this parameters as **unknown**.
The state of any resource that uses such an unknown parameter becomes undefined.

For more context, see
:ref:`how unknowns propagate through the configuration model <language_unknowns>` and
:ref:`how the exporter deals with them <model_export_format>`.

entity
Concepts in the infrastructure are modelled in the configuration with entities. An entity
defines a new type in the configuration model. See :ref:`lang-entity`.
Expand Down
73 changes: 73 additions & 0 deletions docs/language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -923,3 +923,76 @@ Plug-ins
For more complex operations, python plugins can be used. Plugins are exposed in the Inmanta language as function calls, such as the template function call. A template
accepts parameters and returns a value that it computed out of the variables. Each module that is included can also provide plug-ins. These plug-ins are accessible within the namespace of the
module. The :ref:`module-plugins` section of the module guide provides more details about how to write a plugin.


.. _language_unknowns:
Unknowns
========

Wherever the configuration model interacts with the outside world (e.g. to fetch external values) :term:`unknown` values
may be present. These unknowns are propagated through the model to finally end up in the resources that require these unknown
sanderr marked this conversation as resolved.
Show resolved Hide resolved
values. This section describes how unknown values flow through the model, and perhaps equally importantly, where they do not
flow at all.

.. note::
Unknowns are a subtle concept. Luckily, for the majority of model development you don't really need to take them into
account. However, for some advanced scenarios it may be important to know how and where they may occur.

For the most part, unknowns are simply propagated along the data flow: statements like assignment statements, constructors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would first give the simple explanation

Suggested change
For the most part, unknowns are simply propagated along the data flow: statements like assignment statements, constructors
For the most part, unknowns are simply propagated along the data flow: any value derived from an unkown in any way becomes an unknown as well. More specifically: statements like assignment statements, constructors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, but I'm going to move it one sentence later, because it really applies to the "any expression that cannot produce ..." part. e.g. [1, unknown, 3] does not become unknown.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an attempt for a middle ground, to give some more context upfront, but in a way that applies for both statements that follow.

and lists simply include the unknown in their result like they would any other value. Any expression that can not produce
a definite result without knowing the value, will return another unknown. And finally, statements that expand the model
with new blocks based on some value, like the if statement and the for loop, simply do not expand the model with their
respective blocks for unknowns.

The model below presents some examples of how an unknown propagates.

.. code-block:: inmanta

# std::env returns an unknown if the environment variable is not (yet) set
my_unknown = std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST")

a = my_unknown # a is unknown
b = [1, 2, my_unknown, 3] # b is a list with 1 unknown element
c = my_unknown is defined # we can not know if c is null, so c is also unknown
d = true or my_unknown # trivial, value of my_unknown is irrelevant -> d is true
sanderr marked this conversation as resolved.
Show resolved Hide resolved
e = my_unknown or true # lazy boolean operator can not compute result without knowing the value -> e is unknown
f = (e == my_unknown) # both e and my_unknown are unknown but they aren't necessarily the same value -> f is unknown

if my_unknown:
# this block is never executed
else:
# neither is this one
end

for x in my_unknown:
# neither is this one
end

for x in [1, 2, my_unknown]:
# this block is executed twice: x=1 and x=2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my opinion on this is known. I would like to still have this power, but I leave it to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By now the behavior is no longer under discussion, it's simply about documenting it. I'll double check this now.

end

g = my_unknown ? true : false # condition is unknown -> neither branch is executed, result is unknown
h = [1 for x in [1, 2, my_unknown]] # the expression `1` is executed once with x=1 and once with x=2. Unknown is propagated as is -> h = [1, 1, unknown]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find these two to be hard to reconcile with the for loop. For me, a list comprehension is

result = [a for item in c if d]

with a and d expressions referring to item and c an expression

collector = ...
for item in c:
  if d:
    collector.collection += a
result = collector.collection

but again, I raised this before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing a constructor call here is quite common I think. The 1 is just to keep the model simple.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update this to construct a simple entity.

i = [1 for x in [1, 2, my_unknown] if not std::is_unknown(x)] # the unknown is filtered out -> i = [1, 1]

Now that we've covered how unknowns flow through the model, we can discuss what an unknown value actually means. In most cases
it simply represents an unknown value. But because of the propagation semantics outlined above, if it happens to occur in a
list, it may in fact represent any number of values: not only the value is unknown, also its size.

For example, consider a list comprehension that filters a list on some condition. If the list contains an unknown, the compiler
can not know if the filter applies so it will propagate the unknown to the result. When the unknown eventually becomes known,
it might remain in the result, or it might be filtered out, depending on whether it matches the condition.

.. code-block:: inmanta

my_unknown = std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST")
my_unknown2 = std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST2")

l = [1, my_unknown, 3, my_unknown2, 5]
a = [x for x in l if x > 2] # l = [unknown, 3, unknown, 5]

# an unknown can even represent more than one unknown value
edvgui marked this conversation as resolved.
Show resolved Hide resolved
b = my_unknown == 0 ? [1, 2] : [3, 4] # b = unknown -> when it becomes known it will be either [1, 2] or [3, 4]

c = std::len(l) # c = unknown (l contains unknowns, so its length is also unknown)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also remark the risk of having unknowns in conditions

implementation things_are_on for A:
   self.b.is_on = self
end

implementation things_are_off for  A:
     self.b.is_off = self
end

implement A using things_are_on if self.on
implement A using things_are_off if self.off


collector = B()
all = [
 A(b=b, on=True)
  A(b=b, on=False)
   A(b=b, on=unknown())
 ]

std::len(b.is_on) # 1  
std::len(b.is_off) # 1
# the fact that the length of b.is_on and b.is_off are unknown is lost
# the intuitive notion that all things are the things that are off + the things tat are on no longer holds 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize it's been a very long time. But do you remember what you were getting at here? Two notes:

  1. the model had some gaps. I reworked it in the following format that does compile
entity A:
    bool on
end

implementation things_are_on for A:
    self.b.is_on = self
end

implementation things_are_off for A:
    self.b.is_off = self
end

implement A using things_are_on when self.on
implement A using things_are_off when not self.on

entity B:
end
implement B using std::none
A.b [1] -- B
B.is_on [0:] -- A
B.is_off [0:] -- A


b = B()
all = [
    A(b=b, on=true),
    A(b=b, on=false),
    A(b=b, on=std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST")),
]


on_len = std::len(b.is_on)
off_len = std::len(b.is_off)
on_len_txt = std::is_unknown(on_len) ? "unknown" : on_len
off_len_txt = std::is_unknown(off_len) ? "unknown" : off_len
std::print(f"b.is_on = {b.is_on}")
std::print(f"b.is_off = {b.is_off}")
std::print(f"std::len(b.is_on) = {on_len_txt}")  # 2
std::print(f"std::len(b.is_off) = {off_len_txt}")  # 2

# THE BELOW APPEARS TO BE INCORRECT
std::len(b.is_on) # 1
std::len(b.is_off) # 1
# the fact that the length of b.is_on and b.is_off are unknown is lost
# the intuitive notion that all things are the things that are off + the things tat are on no longer holds
  1. This behavior suprises me a lot. It looks like when treats unknowns differently than if does. I'm inclined to conclude that that's a bug for when, rather than a cautionary note for the model developer.

21 changes: 10 additions & 11 deletions docs/platform_developers/modelexportformat.rst
Original file line number Diff line number Diff line change
@@ -1,49 +1,48 @@
.. _model_export_format:
Model Export Format
========================




#. top level is a dict with one entry for each instance in the model
#. the key in this dict is the object reference handle
#. the value is the serialized instance
#. the serialized instance is a dict with three fields: type, attributes and relation.
#. type is the fully qualified name of the type
#. attributes is a dict, with as keys the names of the attributes and as values a dict with one entry.
#. An attribute can have one or more of tree keys: unknows, nones and values. The "values" entry has as value a list with the attribute values.
If any of the values is Unknown or None, it is removed from the values array and the index at which it was removed is recorded in respective the unknowns or nones value
#. relations is like attributes, but the list of values contains the reference handles to which this relations points
#. An attribute can have one or more of tree keys: unknows, nones and values. The "values" entry has as value a list with the attribute values.
If any of the values is :term:`unknown` or None, it is removed from the values array and the index at which it was removed is recorded in respective the unknowns or nones value
#. relations is like attributes, but the list of values contains the reference handles to which this relations points

Basic structure as pseudo jinja template
Basic structure as pseudo jinja template

.. code-block:: js+jinja

{
{% for instance in instances %}
'{{instance.handle}}':{
"type":"{{instance.type.fqn}}",
"attributes":[
"attributes":[
{% for attribute in instance.attributes %}
"{{attribute.name}}": [ {{ attribute.values | join(",") }} ]
{% endfor %}
]
"relations" : [
{% for relation in instance.relations %}
"{{relation.name}}": [
"{{relation.name}}": [
{% for value in relation.values %}
{{value.handle}}
{% endfor %}
]
{% endfor %}
]

{% endif %}
}
}

Type Export Format
========================

.. automodule:: inmanta.model
:members:
:private-members: