Ruby function signatures

Functions can specify how many arguments they expect, and a data type for each argument. The rule set for a function’s arguments is called a signature.

Because Puppet functions support more advanced argument checking than Ruby does, the Ruby functions API uses a lightweight domain-specific language (DSL) to specify signatures.

Ruby functions can have multiple signatures. Using multiple signatures is an easy way to have a function behave differently when passed by different types or quantities of arguments. Instead of writing complex logic to decide what to do, you can write separate implementations and let Puppet select the correct signature.

If a function has multiple signatures, Puppet uses its data type system to check each signature in order, comparing the allowed arguments to the arguments that were actually passed. As soon as Puppet finds a signature that can accept the provided arguments, it calls the associated implementation method, passing the arguments to that method. When the method finishes running and returns a value, Puppet uses that as the function’s return value. If none of the function’s signatures match the provided arguments, Puppet fails compilation and logs an error message describing the mismatch between the provided and expected arguments.

Conversion of Puppet and Ruby data types

When function arguments are passed to a Ruby method, they’re converted to Ruby objects. Similarly, when the Puppet manifest regains control, it converts the method’s return value into a Puppet data type.

Puppet converts data types between the Puppet language and Ruby as follows:
Puppet Ruby
Boolean Boolean
Undef NilClass (value nil)
String String
Number subtype of Numeric
Array Array
Hash Hash
Default Symbol (value :default)
Regexp Regexp
Resource reference Puppet::Pops::Types::PResourceType, or Puppet::Pops::Types::PHostClassType
Lambda (code block) Puppet::Pops::Evaluator::Closure
Data type (Type) A type class under Puppet::Pops::Types. For example, Puppet::Pops::Types::PIntegerType
Tip: When writing iterative functions, use iterative types instead of Puppet types.

Writing signatures with dispatch

To write a signature, use the dispatch method.

The dispatch method takes:

  • The name of an implementation method, provided as a Ruby symbol. The corresponding method must be defined somewhere in the create_function block, usually after all the signatures.

  • A block of code which only contains calls to the parameter and return methods.

# A signature that takes a single string argument
  dispatch :camelcase do
    param 'String', :input_string
    return_type 'String' # optional
  end

Using parameter methods

In the code block of a dispatch statement, you can specify arguments with special parameter methods. All of these methods take two arguments:

  • The allowed data type for the argument, as a string. Types are specified using Puppet’s data type syntax.

  • A user-facing name for the argument, as a symbol. This name is only used in documentation and error messages; it doesn’t have to match the argument names in the implementation method.

The order in which you call these methods is important: the function’s first argument goes first, followed by the second, and so on. The following parameter methods are available:

Model name Description
param or required_param

A mandatory argument. You can use any number of these.

Position: All mandatory arguments must come first.

optional_param

An argument that can be omitted. You can use any number of these. When there are multiple optional arguments, users can only pass latter ones if they also provide values for the prior ones. This also applies to repeated arguments.

Position: Must come after any required arguments.

repeated_param or optional_repeated_param

A repeatable argument, which can receive zero or more values. A signature can only use one repeatable argument.

Position: Must come after any non-repeating arguments.

required_repeated_param

A repeatable argument, which must receive one or more values. A signature can only use one repeatable argument.

Position: Must come after any non-repeating arguments.

block_param or required_block_param

A mandatory lambda (block of Puppet code). A signature can only use one block.

Position: Must come after all other arguments.

optional_block_param

An optional lambda. A signature can only use one block.

Position: Must come after all other arguments.

When specifying a repeatable argument, note that:

  • In your implementation method, the repeatable argument appears as an array, which contains all the provided values that weren’t assigned to earlier, non-repeatable arguments.

  • The specified data type is matched against each value for the repeatable argument, not the repeatable argument as a whole. For example, if you want to accept any number of numbers, specify repeated_param 'Numeric', :values_to_average, not repeated_param 'Array[Numeric]', :values_to_average.

For lambdas, note that:

  • The data type for a block argument is Callable, or a Variant that only contains Callables.

  • The Callable type can optionally specify the type and quantity of parameters that the lambda accepts. For example, Callable[String, String] matches any lambda that can be called with a pair of strings.

Matching arguments with implementation methods

The implementation method that corresponds to a signature must be able to accept any combination of arguments that the signature might allow.

If the signature has optional arguments, the corresponding method arguments need default values. Otherwise, the function fails if the arguments are omitted. For example:
dispatch :epp do
  required_param 'String', :template_file
  optional_param 'Hash', :parameters_hash
end

def epp(template_file, parameters_hash = {})
  # Note that parameters_hash defaults to an empty hash.
end
If the signature has a repeatable argument, the method must use a splat parameter (*args) as its final argument. For example:
dispatch :average do
  required_repeated_param 'Numeric', :values_to_average
end

def average(*values)
  # Inside the method, the `values` variable is an array of numbers.
end

Using the return_type method

After specifying a signature’s arguments, you can use the return_type method to specify the data type of its return value. This method takes one argument: a Puppet data type, specified as a string.

dispatch :camelcase do
  param 'String', :input_string
  return_type 'String'
end
The return type serves two purposes: documentation, and insurance.
  • Puppet Strings can include information about the return value of a function.

  • If something goes wrong and your function returns the wrong type (like nil when a string is expected), it fails early with an informative error instead of allowing compilation to continue with an incorrect value.

Specifying aliases using local_types

If you're using complicated abstract data types to validate arguments, and you're using these data types in multiple signatures, they can become difficult to work with and maintain. In these cases, you can specify short aliases for your complex data types and use the aliases in your signatures.

To specify aliases, use the local_types method:

  • You must call local_types only one time, before any signatures.

  • The local_types method takes a lambda, which only contains calls to the type method.

  • The type method takes a single string argument, in the form '<NAME> = <TYPE>'.

    • Capitalize the name, camel case word (PartColor), similar to a Ruby class name or the existing Puppet data types.

    • The type is a valid Puppet data type.

Example:
local_types do
  type 'PartColor = Enum[blue, red, green, mauve, teal, white, pine]'
  type 'Part = Enum[cubicle_wall, chair, wall, desk, carpet]'
  type 'PartToColorMap = Hash[Part, PartColor]'
end

dispatch :define_colors do
  param 'PartToColorMap', :part_color_map
end

def define_colors(part_color_map)
  # etc
end

Using automatic signatures

If your function only needs one signature, and you’re willing to skip the API’s data type checking, you can use an automatic signature. Be aware that there are some drawbacks to using automatic signatures.

Although functions with automatic signatures are simpler to write, they give worse error messages when called incorrectly. You'll get a useful error if you call the function with the wrong number of arguments, but if you give the wrong type of argument, you’ll get something unhelpful. For example, if you pass the function above a number instead of a string, it reports Error: Evaluation Error: Error while evaluating a Function Call, undefined method 'split' for 5:Fixnum at /Users/nick/Desktop/test2.pp:7:8 on node magpie.lan.

If it's possible that your function will be used by anyone other than yourself, support your users by writing a signature with dispatch.

To use an automatic signature:

  • Do not write a dispatch block.

  • Define one implementation method whose name matches the final namespace segment of the function’s name.

Puppet::Functions.create_function(:'stdlib::camelcase') do
  def camelcase(str)
    str.split('_').map{|e| e.capitalize}.join
  end
end

In this case, because the last segment of stdlib::camelcase is camelcase, we must define a method named camelcase.

Related topics: Ruby symbols, Abstract data types.