Thursday, November 15, 2018

Puppet 6 type system - Object Inheritance

Puppet 6 type system - Object Inheritance

Puppet 6 Type System - Object Inheritance

Introduction

This is the third posting in the series about the Object data type in the Puppet Type System - Pcore. The first post introduced the Object data type and the history behind Pcore. You probably want to read that first. The second post covers more about how attributes are specified. In this post, I will cover inheritance as well as one feature I forgot in the second post.

Constant Attributes

Yeah, so, I forgot to mention that there is a short form for specifying constants. In the second post I showed that a constant can be defined like this:

attributes => {
    avg_life_expectancy => {
    type => Float,
    kind => constant,
    value => 70.5,
  }
}

Which is a bit much when really all that is needed is the name and the value (since type can be inferred from the value). For that reason there is constants that work the same way as attributes, but the short form expects a value rather than a type.

This means that the above example can be written:

constants => {
    avg_life_expectancy => 70.5
  }

Ok - with that bit out of the way, moving on to the main attraction of this post… Inheritance.

Object Inheritance

The Puppet Type system supports classic OO inheritance such that an object type can inherit the definition of exactly one parent object type (which in turn can inherit, and so on). None of the OO concepts “interface”, “multiple inheritance”, or “extension” exists in Pcore, although an Object definition is in fact an interface from which an implementation is generated or for which an implementation is registered at runtime.

Notably, it is only possible to inherit from data types that are in turn some kind of Object. It is for example not possible to inherit from Integer or String - for those it is possible to instead create a type alias - but it is not possible to create new kinds of integers or strings.

Here are examples of using type aliases:

type PositiveInteger = Integer[0, default]
type Age = PositiveInteger

Specifying Inheritance

To make an Object type inherit another its parent type is
specified. The parent type must be an Object type:

type Vehicle = {
  attributes => {
    manufacturer => String,
  }
}
type Car = {
  parent => Vehicle,
  attributes => {
    reg_nbr => String,
  }
}

Creating Instances

When creating an instance, it is possible to either specify arguments by position or giving them as a hash. This is the same
as when there is no parent type specified, but when there is a parent, all of the parent’s attributes must be given before the attributes of the type. This also applies if there is a longer inheritance chain.

Giving attributes by position - ancestors in order:

notice(Car('Lamborghini', 'XYZ 666'))

Giving attributes as a hash - order does not matter:

notice(Car(
  reg_nbr =>'XYZ 666',
  manufacturer => 'Lamborghini'
))

As you have probably already figured out, using “arguments by
position” is really useful when there is just one or two arguments, but becomes difficult to read and maintain when there are multiple
and attributes. Also, giving arguments in hash form is required when a parent type has optional arguments.

Using ‘override’

A type may override inherited attributes (including constants) and operations (to be described in a separate post). In order to override something there are a number of rules regarding how and with what something can be overridden. These rules are in place to assert that the contract put in place by a parent type is not violated by a child type - for example say we have a Vehicle type with an attribute weight => Integer[0, default], then it would not be good if some child type changed that to be a String since that would mean that not all kinds of vehicle have the same contract!

When specifying override => true for an attribute the attribute it is overriding must exist in a parent. This is a code safety thing as logic could otherwise think it is overriding something but instead ending up adding a new attribute that probably does not get read or used as intended. Likewise, if override is not set for an attribute it is an error if this attribute already exists in a parent. This is also about code safety as the author of a parent type may not be aware of your subtype, and likewise you may not be aware of new attributes added in a later version. In summary, these rules protected code from having potential problems with both accidental and non effective overrides.

When overriding all properties of the attribute must be specified - there is no “merging” of the parent’s specification and the overriding child’s.

The rules are:

  • The given type must be the same or a narrower type than the type specified for the parent attribute. For example if parent specifies attribute as Integer, and override can narrow that to Integer[0, 100] since all values for the child are also valid for the parent.
  • It is allowed to override a non constant attribute (implied required or optional, derived, derived_or_given) with a constant.
  • It is not allowed to override a constant as that would break the contract.
  • It is not allowed to override an attribute that is specified with final => true.

