Introduction to Objects
Programmers Version

Damien M. Jones

"Help lead the revolution..."

It's my hope that you've already read the "Users Version" of this introduction. I want you to be tantalized by the possibilities the new object compiler in UF5 presents so that you'll be motivated to write classes for it. The full benefit of the object compiler really comes into play when everyone is participating, so this text is part tutorial and part evangelism.

Evangelism is necessary because learning to write object-oriented code requires a bit of work. If you are a professional programmer you are likely already familiar with object-oriented programming; you can read the reference material in the Ultra Fractal help file and be well on your way without my help. This guide is not for you. But if--like most fractal programmers and formula authors--you are a hobbyist programmer with little or no professional training or experience, you may find concepts like classes, objects, inheritance, overriding, patterns, members, methods, visibility, and scope rather foreign.

I cannot, in the space of a few short paragraphs, turn you into a professional, object-oriented programming (OOP)-wielding code ninja. To get that far will require practice. I can, however, give you a good start on becoming familiar with UF5's OOP features so that you can begin exploring on your own.

If you get stuck on a concept, you should stop and ask questions. Each of these OOP concepts builds on the others. Skipping ahead before you're ready may leave you frustrated. Save yourself some trouble and ask questions. The best place to ask questions is the UF Programmers discussion list, which you can join by sending email here:

  uf-programmers-subscribe@lists.fractalus.com

CONCEPTS: FUNCTIONS AND SCOPE

With UF4, your formula file is a big block. Any variables you set up in one place are readable everywhere else. UF knows when to call the global:, init:, loop:, final:, or bailout: sections automatically. Within each section, your code runs from top to bottom in a simple fashion.

UF5 allows you to define functions. Functions are simple blocks of code that can be referenced repeatedly with a convenient "short-hand" notation. For example, suppose you have a block of code that looks like this:

  ; convert z2 to polar coordinates
  a = atan2(z2)     ; get angle of z2, -pi < a < pi
  if (a < 0)        ; -pi < a < 0
    a = a + 2*#pi   ; use positive version of this angle
  endif             ; now, 0 <= a < 2pi
  d = cabs(z2)      ; length of z2
  z2 = a + flip(d)  ; z2 is now in polar coordinates!

This (arguably useful) block of code changes the value of z2 from rectangular coordinates to polar coordinates. It's not very long, but if it's used more than once, it has to be copied for each use. And, if another variable (besides z2) is to be converted, it must be modified as well as copied, leaving opportunities for mistakes to be made.

Instead, we define a function that includes this code:

  complex func RectangularToPolar(const complex z2)
    float a
    float d
    ; convert z2 to polar coordinates
    a = atan2(z2)       ; get angle of z2, -pi < a < pi
    if (a < 0)          ; -pi < a < 0
      a = a + 2*#pi     ; use positive version of this angle
    endif               ; now, 0 <= a < 2pi
    d = cabs(z2)        ; length of z2
    return a + flip(d)  ; result is z2 in polar coordinates!
  endfunc

And then in our code where we originally had the block, we replace it with this:

  z2 = RectangularToPolar(z2)

We can call our new function again and again, and we only have to define the function once. If we find a bug in our function, we only have to fix one place.

