You can use iterative types to write efficient iterative functions, or to chain together the iterative functions built into Puppet.
Iterative functions include Iterable
and Iterator
types, as well as other types you can iterate over, such as arrays and hashes. For example, an Array[Integer]
is also an Iterable[Integer]
.
Iterable
and Iterator
types are used internally by Puppet to efficiently chain the results of its built-in iterative functions. You can’t write iterative functions solely in the Puppet language. For help writing less complex functions in Puppet code, see Writing functions in Puppet.Iterable
and Iterator
type design
The Iterable
type represents all things an iterative function can iterate over. Before this type was introduced in Puppet 4.4, if you wanted to design working iterative functions, you'd have to write code that accommodated all relevant types, such as Array
, Hash
, Integer
, and Type[Integer]
.
Signatures of iterative functions accept an Iterable
type argument. This means that you no longer have to design iterative functions to check against every type. This behavior does not affect how the Puppet code that invokes these functions works, but does change the errors you see if you try to iterate over a value that does not have the Iterable
type.
The Iterator
type, which is a subtype of Iterable
, is a special algorithm-based Iterable
not backed by a concrete data type. When asked to produce a value, an Iterator
produces the next value from its input, and then either yields a transformation of this value, or takes its input and yields each value from a formula based on that value. For example, the step
function produces consecutive values but does not need to first produce an array containing all of the values.
Writing iterative functions
When writing iterative functions, use the Iterable
type instead of the more specific, individual types.
The Iterable
type has a type parameter that describes the type that is yielded in each iteration. For example, an Array[Integer]
is also an Iterable[Integer]
.
When writing a function that returns an Iterator
, declare the return type as Iterable
. This is the most flexible way to handle an Iterator
.
For best practices on implementing iterative functions, examine existing iterative functions in Puppet and read the Ruby documentation for the helper classes these functions use. See the implementations of each
and map
for functions that always produce a new result, and reverse_each
and step
for new iterative functions that return an Iterable
when called without a block.
For example, this is the Ruby code for the step
function:
Puppet::Functions.create_function(:step) do
dispatch :step do
param 'Iterable', :iterable
param 'Integer[1]', :step
end
dispatch :step_block do
param 'Iterable', :iterable
param 'Integer[1]', :step
block_param 'Callable[1,1]', :block
end
def step(iterable, step)
# produces an Iterable
Puppet::Pops::Types::Iterable.asserted_iterable(self, iterable).step(step)
end
def step_block(iterable, step, &block)
Puppet::Pops::Types::Iterable.asserted_iterable(self, iterable).step(step, &block)
nil
end
end
Efficiently chaining iterative functions
Iterative functions are often used in chains, where the result of one function is used as the next function’s parameter. A typical example is a map
/reduce
function, where values are first modified, and then an aggregate value is computed.
For example, this use of reverse_each
and reduce
:
[1,2,3].reverse_each.reduce |$result, $x| { $result - $x }
The reverse_each
function iterates over the Array
to reverse the order of its values from [1,2,3]
to [3,2,1]
. The reduce
function iterates over the Array
, subtracting each value from the previous value. The $result
is 0
, because 3 - 2 - 1 = 0.
Iterable types allow functions like these to execute more efficiently in a chain of calls, because they eliminate each function’s need to create an intermediate copy of the mapped values in the appropriate type.
In the above example, the mapped values would be the array [3,2,1]
produced by the reverse_each
function. The first time the reduce
function is called, it receives the values 3
and 2
— the value 1
has not yet been computed. In the next iteration, reduce
receives the value 1
, and the chain ends because there are no more values in the array.
Limitations and workarounds
When you use it last in a chain, you can assign a value of Iterator[T]
(where T
is a data type) to a variable and pass it on. However, you cannot assign an Iterator
to a parameter value. It's also not possible to call legacy 3.x functions with an Iterator
.
If you assign an Iterator
to a resource attribute, you get an error. This is because the Iterator
type is a special algorithm-based Iterable
that is not backed by a concrete data type. In addition, parameters in resources are serialized, and Puppet cannot serialize a temporary algorithmic result.
For example, if you used the following Puppet code:
notify { 'example1':
message => [1,2,3].reverse_each,
}
You would recieve the following error:
Error while evaluating a '=>' expression, Use of an Iterator is not supported here
Puppet needs a concrete data type for serialization, but the result of [1,2,3].reverse_each
is only a temporary Iterator
value.
To convert the Iterator
-typed value to an Array
, map the value.
This example results in an array by chaining the map
function:
notify { 'mapped_iterator':
message => [1,2,3].reverse_each.map |$x| { $x },
}
You can also use the splat operator *
to convert the value into an array.notify { 'mapped_iterator':
message => *[1,2,3].reverse_each,
}
Both of these examples result in a notice containing [3,2,1]
.
If you use *
in a context where it also unfolds, the result is the same as unfolding an array: each value of the array becomes a separate value, which results in separate arguments in a function call.Related topics: step
functions, each
functions, reduce
functions, map
functions, reverse_each
functions.