There is a little more to this as attributes and object functions (a.k.a methods) are in the same name space and certain forms of overriding functions with attributes and vice versa are possible, but the rules above apply also in those circumstances.

Using ‘final’

As noted in the section about ‘override’, specifying final => true will remove the possibility to override that attribute. This feature should be used sparingly as it makes it more difficult to reuse types, but it may be warranted for things related to assertions or security - for example you would not want a child type to be able to “lie”.

Matching on Type

With inheritance matching naturally matches on the type and all of the parent types. For example, using Car and Vehicle example:

$a_car = Car(
  reg_nbr => 'ABC 123',
  manufacturer => 'Lamborghini'
)
notice $a_car =~ Car     # notices true
notice $a_car =~ Vehicle # notices true
notice $a_car =~ Object  # notices true

It is currently not possible to match on individual traits/interface - for example this returns false:

notice $a_car =~ Object['attributes' => { 'reg_nbr' => String }]

This does not match even if the value $a_car is a Car and it has an attribute named reg_nbr since it is only matching on the type itself and the object specification used to match creates another (anonymous) subtype of Object.

In the Ruby API it is possible to get the type information (i.e. there is “reflection”), but that is not yet available in the Puppet Language without having to jump through hoops.

Why use Objects

You probably wonder when you should be using objects instead of just plain-old-hashes, they are after all quite similar, especially if you typed the hash as a Struct and gave it an alias.

type MyThing = Struct[ 'reg_nbr' => String, 'manufacturer' => String]

That is pretty much the same as the Car data type we defined earlier. Here is what the differences are:

  • The struct value needs to be type checked every time it is given to a function as the underlying value is just a Hash - it does not know if it matches any particular structs at all.
  • In contrast, an Object is “type checked” when it is created and therefore it is enough to to just check if something is a Car as that implies that the data types for all of its attributes have been checked and found to be ok.
  • There is no inheritance among Hash/Struct - if you want something like Car and Vehicle you have to repeat all of the “parent” struct definitions in a “child” struct. This become a chore and maintenance nightmare if there are many different data types with many common traits. (Some technologies that you may want to manage with Puppet has very rich configuration data for example).
  • Objects support polymorphic behavior - i.e. you can define methods / functions that operate on objects such that different types of objects have different behavior. While you can write functions that operate on structs of certain subtypes of Struct, you cannot select which one to call based on the kind of struct without having prior knowledge of all such structs and having them in a large case expression (or a bunch of if-then-else). More about this in a later blog post.

Summary

This blog post introduced the concept of inheritance between Object based data types in Puppet’s type system (Pcore). Puppet uses a classic Object Oriented single inheritance scheme.

Monday, November 12, 2018

Puppet 6 type system Object attributes

Puppet 6 type system Object attributes

Puppet 6 Type System - More about Object Attributes

Introduction

This is the second posting in the series about the Object data type in the Puppet Type System - Pcore. The first post introduced the Object data type and the history behind Pcore. You probably want to read that first.

In this post I am going to show how attributes of Objects work in more detail.

Recap Defining an Object data type in Puppet

As you may recall from the earlier post - an Object data type can be created like this in Puppet:

type Car = Object[attributes => {
  reg_nbr => String,
  # ...
}]

(And, if done in Ruby, the part between the brackets goes into the interface section of the create_type body (see the first post).)

When defining an Object, the above can be shortened:

# The brackets can be omitted
type Car = Object {attributes => { reg_nbr => String }}
# The type Object can be omitted
type Car = {attributes => { reg_nbr => String }}

Attributes

Attributes are the instance/member variables of instances of an Object data type and they come in different flavours: required, optional, derived (two kinds), constant and they can be marked as being an override or being final - all explained below.