Let's look at the function definition more closely. First, we tell UF what type of value the function will return. In this case, it's a complex number. Then, we tell it what kinds of parameters the function needs to do its job. These are different from the parameters users set within UF; these are internal function parameters in our code. In this case, we need a complex number we'll call z2. The 'const' keyword tells UF that we're not going to change the value of z2 inside the function; this lets UF operate a bit more quickly and also helps catch programming mistakes (if we change z2 when we didn't mean to).

Immediately after the function declaration, we declare two variables, a and d. When you declare variables inside a function, UF treats them differently than when you declare them in a formula. Formula variables are visible everywhere, but function variables only have meaning inside the function itself. We call these "local" variables. "Scope" is just a fancy word that describes where variables have meaning. a and d have "local" scope.

Note that at the end of the function, we don't save the result in z2. We told UF we weren't going to modify z2 when we declared it 'const'. But we also told UF that our function would return a complex number. That is what the return statement does--it returns a value.

I want to point out here that the z2 used inside the function is ALSO a local variable, and it's not the same as the z2 in the formula code. Suppose we declared our function this way:

  complex func RectangularToPolar(complex z2)
    float a
    float d
    ; convert z2 to polar coordinates
    a = atan2(z2)       ; get angle of z2, -pi < a < pi
    if (a < 0)          ; -pi < a < 0
      a = a + 2*#pi     ; use positive version of this angle
    endif               ; now, 0 <= a < 2pi
    d = cabs(z2)        ; length of z2
    z2 = a + flip(d)    ; z2 is now in polar coordinates!
    return z2
  endfunc

The only difference is that z2 is not declared const (so we can change it) and we store the result in z2 and THEN return it. This isn't wrong, but it's a bit less efficient. But it will illustrate an important point. If we call our function this way:

  z2 = (4,4)
  polarz2 = RectangularToPolar(z2)
  ; z2 is still (4,4)

Notice now we're not storing the result back into z2. If we look at this code without looking at the function definition, it doesn't look like z2 will be modified. And in fact, it won't be. But the function definition changes z2! What gives?

The answer is that the z2 inside the function is a copy. Inside the function, wherever z2 appears, it means the z2, the local variable. Outside the function, z2 means z2, the global variable (the one that was declared by the formula, not the function).

This applies to more than just the formula parameters, it applies to any of the local variables. Suppose we had code like this:

  float a = real(z2)
  polarz2 = RectangularToPolar(z2)
  ; a is still real(z2)

This works because inside the function, a refers to the local variable a, not the global variable a. This is what local variables mean.

The power of local variables is that you can write code without worrying about side-effects. You use local variables inside your functions, and you never have to worry about changing a variable outside your function by accident.

Local variables (and scope in general) is very important to help you understand members and methods later on.

CONCEPTS: CLASSES AND OBJECTS

As I indicated in the "Users Version", a class is a type of formula. With UF4 we only had very big blocks, but with UF5 we can make much smaller formula pieces. Imagine you are building a house. One method (UF4) requires you to use pre-fabricated walls. It is easy to put four walls together to make a house, but if you want a window in the wall, you must use a wall that contains a window. If you want a window AND a door, you have to find a wall that contains those. You can't get them separately.

UF5 is more like having bricks. You can build the wall however you like, and you can leave a space for a window to fit. And you can use anyone's window, as long as it's been built to fit into the standard window slot. Likewise for a door; you can leave a slot for a door, and put any standard door in it. In fact, you might even--rather than using the bricks yourself--go get a pre-fabricated wall that contains slots for windows and doors. Your wall is pre-built, but you still can use any standard door or window (you don't have to get them bundled together).

A class is the type of thing (window, door, wall). An object is the specific thing, with the parameters that control it (the actual window for my front wall, my actual back door). To a user these differences do not matter, but for a programmer they are more important.

In UF, you write code that defines classes. You write code that uses objects. A class is a definition, a description of a type. An object is the thing itself (we call this an "instance"). So we describe a window: how big it is, how thick the frame can be, where we can safely attach it to the building frame. That is the class. When we use a window, we are creating and using an object--the specific instance of the class (type).

CONCEPTS: METHODS AND MEMBERS

Each defined class has three types of things associated with it: data (variables), functions, and user-selectable parameters. A "method" is a function that is associated with a class. It may also be called a "member function". Variables associated with a class are called "member variables" and member functions and member variables together are called "members".

When you define a class, you list the member variables, methods, and user-selectable parameters that make up the class. But none of those have any value until you actually create an object of that class.

For our example, we will use this example class. It is not terribly useful, but it is easy to understand.

class CircleTester {
public:
  float lastdistance

  func CircleTester()
    lastdistance = 0
  endfunc
  
  bool func IsInside(const complex p)
    lastdistance = cabs(p - @center)
    if (lastdistance < @radius)
      return true
    else
      return false
    endif
  endfunc
  
default:
  title = "Circle Tester"
  
  complex param center
    caption = "Circle Center"
    default = (0,0)
  endparam
  float param radius
    caption = "Circle Radius"
    default = 1
  endparam
}

This class has a member variable 'lastdistance'. It has two methods, one called 'CircleTester' and one called 'IsInside'. (The CircleTester method is the same name as the class; this is a special method called a 'constructor' that we'll talk about later.) This class also has two user parameters, 'center' and 'radius'.

We'll talk about this class from the bottom up. The default: section should look familiar to you if you've written UF formulas before; it lists each user parameter and provides a title for the class.

In the public: section we see the members (variables and functions) for this class. The IsInside() function takes a complex number and returns true is that number is inside a user-defined circle; it returns false otherwise. But curiously, it also saves the distance to the circle's center into a variable called lastdistance. Normally, UF will complain if you try to use a variable without declaring it inside the function, but in this case, the variable is declared in the class. That means it is available to all the class's methods. More than that, the value goes with the object. Each object has its own copy of the class member variables. (Member variables are also called "fields" in the Ultra Fractal help file; they mean the same thing.)

An example will make this clearer. This code uses the CircleTester class:

  $define debug
  CircleTester object1 = new CircleTester()
  CircleTester object2 = new CircleTester()
  
  bool inside1 = object1.IsInside((3,4))
  bool inside2 = object2.IsInside((2,0))
  
  print(object1.lastdistance)
  print(object2.lastdistance)

Let's pick this code apart. First, we create two instances (objects) of our CircleTester class. Since these aren't created from parameters, they will get the default parameters of 0 for the center and 1 for the radius; that's OK for now. The important point is that we have two separate tester objects.

Next, we perform two tests, one with the point (3,4) on object1 and one with the point (2,0) on object2. The IsInside() function is a method, so to call it we have to use an object name, a period, and then the method name.

Finally, we print out the lastdistance value from each of our objects. We will see two numbers, 5 and 2. As with the method, to access the object's member variable we use the object name, a period, and the member variable name. If we didn't specify the object name, UF wouldn't know which object contained the variable; it would look like a local or global variable (and in fact UF will look for those instead).

How does the IsInside() function know which object's variable to change? That's why you have to give an object name to call a method. Inside the method, when it accesses a member variable by name, it's the member variable that belongs to the object by which the method was called. Whenever you call a method, UF knows which object's data the method should have access to. And since a method needs to know where the object's data is, you can't call a method without an object. Our example code has two separate objects, so when object1.IsInside() changes 'lastdistance', it's changing object1.lastdistance. When object2.IsInside() changes 'lastdistance', it's changing object2.lastdistance.

CONCEPTS: CONSTRUCTORS AND CREATING OBJECTS

A 'constructor' is a special function that UF calls whenever a new object is created. You name the constructor function with the same name as the class.

In our CircleTester class, we included a CircleTester() function. This is the function that UF calls automatically when new CircleTester objects are created. For our example, we want to make sure lastdistance is set to a meaningful value, so we set it to zero. This is a very good practice; you should always explicitly set your variables to a starting value so you know they're ready for use. (And in fact UF will warn you in many cases if you try to use a value before it's initialized.) For member variables it's not technically necessary, but for any local variables it is; the best approach is to always make sure your variables are initialized so it's obvious to anyone reading the code.

It is possible to create constructors that require parameters. You will see many examples of this in the common class library, where most of the classes require a parameter. We'll talk about that later, when we cover specific classes from the library.

When you want to create an object from user-selected parameters, you use the parameter name in place of the class name, like this:

  CircleTester object1 = new @circle()

Note that your object variable still has to be the correct class type or UF will give you an error. You list the parameter like this:

  CircleTester param circle
    caption = "Tester Object"
    default = CircleTester
  endparam

This would go in the default: section of the code that USES the @circle parameter and the CircleTester class.

CONCEPTS: MEMBER VISIBILITY

In our example CircleTester class, there were two sections, default: and public:. public: doesn't just mean "member variables and methods go here", it means something else as well. It means "these members can be accessed by code that doesn't belong to the class." We call this "visibility" because they can be "seen" by non-class code.

There are two other categories of visibility, protected: and private:. They are very similar in that both prevent non-class code from seeing their members. They differ only that with private:, derived classes can't see the members, either. (See the Inheritance section for more on this.)

It may seem very strange to restrict access to an object's data or methods. After all, the data is there to be used, right? Well, in very practical terms, programmers are human. In many cases those members are there only because the class code needs to keep track of things for itself, and it really doesn't want to make that data public. Not because you shouldn't see it, but because it's very particular to how the class operates, and if the class is updated, it might change how the data is stored. If your code relied on internal data like that, when the class is updated, your code will stop working. That would be bad.

Member visibility--being able to hide data from other code--is all about helping you write reliable code. As a rule of thumb, all of your member variables should be protected: unless the code that uses the objects will be accessing them very, very often. Instead of making the member variables public:, you can write "accessor" functions:

public:
  float func GetLastDistance()
    return lastdistance
  endfunc

Then make lastdistance protected: like this:

protected:
  float lastdistance

Now, the only way to get the value is to call GetLastDistance() instead. Then, if the class is updated and the lastdistance member variable is no longer available, you can change how GetLastDistance() works so that it is backwards-compatible, and code that uses the class will continue to work--even though the internals of the class have completely changed.

CONCEPTS: INHERITANCE AND OVERRIDING

We've come a long way. We've covered functions, scope, classes, members, methods, constructors, and visibility. These have all been necessary building blocks to introduce one of the most powerful concepts in object-oriented programming: inheritance. You need to know how inheritance works in order to take advantage of all the code made available to you.

Inheritance is a way to define a class by saying "it's like this other class, but change this, and this, and this..." Inheritance is what lets you build on the code of others, WITHOUT actually having to copy their code. When you define a class this way, we say that "class B inherits from class A" or "class B derives from class A" or "class A is the base class of B". These phrases all mean the same thing: class A has some stuff, and class B adds more stuff and/or replaces things in class A.

Nothing beats an example, so let's make one.

class EllipseTester(CircleTester) {
public:
  bool func IsInside(const complex p)
    lastdistance = cabs(p - @center) + cabs(p - @center2)
    if (lastdistance < @radius)
      return true
    else
      return false
    endif
  endfunc

default:
  title = "Ellipse Tester"
  
  complex param center
    caption = "Center 1"
    default = (-0.25,0)
  endparam
  complex param center2
    caption = "Center 2"
    default = (0.25,0)
  endparam
  float param radius
    caption = "Combined Distance"
    default = 1
  endparam
}

This is an ellipse tester, which defines an ellipse with two points and a combined distance to each of those points. We provide the name of the class, and in parentheses indicate the name of the base class--our CircleTester class from above.

We don't provide a constructor function, even though I clearly said it is best to make sure our variables get initialized. In fact, I didn't even declare lastdistance. That's OK, though, because EllipseTester "inherits" these things from the CircleTester class. Because an EllipseTester object IS a CircleTester object (just a more specific type) it will have a lastdistance member. And since we didn't create a constructor for EllipseTester, the constructor for CircleTester is used instead. (If we had defined a GetLastDistance() function, that too would be available in EllipseTester automatically.)

We did, however, provide our own IsInside() function. Because the function has the same name as a function in the base class, it has to have exactly the same return value type and exactly the same parameter list. Even the parameter names have to be the same. But the body of the function can be completely different. In EllipseTester, we use a different distance calculation method--one that works for ellipses, rather than circles. When we do this, we are "overriding" the function from the base class and providing a new version for the new class.

In the default: section we define a new title and we redefine the parameters. We could have left the 'center' parameter alone, but it is helpful in this case to provide a new caption and default value. Note that if you want to change only one thing in a parameter, you must give all the parameter's details when you override it, even the details you are not changing.

We can create EllipseTester objects just like we create CircleTester objects:

  EllipseTester object1 = new EllipseTester()
  bool inside1 = object1.IsInside((3,4))

Notice that the actual call to IsInside() looks exactly the same as for CircleTester. UF knows what class object1 is, so it knows which IsInside() function to call.

It might seem that UF knows what class object1 is because we told it:

  EllipseTester object1 = new EllipseTester()
  ^------ here -------^

But that's not really how it knows. Instead, it knows because of this:

  EllipseTester object1 = new EllipseTester()
                          ^---- here -----^

Because we can rewrite the code like this:

  CircleTester object1 = new EllipseTester()
  bool inside1 = object1.IsInside((3,4))

And the IsInside() that gets called is actually the one from EllipseTester, not the one from CircleTester.

This is the very most clever thing about object-oriented programming. Objects know what their methods are, even if you treat them as though they're of the base class type.

You can always treat an object as though it's the base type. Note that we assigned a reference to an EllipseTester object to a CircleTester variable. This is always valid, because an EllipseTester is always a CircleTester (because EllipseTester is derived from CircleTester). It isn't always possible to go the other way. Suppose we've added a SquareTester class that is also derived from CircleTester. Then these examples would hold:

  SquareTester square = new SquareTester()
  EllipseTester ellipse = new EllipseTester()
  CircleTester circle = new CircleTester()
  CircleTester tester = 0
  
  tester = circle   ; valid, because a CircleTester is a CircleTester
  tester = square   ; valid, because a SquareTester is a CircleTester
  tester = ellipse  ; valid, because an EllipseTester is a CircleTester
  ellipse = tester  ; not valid, because a CircleTester is not an
                    ; EllipseTester

  ellipse = EllipseTester(tester) ; valid, because at this point
                                  ; tester has an EllipseTester
  circle = CircleTester(tester)   ; valid, because tester has an
                                  ; EllipseTester, which is derived
                                  ; from CircleTester
  square = SquareTester(tester)   ; not valid, because tester has an
                                  ; EllipseTester, which is NOT a
                                  ; parent class of SquareTester;
                                  ; square will be set to NULL (0)

This needs a bit of explanation. With the first set of statements, we define some variables and put objects into them. We assign a NULL (0) value to tester because we haven't put anything in it yet--it doesn't refer to any object--and we said it was always a good idea to set our variables explicitly to some starting value.

The second set of statements shows various assignments. A variable has a type (you set the type when you declare the variable, as we did in the first set of statements) and you can only put things into a variable if the types are compatible. UF decides this when it compiles your formula, so if the types don't fit, you will get an error and your formula won't compile at all. The last statement in this set shows what happens if you're not paying attention; even though your code puts a value into tester that should be compatible, the compiler looks at the type--putting a CircleTester into an EllipseTester variable--and knows that can't be guaranteed to work, so it gives you an error. This helps you catch mistakes if that's not what you intended to do; if it IS what you intended, though, this is a problem.

The solution is to use a "cast". "Casting" is when you ask the UF compiler to attempt to treat one object as a different type. The compiler then knows you're prepared for the conversion not to work, and if it doesn't work (because the types really are incompatible, with the values in the variables when the code runs) it will give you a zero value (NULL). So, the first cast works, because (ignoring the invalid statement in the previous set that won't compile) tester would have an EllipseTester object in it.

The second cast also works, because even though tester has an EllipseTester object, that can be treated as a CircleTester when necessary, since CircleTester is a parent class of EllipseTester (just like this would work for an assignment instead of a cast).

The third cast does not "work", because you can't treat an EllipseTester object as a SquareTester; SquareTester is not a parent class of EllipseTester. Because the cast doesn't work, the result of the case is NULL (0) and that is what gets stored in the square variable.

Although you can use casts to do certain things, you should be wary of writing code that relies on casts. Code written this way is hard to maintain and hard to re-use. It is not always wrong, but it is a clue that perhaps the approach is not the best.

CONCEPTS: STATIC FUNCTIONS AND 'THIS'

Inside a class method you can reference member variables directly. But sometimes you need to use a reference to the object the method is working on. The variable 'this' is always defined inside a method to be that current object. So these do the same thing:

  lastdistance = cabs(p - @center)
  this.lastdistance = cabs(p - @center)

Most of the time you will not need to reference 'this'. It is useful primarily when you are calling methods in other classes and they need to know about your object.

We mentioned earlier that objects know what their methods are, so even if you treat an object reference as though it's a base class type, it knows what its methods are. But that means you can't call a method unless you have an object of that type. (After all, if you don't have an object, there is no 'this' to give to the method.)

There is a way, though, to create a function associated with a class that does not require or use an object. Such a function is called a "static method". In this case "static" doesn't refer to the hiss from your radio when it's not tuned to a station; it's "static" in the sense that it doesn't change. It's always there, even when objects come and go. And it's also "static" in the sense that UF doesn't change the function based on the object. This seems confusing, but here are some examples. Suppose we have added a static method to CircleTester:

  static complex func Rotate(complex p, float a)
    return p * exp(flip(a*180/#pi))  ; rotate p by a degrees
  endfunc

Note the keyword 'static' that tells UF this is a static method and not a regular method. We can invoke this method several ways:

  complex p = (3,4)
  CircleTester circle = new CircleTester()
  p = circle.Rotate(p, 30)       ; invoke as a method of a variable
  p = CircleTester.Rotate(p, 30) ; invoke directly as a static method
  EllipseTester ellipse = new EllipseTester()
  p = ellipse.Rotate(p, 30)      ; calls CircleTester.Rotate()

Note the last example: since EllipseTester doesn't have a Rotate() method, UF looks in CircleTester and finds the method there and calls it. But what if we add this to EllipseTester?

  static complex func Rotate(complex p, float a)
    return p * exp(flip(-a))  ; rotate p by -a radians
  endfunc

(This version is different because it treats a as radians rather than degrees and rotates in the other direction. In general it's a bad idea to make this kind of change in a derived class because it's very confusing. But this is an example to show you how UF chooses the method to call, so we'll let it go.)

With this static method in EllipseTester, we observe the following:

  complex p = (3,4)
  CircleTester circle = new CircleTester()
  p = circle.Rotate(p, 30)       ; calls CircleTester.Rotate()
  EllipseTester ellipse = new EllipseTester()
  p = ellipse.Rotate(p, 30)      ; calls EllipseTester.Rotate()
  circle = ellipse
  p = circle.Rotate(p, 30)       ; calls CircleTester.Rotate() !!

Note that with a normal method, UF looks at the OBJECT to see which method to call, so even if you assign a derived class object to a base class variable, the derived class method will be called. But for a static method, there is no object; UF just looks at the variable type (not the contents of the variable) to decide which static method to call.

Static methods are useful because you can call them even before you have an object reference. But you can't override static methods the same way as normal methods. Even more importantly, there is no 'this' available inside a static method; you can call it without an object, so you won't have access to object member variables even if you invoke the static method through an object.

Some of the common library classes use static methods, so that's why this unusual concept is covered here. Most of the time you won't need to create static methods yourself.

PRACTICE: COMMON LIBRARY

Congratulations on making it this far. We're done with concepts, and now it's time to start getting into the nitty-gritty, the details, the real work.

When you write classes for UF5 you don't have to start from scratch. We have already built a common set of classes for you to work with, and these classes serve two functions:

  1. They provide a foundation that takes care of many of the "mundane" details of setting up calculations inside UF.
  2. They provide a consistent framework so that your classes will fit into matching slots on others' classes.

The common library is a collaboration of several formula authors and programmers from the UF beta testing team. Some of us have professional programming experience. Most of us have extensive UF programming experience. We aren't perfect, so the common library likely won't be perfect, but there is usually a reason for every quirk in the library.

The library itself is also moderately large. You can find a complete reference here:

http://formulas.ultrafractal.com/reference/

"Complete" in this sense means "all we have at the moment". We are always adding to this documentation.

If you've just installed UF5, you should be sure to update your formula collection to be certain you have the latest edition of the common library from the public formula database.

PRACTICE: IMPORTING LIBRARIES

When you write a class with UF5, by default it will only look inside your .ulb file for classes. Working with the common library means that many of your classes will be in common.ulb. There are two places you need to indicate you're using the library. The first is in the class declaration, where you indicate the base class; the second is inside the class body.

class MY_FunkyTrapShape(common.ulb:TrapShape) {
  ; this is my funky trap shape class
public:
  import "common.ulb"
  ...

There are situations where you could omit the 'import' statement but it's simpler to just include it whenever you're working with classes that are based on the common library.

PRACTICE: NAMING CONVENTIONS

If you are writing formulas for yourself and you don't intend to share either the formulas or the parameters with anyone, you may name your classes whatever you like. Since nobody will ever see or work with your code except you, it doesn't matter.

On the other hand, if you do plan on sharing (and we hope you do!) then there are some common practices we use so that everyone's classes will play nicely together. First, we suggest you sign up as a formula author at the public formula database site:

http://formulas.ultrafractal.com/

It doesn't cost anything and your email address is never given to anyone. During the signup process you'll choose a three- or four-letter code that identifies you. We suggest your initials (e.g. 'DMJ' for Damien M. Jones).

Once you've set your prefix code, you should name your classes with that prefix (in upper case letters) followed by an underscore and then the class name in mixed case. Examples:

class DMJ_TrapPosition ...
class MMF_SimpleMatrixTransform ...
class REB_CayleyJulia ...

You don't have to follow this convention, but if others are trying to build on your work it makes it easier to avoid 'name collisions'. This is when two people try to use exactly the same name.

Class names that don't have a prefix should only come from the common library.

PRACTICE: TRAPSHAPE

Let's start with a very easy example, trap shapes. Trap shapes are used primarily in orbit trap-based colorings. The concept is that you mark a "shape" in the complex plane and watch the interaction between the fractal orbit (the iterating values from the fractal formula) and the trap shape itself.

We'll need a "test harness", which is just a particular parameter setup where we can easily plug in our new trap shape. Start with this:

TrapShapeTestHarness {
; www.fractalus.com
::2gYUqjn2VaVXvpNMU03Ri/DW+9BOpLMWn8LtVrTTqaTq84kQmEHiVdsjsNly/+djTIQLJE3n
  An75cuf4rv25GWqjJvd6EEyJcSOFvywqeugVxRr4WH6XMjibtY0eRmrgukQQFcx2Cn/vS2Bu
  xSjq5na4ZCnlifgVK4K0TzQ/WDc/xi5x3MPmQWinOxTw7uUWlToVU8ds0X2a07UZYkuilKcH
  oREy0JlsqKhabDauyxN0vQmlMngKZbV1ONXbK3JZeEls3E1QAqochkrYlQ28sjpyYmsZ7yLx
  IQEzBK+RuibEp/shNumd16WtuXys2z5J3c7xFrfC+lL3Y0ueIN711bbE2HDXYsdZTu1D5Mxr
  cDog7MgfEl1xMOKZefKUtuSvHKAxzJXaaDTI17c0IeMwVosiMezuuhps5ANFsXNdCg5STShi
  zM9WVTl+Kx7LsPaYZQHg7etUbgdw2iVa7yrWi/jZjwV3Da7j1F14Pa946TV5L43GcjhsJ7hq
  IFnqLL1KfcWHZrOaBfVevPWHD6J3FA4dWu5TRA+f1ngQlnQ7ZuuutQQXqflbhepQYkZE5uQl
  3mygpTRhAFOey8jWIfG0WHvKMG2X47DHZ46ysV8UX/5otei8QnZ6GZ/XtQ5wDyf8Oyjw8Bv3
  lRkrUzhuj+PcwN4rxK0jGekduKIoVBDHmXa4sssIaA4Gt97MBjHVw8dqUfHazk1xUVDj9DKO
  BgBGoNSOeky2M0p5Sd2VbIfCsfvUbhHRgHi/49Btoq3Y9OMMY7FuCXBsrVolZD2I0BgMLO5C
  zwMWrWKyGI97u+4Klgj3/9gAq2qU+/qFCPoUjXNODpfk7IXh1B33Uc3hVdZcdOd8iQ/d92St
  Gee3BuFJg348Glg8spL/W0i4lLb/a9luNfPaRUc0ykkWDxNRQjtvuI57RtGu5EjkFw7Laeg3
  5+Up7c5xn/FnkMdy/18q7uH=
}

Paste this into a new fractal window. Go to the outside coloring tab on the Layer Properties tool window and locate the Trap Shape class slot. Click the icon and a browser will appear; you can choose any TrapShape- derived class to go in this slot. Look in dmj5.ulb, kcc5.ulb, or reb.ulb for lots of choices, if you want more than Standard.ulb contains.

A TrapShape class has a fairly simple job. Given any complex number, determine a "distance" to the trap in a way that makes all points with the same distance form a particular shape. For example, if the trap "distance" is simply the distance to a fixed point, all points with the same distances will make a circle. If the trap "distance" is the sum of the distances to two fixed points, all points with the same distances will make an ellipse.

To make a new TrapShape mostly involves figuring out the "distance" function, and plugging it into the TrapShape frame. The "distance" function is found in its "Iterate" method, which is the method called whenever there's a trap distance that needs to be figured out. So we can make a new TrapShape like this:

class MY_FunkyTrapShape(common.ulb:TrapShape) {
  ; this is my funky trap shape class
public:
  import "common.ulb"

  float Iterate(complex pz)
    return cabs(pz) * sqr(cos(atan2(pz)*5))
  endfunc
  
default:
  title = "Funky Trap"
}

And that's it, we have a trap shape that we can plug into a lot of different formulas. You can plug it right into the test harness given above and take a look.

Notice that for our trap shape, we didn't include parameters that set the center of the trap shape, or its rotation. Classes which use TrapShape objects normally take care of this by allowing the user to choose a Transform (like TrapPosition) which accomplishes the same thing. This illustrates an important point when working with objects: don't duplicate effort. If there's another class that does what you need, consider using it. PRACTICE: TRANSFORM

Writing a transformation for UF4 is very straightforward. You create a skeleton formula with global:, transform:, and default: sections, then you fill them in. You put any one-time initialization in the global: section. You fill in transform: with whatever calculations you want to do to the #pixel value. You fill in the default: section with parameters to control the transformation.

You can still write transformations this way with UF5. But if you do, you will lose some of the benefits of the object system, because even though your transformation can USE an object, the transformation ITSELF is not an object, so other code that wants to use a transformation object can't use your transformation.

The object version of a transformation is derived from the UserTransform class. UserTransform is derived from Transform, which is a general class that turns out to be useful for lots of things, but the UserTransform branch is where the transformations corresponding to UF4 transformations belong.

If you write your transformation as a UserTransform-derived class, you can use it with the "skeleton" transformation formula just like an old- style transformation, but you can also use it with any class that has a Transform slot. And it's as easy to write a UserTransform as it is to write a transformation formula.

With a UserTransform, code that you would have put in the global: section should be placed in the class constructor. Code that would have been in the transform: section goes in the Iterate() function. And the default: section can be copied as-is.

There are a few more notes. First, any variables that would have been declared in the global: section should be declared as member variables. You can reference these from your Iterate() function directly. Second, in your Iterate() function you should never refer to #pixel; you should instead reference the pz parameter given to Iterate(). Third, you should not refer to #solid or write to it; instead, set the m_Solid member variable (it's declared all the way up in the Transform base class). Fourth, you should try to avoid any other location-dependent values like #width or #screen; depending on the context your class will be used in, these values might not be appropriate for your transformation.

For more details, see the class reference on UserTransform.

PRACTICE: FORMULA

As with transformations in UF4 having an object version in UF5 (the UserTransform class), with UF5 we have an object version of the fractal formula. It's called Formula.

The job of a Formula is simple: given a starting complex number, create a sequence of complex numbers, and after each step, decide whether the sequence should continue or not.

With UF4, fractal formulas have global:, init:, loop:, bailout:, and default: sections. With the Formula class, these correspond to the constructor, the Init() function, the Iterate() function, the IsBailedOut() function, and a default: section. There are also some other functions that we'll get to when we talk about special Formula class types.

Just like with transformations, any variables you would declare in a global: section instead become member variables. You can then access them directly in your Init() and Iterate() functions.

The Init() function does the same job as an init: section. You should set up any variables you need (again declared as member variables) and return the first value in the sequence. Don't set #z directly, and don't refer to #pixel--#pixel is replaced by pz and you should return the first value rather than set #z. As with UserTransform, don't refer to other location-dependent # variables as they might not be valid for how your Formula is being used.

The Iterate() function corresponds to the loop: section. Rather than modify #z directly, you should return the next value in the iteration sequence. Don't look at #z, use the pz parameter instead. Don't assume the pz given to you is the same as the last value you returned; it may have been changed by the calling code. (If you need #pixel in your Iterate() function, save the pz value in a member variable during your Init() function.)

The IsBailedOut() function corresponds to the bailout: section, but it deserves some special notes. First, the bailout: section returns TRUE if iteration should continue; the IsBailedOut() function returns FALSE (because the assertion, "it is bailed out", is false). IsBailedOut() returns the OPPOSITE of what a bailout: section would. Second, you can provide the code for this function yourself if you like, but you may also do the computation in your Iterate() function and set m_BailedOut accordingly. (The base class implementation of IsBailedOut() just returns m_BailedOut.)

There are some extra functions in the Formula class that also require some explanation. Sometimes when using a Formula it is helpful to know what its primary exponent is and what the bailout values are. To know how to interpret these values you need to know whether a formula is "divergent", "convergent", both, or neither. The answer will depend on the mathematics of the formula.

When points are iterated through a fractal equation, the resulting sequence (the "orbit") exhibits one of four behaviors:

  1. They grow without bound. We call this a "divergent" orbit.
  2. They approach (but possibly never reach) a single value. We call this a "convergent" orbit.
  3. They settle into a repeating pattern. We call this a "periodic" orbit.
  4. They appear to be random. We call this an "ergodic" orbit.

Ultra Fractal, like many software programs before it, divide each fractal into two areas, "inside" and "outside". For classic fractals like the Mandelbrot set, the "outside" includes all points which produce divergent orbits; the "inside" includes everything else. We call this type of fractal "divergent" because that's what the outside areas are composed of, and the outside areas normally have the most interesting detail.

However there are some fractal formulas which do not have many divergent orbits at all. For these fractals, the classification scheme used for the Mandelbrot set would not be useful. The most interesting areas to color in these fractals are the convergent orbits. Formulas in this category include Newton, Nova, Halley, and other root-finding- based formulas. For these, we treat the convergent orbits as "outside" and everything else as "inside". We call them "convergent" fractals.

A few formulas don't even have convergent orbits, but only periodic and ergodic orbits. We still consider them convergent fractals for the most part.

The reason all of this is important is that you need to know whether your formula is divergent, convergent, both, or neither. You need to know, because it will affect how your bailout is computed. The basic test to see whether a value is growing without bound is to set a very large bound (well outside the area where the orbit might turn around and do something interesting) and if the orbit value ever exceeds that bound, we assume it's a divergent orbit. We call this bound the "bailout value" and it's very easy to make this test. (In fact, the earliest computations of the Mandelbrot set used it because it was very fast.) Divergent bailouts tend to be large numbers. Increasing the size of a divergent bailout increases the number of iterations that must be done before the bailout is reached.

The test to see if an orbit has converged is slightly harder. It just tests the distance between the current orbit value and the previous one, and if the difference is LESS than a very small number, we assume the orbit has converged. In this case, the "bailout value" is a SMALL number. Decreasing the size of the bailout increases the number of iterations that must be performed to reach it.

The test to see if an orbit is periodic is quite a bit harder and I'm not going to cover it here. Typically, though, it also uses a SMALL value and decreasing it increases the iterations required to reach it.

There's no test for ergodic orbits other than to run them for a long time and, if they don't appear to be divergent, convergent, or periodic, we assume they're ergodic. In truth ergodic values may only appear when irrational numbers are used in the fractal formula, so we can only approximate those on a computer, and we would never have a "real" ergodic orbit.

Once you know what kind of fractal formula you're writing, you can choose the appropriate Formula type. For divergent fractals, use the DivergentFormula type; for convergent fractals, use the ConvergentFormula type; for fractals that have both divergent and convergent areas in the outside, use the ConvergentDivergentFormula type. Use the selected type as your base class, rather than the basic Formula class, and your IsBailedOut() method will be written for you, your bailout parameters will already be provided, and the two methods for giving those values--GetUpperBailout() and GetLowerBailout()--will already be written.

PRACTICE: COLORING

Writing coloring formulas for UF4 involved filling out global:, init:, loop:, final:, and default: sections. With UF5 you can use the GradientColoring or DirectColoring classes to make object versions of coloring algorithms. GradientColoring is the base class you use when you're writing a gradient-based coloring; DirectColoring is the base class you use when you're writing a direct-coloring formula. They are both derived from Coloring, but most of the time you won't need to deal with Coloring directly.

Just like with transformations and fractal formulas, your variables should be declared as member variables and your global: initialization code placed in the constructor. The contents of your init: section go in your Init() function and the loop: section goes in the Iterate() function. Your final: section goes in the ResultIndex() section (for gradient-based coloring) or the Result() section (for direct coloring). Your default: section can be copied directly.

There's a reason your final: code goes in a different place, depending on the type of coloring you're using. It's because the return value is different for the two different types of coloring formulas. A gradient-based coloring formula returns a float; a direct-coloring formula returns a color. So, two functions are provided: ResultIndex() (which returns a float) and Result() (which returns a color).

What does Result() do in GradientColoring? It calls ResultIndex() and then passes the return value to gradient(), which provides the color from the gradient at the indicated index--very similar to what UF does normally when you use a gradient-based coloring. This color is then returned.

This behavior shows how you can "wrap" functionality and make things fit together in surprising ways. A formula that wants a DirectColoring can instead indicate it wants a Coloring (not specifying gradient-based or direct coloring) and the GradientColoring code will automatically make any gradient-based coloring work like a direct coloring. (You can't go the other way; there's no simple, defined way to make a color into an index value.)

One more note: the Init() function for Coloring provides two parameters rather than just one--it provides both the first iteration value AND the pixel value given to the fractal formula to produce that iteration value. Some coloring formulas need both of these values to work. The base Coloring class will automatically save the pixel value in the m_Pixel member variable so you can easily refer to it later.

PRACTICE: PATTERNS

By now you may have noticed some "patterns" emerging from the collected common formulas, especially those that have parallels to UF4 formulas. Many of the classes follow the constructor/Init/Iterate pattern. This is not an accident; it is done this way so that you can use all of the classes more quickly. We make extra allowances in the patterns to accomodate the unique needs of fractal formula generation, but here is a brief summary of the supported patterns in the common library:

Transform: complex(1) -> complex(1) + bool(1)
Formula: complex(1) -> complex(n) + bool(n)
GradientColoring: complex(n) -> float(1) + bool(1)
DirectColoring: complex(n) -> color(1) + bool(1)

IntegerTransfer: int(1) -> int(1)
Transfer: float(1) -> float(1)
ColorTransfer: color(1) -> color(1)

IntegerGenerator: int(1) -> int(n) + bool(n)
Generator: float(1) -> float(n) + bool(n)

TrapShape: complex(1) -> float(1)
ColorTrap: complex(1) -> color(1)
GradientWrapper: float(1) -> color(1)

Obviously there are some combinations that have been left out, but these cover most of the needs of fractal generation.

PRACTICE: VERSION NUMBERING

With UF4, each formula was an isolated block of code. Changes to one formula could affect only those users and images that relied on it. And it was possible for affected users to keep old copies of formula files to render old parameters if no other solution could be found.

With UF5, the situation is more complicated. Any particular class may depend on library files written by other formula authors, increasing the chances for incompatibilities substantially. We assume that all formula authors (including us) are human, and therefore prone to make mistakes. To protect ourselves, we include "version parameters" that will allow us to fix problems once they're discovered.

Here's how it works. We define a parameter, named "v_" followed by the class name (e.g. "v_trapshape"), and set its default value to 100. We set the parameter to be invisible (since we don't really expect the user to modify it). Whenever we distribute an updated version of our class, we make sure we increase the default value. Parameters created with older versions of the class will already be tagged with a version parameter, so they won't be affected by the change in default value; new parameters will get the new default value.

If at any point we discover an error, or something that makes the class produces fractals that look different, we now have a parameter that tells us which version of the class produced the "correct" image for those parameters. We can add code that looks at the version parameter and changes the class behavior so it reproduces the old image while still working properly with new images.

Our version parameter is based on our class name because each class is derived from some other class, and each of those classes all the way up to the Generic base class can have its own version number (and needs to have it in case changes to those base classes have to be corrected). If you follow this version number convention, your version numbering will work alongside the common library version numbers. You can use any of the common library version number parameters as a template for your class version number.

GO FORTH AND CODE

Yes, it's a lot to take in. One of the biggest challenges in writing object-oriented code is that there is so much to learn; not only do you need to know the details of object-oriented programming, but you also need to know how the class library you're using works so that you can use it effectively. That's why we have an online reference that details the classes in the common library, so that you have a place to start. UF5 has formula ratings which will help steer you towards formulas that are well-written and generally useful. And we have an active mailing list where you can post technical questions.

The introduction of object-oriented programming to Ultra Fractal is a big deal, and it's going to take formula authors some time to adjust to a more robust programming environment. You don't have to learn it all at once. Proceed at your own pace, and most importantly, have fun!