Skip to content

Latest commit

 

History

History
481 lines (348 loc) · 15.4 KB

classes-and-objects.pod

File metadata and controls

481 lines (348 loc) · 15.4 KB

TODO: start with a much simpler bare-bones example!

The following program shows how a dependency handler might look in Perl 6. It showcases custom constructors, private and public attributes, methods and various aspects of signatures. It's not very much code, and yet the result is interesting and, at times, useful.

Starting with class

Perl 6, like many other languages, uses the class keyword to introduce a new class. Anything inside of the block that follows is part of the class definition. You may place arbitrary code there, just as you can with any other block, but classes commonly contain declarations. The example code includes declarations relating to state (attributes, introduced through the has keyword) and behavior (methods, through the method keyword).

Which package?

Declaring a class creates a type object, which by default gets installed into the package (just like a variable declared with our scope). This type object is an "empty instance" of the class. You've already seen these. For example, types such as Int and Str refer to the type object of one of the Perl 6 built-in classes. The example uses the class name Task so that other code can refer to it later, such as to create class instances by calling the new method.

I can has state?

The first three lines inside the class block all declare attributes (called fields or instance storage in other languages). These are storage locations that every instance of a class gets. Just as a my variable can not be accessed from the outside of its declared scope, attributes are not accessible outside of the class. This encapsulation is one of the key principles of object oriented design.

The first declaration specifies instance storage for a callback -- a bit of code to invoke in order to perform the task that an object represents:

A previous paragraph said that all attributes are private to the class. Explain the subtle distinction here.

The & sigil indicates that this attribute represents something invocable. The ! character is a twigil, or secondary sigil. A twigil forms part of the name of the variable. In this case, the ! twigil emphasizes that this attribute is private to the class.

The second declaration also uses the private twigil:

The "subclass" language is slightly wrong, but I don't know if we want to get into allomorphism here. Is there a rephrasing that's more correct?

However, this attribute represents an array of items, so it requires the @ sigil. These items each specify a task that must be completed before the present one can complete. Furthermore, the type declaration on this attribute indicates that the array may only hold instances of the Task class (or some subclass of it).

The third attribute represents the state of completion of a task:

This scalar attribute (with the $ sigil) has a type of Bool. Instead of the ! twigil, this twigil is .. While Perl 6 does enforce encapsulation on attributes, it also saves you from writing accessor methods. Replacing the ! with a . both declares the attribute $!done and an accessor method named done. It's as if you had written:

Note that this is not like declaring a public attribute, as some languages allow; you really get both a private storage location and a method, without having to write the method by hand. You are free instead to write your own accessor method, if at some future point you need to do something more complex than return the value.

Note that using the . twigil has created a method that will provide with readonly access to the attribute. If instead the users of this object should be able to reset a task's completion state (perhaps to perform it again), you can change the attribute declaration:

The is rw trait causes the generated accessor method to return something external code can modify to change the value of the attribute.

Methods

While attributes give objects state, methods give objects behaviors. Ignore the new method temporarily; it's a special type of method. Consider the second method, add-dependency, which adds a new task to this task's dependency list.

In many ways, this looks a lot like a sub declaration. However, there are two important differences. First, declaring this routine as a method adds it to the list of methods for the current class. Thus any instance of the Task class can call this method with the . method call operator. Second, a method places its invocant into the special variable self.

The method itself takes the passed parameter--which must be an instance of the Task class--and pushes it onto the invocant's @!dependencies attribute.

The second method contains the main logic of the dependency handler:

It takes no parameters, working instead with the object's attributes. First, it checks if the task has already completed by checking the $!done attribute. If so, there's nothing to do.

Otherwise, the method performs all of the task's dependencies, using the for construct to iterate over all of the items in the @!dependencies attribute. This iteration places each item--each a Task object--into the topic variable, $_. Using the . method call operator without specifying an explicit invocant uses the current topic as the invocant. Thus the iteration construct calls the .perform() method on every Task object in the @!dependencies attribute of the current invocant.

After all of the dependencies have completed, it's time to perform the current Task's task by invoking the &!callback attribute directly; this is the purpose of the parentheses. Finally, the method sets the $!done attribute to True, so that subsequent invocations of perform on this object (if this Task is a dependency of another Task, for example) will not repeat the task.

Constructors