Recap of type creation, creating a new instance, and getting attributes:

# This defines the type
type Car = Object {attributes => { reg_nbr => String }}
# This creates an instance
$a_car = Car('ABC 123')
# This gets that instance's variable/attribute reg_nbr so
# 'ABC 123' will be noticed
notice($a_car.reg_nbr)

Attribute Definition - Short and Long Form

Attribute Name

The name is given as a hash key in both the short and long form.
The attribute’s name is a String and it must be unique within the object among both attributes and operations. This rule extends to its parents attributes and operations as a redeclaration means it is an override of the parent’s definition and it must be marked as such to be accepted. The name must start with a lowercase letter and cannot be qualified (i.e. defined as being in a namespace).

Short Form

You have already seen the short form of attribute definition:

reg_nbr => String

Which is an attribute name to data type mapping. An attribute specified like this is always a regular required attribute. All other kinds of definitions require use of the Long Form.

Long Form

In the long form of attribute declaration the map is from an attribute name to a Hash of attribute options. The equivalence of the short form is this:

reg_nbr => { type => String }

When using the long form there must at least be a definition of type.

Attribute Options

name type meaning
annotations - Advanced: Allows association of annotations - to be explained in a separate blog post
final Boolean A final attribute cannot be overridden in sub types.
kind Enum See “Attribute Kind” below
override Boolean Must be set to true if overriding a parent type attribute or operation, an error is raised if attribute/operation does not exist in a parent type.
type Type An upper cased type reference
value Any A literal default value constrained by kind and type

Note: Inheritance will be covered in a coming blog post and I will explain the importance of final and override then.

Attribute Kind

name meaning
constant The attribute is a constant; cannot be set when creating an instance, must have value specified.
derived The attribute’s value is computed. There must exist a method at runtime to compute the value. The attribute’s value cannot be given when creating an instance.
given_or_derived Same as derived, but the value may be given when an instance is created. Think of this as a computed default value.
reference Advanced: Default is false, and true means that the value is not contained and is thus serialized as a reference to a value that must exist elsewhere (typically in the same serialization). To be explained in another blog post.

Note: derived was covered in the first blog post in this series.

Multi Valued Attributes

Multi valued attributes are simply defined as being of Array/Tuple or Hash/Struct data type where the type parameters are used to constrain the number of allowed values and their data type which can be any type in Pcore.

This is a big win compared to some other modeling technologies where multi valued attributes must be scalars.

type Garage = { attributes => {
  parked_cars => Array[Car]
}}

Union Values are Variants

Since Pcore has a Variant data type; describing that a value must be one out of several possible data types, it is easy to model more complicated data models.

means_of_transportation => Variant[Car, Boat, Bike]

Extra attributes/values

The Object type does not allow “extra attributes” like in some modeling
technologies where it is possible to specify a required set and any named additional extra attributes. With Pcore Object you have to model that
as an Object with a hash attribute where the extra “optional” values go.

Typical Usage

Typically, attributes are either required and can be specified using the short form, or they are optional and can either be specified:

  • in short form using Optional[T] if it is acceptable to have undef as the default value, or…
  • in long form with type and value (if default value should be something other than undef). In this case the type should not be Optional[T] unless you want to be able to explicitly assign undef even if default value is something else.

Here is an example showing different kinds of attributes:

type Person = { attributes => {
  # name is required
  name => String,

  # optional with undef default value
  nick_name => Optional[String],

  # fav color is optional and defaults to 'blue'
  fav_color => {
    type => Enum['blue', 'red', 'green'],
    value => 'blue', 
  }
  # avg_life_expectancy is a constant
  avg_life_expectancy => {
    type => Float,
    kind => constant,
    value => 70.5,    # globally
  }
}}

Summary

In this post I covered the details of specifying Object attributes and the various kinds of attributes “required”, “optional”, “constant”, and “derived”. In the next post I will cover inheritance of Object types.