Perl 6 is rather more liberal than many languages in the area of constructors. A constructor is anything that returns an instance of the class. Furthermore, constructors are ordinary methods. You inherit a default constructor named new from the base class Object, but you are free to override new, as this example does:

What's the * in the bless call?

The biggest difference between constructors in Perl 6 and constructors in languages such as C# and Java is that rather than setting up state on a somehow already magically created object, Perl 6 constructors actually create the object themselves. This easiest way to do this is by calling the bless method, also inherited from Object. The bless method expects a positional parameter--the so-called "candidate"--and a set of named parameters providing the initial values for each attribute.

The example's constructor turns positional arguments into named arguments, so that the class can provide a nice constructor for its users. The first parameter is the callback (the thing to do to execute the task). The rest of the parameters are dependent Task instances. The constructor captures these into the @dependencies slurpy array and passes them as named parameters to bless (note that :$callback uses the name of the variable--minus the sigil--as the name of the parameter).

Consuming our class

After creating a class, you can create instances of the class. Declaring a custom constructor provides a simple way of declaring tasks along with their dependencies. To create a single task with no dependencies, write:

An earlier section explained that declaring the class Task installed a type object had been installed in the namespace. This type object is a kind of "empty instance" of the class, specifically an instance without any state. You can call methods on that instance, as long as they do not try to access any state; new is an example, as it creates a new object rather than modifying or accessing an existing object.

Unfortunately, dinner never magically happens. It has dependent tasks:

Notice how the custom constructor and sensible use of whitespace allows a layout which makes task dependencies clear.

Finally, the perform method call recursively calls the perform method on the various other dependencies in order, giving the output:

making some money
going to the store
buying food
cleaning kitchen
making dinner
eating dinner. NOM!

Exercises

1. The method add-dependency in Task permits the creation of cycles in the dependency graph. That is, if you follow dependencies, you can eventually return to the original Task. Show how to create a graph with cycles and explain why the perform method of a Task whose dependencies contain a cycle would never terminate successfully.

Answer: You can create two tasks, and then "short-circuit" them with add-dependency:

The perform method will never terminate because the first thing the method does is to call all the perform methods of its dependencies. Because $a and $b are dependencies of each other, none of them would ever get around to calling their callbacks. The program will exhaust memory before it ever prints 'A' or 'B'.

2. Is there a way to detect the presence of a cycle during the course of a perform call? Is there a way to prevent cycles from ever forming through add-dependency?

Answer: To detect the presence of a cycle during a perform call, keep track of which Tasks have started; prevent a Task from starting twice before finishing:

It's time to introduce the augment syntax.

Also, !$!done is unnecessarily hard to read if you see ! as a quasi-quoting symbol. It's a $ sandwich.

Another approach is to stop cycles from forming during add-dependency by checking whether there's already a dependency running in the other direction. (This is the only situation in which a cycle can occur.) This requires the addition of a helper method depends-on, which checks whether a task depends on another one, either directly or transitively. Note the use of » and [||] to write succinctly what would otherwise have involved looping over all the dependencies of the Task:

3. How could Task objects execute their dependencies in parallel? (Think especially about how to avoid collisions in "diamond dependencies", where a Task has two different dependencies which in turn have the same dependency.)

Answer: Enabling parallelism is easy; change the line .perform() for @!dependencies; into @!dependencies».perform(). However, there may be race conditions in the case of diamond dependencies, wherein Tasks A starts B and C in parallel, and both start a copy of D, making D run twice. The solution to this is the same as with the cycle-detection in Question 2: introducing an attribute $!started. Note that it's impolite to die if a Task has started but not yet finished, because this time it might be due to parallelism rather than cycles:

It occurs to me that the above solution might not be 100% thread-safe. Is there a good way to guarantee, in Perl 6, that the attribute $!started gets checked-and-set atomically?

TODO: Optiionally talk about multi methods

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 1:

Unknown directive: =head0

Around line 72:

=end for without matching =begin. (Stack: [empty])

Around line 117:

=end for without matching =begin. (Stack: [empty])

Around line 137:

=end for without matching =begin. (Stack: [empty])

Around line 284:

=end for without matching =begin. (Stack: [empty])

Around line 391:

=end for without matching =begin. (Stack: [empty])

Around line 417:

Non-ASCII character seen before =encoding in 'C<»>'. Assuming UTF-8

Around line 475:

'=end' without a target?

Around line 481:

=end authors without matching =begin. (Stack: [empty])