logo
The Falcon Programming Language
A fast, easy and powerful programming language
Location: Home page >> Falcon wiki
User ID:
Password:
 

Falcon Wiki - Survival:Prototype based OOP


 Prototype based OOP

Classes and singleton objects have a fixed structure which cannot be changed during runtime. At times, defining the structure of objects dynamically may be useful; for example , property tables may be loaded from a file, or class definitions may be provided externally from another program. Creating an instance is then a matter of copying the structure of an original model item (the prototype), and this operation doesn't bind the structure of the new instance to stay faithful to the base instance definition for its whole lifetime.

We have already seen that arrays can hold both ordinal data and bindings, and these bindings can also be functions. When calling a function bound with an array, the value of self gets defined as the array for which the function has been called. See the following example:

vect = [ 'alpha', 'beta', 'gamma' ]
vect.dump = function ()
    for n in [0: self.len()]
       > @"$(n): ", self[n]
    end
end
 
vect.dump()

So, arrays with bindings can be seen as instances of abstract classes, which also hold a ordered set of 1..n values, which are accessible with the square accessors.

Dictionaries can provide another form of prototyped instances. String keys without spaces can be seen as property and method names. To tell Falcon that we'd like to have a dictionary as a prototype, we need just to bless it:

function sub_func( value )
   self['prop'] -= value
   return self.prop
end

dict = bless( [
  'prop' => 0,
  'add' => function ( value )
             self.prop += value
              return self.prop
            end ,
  'sub' => sub_func
   ])

As the above example shows, it is possible to place in the dictionary data and functions, either declared directly in the dictionary or elsewhere.

Blessing (that is, calling the bless function on the dictionary) is necessary because dictionaries are meant to hold potentially huge amounts of data (in the order of several hundred thousand items); the non-blessed dictionaries can be distinguished so that applying method on them doesn't require a full scan of their content, but just a search on the standard dictionary methods. Without a blessing mechanism, a simple len method applied on the dictionary would have caused first a complete search in all the keys, and then the needed scan in the standard dictionary methods. This is often not desired. Also, blessing a dictionary has the visual effect of declaring it as not just a dictionary.

Blessed dictionaries also can be accessed with the dot accessor, and functions stored in dictionary values and retrieved through the dot accessor are called as methods, with the self item correctly set to the owning item. Under every other aspect, they stay normal dictionaries; for example , as shown in the sub function, they can still be accessed by their key. Adding new string keys has the effect of adding new properties or methods dynamically; still, it is possible to add any item as a dictionary key, and retrieve it as usual.

dict.add( 10 )  // adding 10 to prop
> "test 1: ", dict.prop
dict.sub( 5 )   // and now subtracting 5
> "test 2: ",dict["prop"]  // accessing as a property
 
// Adding dynamically a new method
dict["mul"] = function( val ); self.prop *= val; end
dict.mul( 3 )
> "test 3: ", dict.prop

Except for the fact that blessed dictionaries define their inner data to be accessible also via the dot operator, and while array bindings do not reference the sequential data contained in the owning array, they are mostly interchangeable; so we'll use just blessed dictionaries in the rest of the chapter, eventually specifying specific behavior of dictionaries or array bindings.

Exactly as for class based OOP, properties declared with an underline sign _ are accessible only through the self item, becoming private; however, they can be accessed normally through the dictionary interface (they are just strings).

 Instance creation

A program relying on prototype OOP usually needs some means to create similar objects (the instances). This is usually achieved by two means: factory functions or instance cloning.

Prototype factory functions

The most direct way to create an object in prototype OOP is to have it returned from a function. For example :

function Account( initialAmount )
    return bless([
      "amount" => initialAmount == nil ? 0 : initialAmount,
      "deposit" => function ( amount ); self.amount += amount; end ,
      "withdraw" => function ( amount )
          if amount > self.amount: raise "Not enough money!"
          self.amount -= amount
      end 
      ])
end

acct = Account( 100 )
> "Initial amount ", acct.amount

acct.deposit( 10 )
> "After a small deposit: ", acct.amount

Remember that a ; is needed after each statement when declaring functions inside dictionaries. 

In this example, Account is a normal function, but it just returns a new instance of the dictionary. 

Prototype cloning

The word prototype means that every object can be seen as a prototype of a hierarchy of cloneable and differentiated objects.

Every Falcon item can be cloned through the clone method of the base FBOM system. Continuing the above example:

nacct = acct.clone()
nacct.withdraw( 100 )
> @ "Left on acct $(acct.amount) and on nact $(nact.amount)."

Normally, clone performs a shallow copy of everything that's in the cloned object; this means that other deep objects that may be contained in the instance (as, for example , arrays or other instances) are not cloned themselves.  To override the normal cloning process, it is possible to re-define the clone method inside the object:

nacct["clone"] = function ( amount )
    newItem = itemCopy(self)
    if amount: newItem.amount = amount
    return newItem
end 
 
acct2 = nacct.clone( 50 ) 
> "New instance reinitialized with ", acct2.amount 

Calling the clone will now invoke the function we have written; to avoid using the clone method again (which would cause an endless recursion, we may either store it in a private property or use itemCopy function as we did in this example.

Referencing the factory function

A particularly elegant technique is to have the factory function stored somewhere in the returned vector:

function Account( initialAmount )
   return bless([
      "new" => Account,
      "amount" => initialAmount == nil ? 0 : initialAmount,
      "deposit" => function ( amount ); self.amount += amount; end ,
      "withdraw" => function ( amount )
          if amount > self.amount: raise "Not enough money!"
          self.amount -= amount
      end
      ])
end

instance = Account( 10 )
other = instance.new( 20 )
> "Amount in new instance: ", other.amount

This doesn't require copying the prototype, which may differ from the base idea of an initial instance as it should be.

A small prototype class sample

Classes themselves can be seen as objects. This is the base idea of reflection in reflective languages as Java and C#. So, it is possible to create classes that will actually have the duty to configure new instances and eventually provide some basic services. 

In this example, we create a base class which gives birth to an instance: 

base = bless([ 
      "new" => function (prop)
          return bless([
            "class" => self,
            "method" => self["method"],
            "property" => prop
         ]);
      end ,
 
      "method" => function (); > "Hello from ", self.property; end 
   ])
 
inst = base.new( "me" ) 
inst.method()

// outputs
Hello from me

Notice the usage of the array access operator instead of the dot operator to retrieve method during instance creation. Accessing an object via the dot operator, we'd store in the final instance method a reference to the object where the method was declared (that is, our base class). Calling inst.method would then actually resolve in calling base.method; this is not what we want. The idea is that the final method gets called having the newly created instance as self. By retrieving the method as a simple content of the dictionary, we can propagate it to the child instances, which will see them as functions, and transform them in correct methods as the dot operator is applied.

Operator overloading

As with standard Objects and Classes, prototype object operators can be overloaded. The concept described in Objects and Classes is essentially the same for prototype objects. One only need define the specific operator to overload. Operators fall into the following categories:

  • mathematical operators overloading
  • comparison overloadin

Mathematical operator overloading

Binary operator overloading take a single parameter (the second operand) and are usually, but not necessarily expected to return a value of the same type as self, or of the same type of the second operand. Binary operators are:

  • add__ Overloads "+" and "+=" operators
  • sub__ Overloads "-" and "-=" operators
  • mul__ Overloads "*" and "*=" operators
  • div__ Overloads "/" and "/=" operators
  • mod__ Overloads "%" and "%=" operators
  • pow__ Overloads "**" operator
imBlessed = bless ( [
	'angel' => 100,
	'devil' => 25,
	'add__' => function(val)
		printl('add__ called')
		self.angel += val
		return self
	end,
	'sub__' => function(val)
		printl('sub__ called')
		self.devil -= val
		return self
	end
] )

imBlessed += 25
> imBlessed.angel
aBitLess = imBlessed - 50
> aBitLess.devil

// OUTPUTS:
add__ called
125
sub__ called
-25

Unary operators overrides receive no parameters; they are the following:

  • neg__ Overloads the unary prefix negation operator ("-" in front of a symbol).
  • inc__ Overloads the prefix "++" increment operator.
  • dec__ Overloads the prefix "--" decrement operator.
  • incpost__ Overloads the postfix "++" increment operator.
  • decpost__ Overloads the postfix "--" decrement operator.

Little contrived example using neg__ and incpost__

unaryExp = bless ( [
	'chg' => 100,
	'incpost__' => function()
		printl('incpost__ called')
		self.chg = self.chg + 1
		return self
	end,
	'neg__' => function()
		printl('neg__ called')
		self.chg *= -1
		return self
	end
] )

unaryExp++
> unaryExp.chg
tmp = - unaryExp
> tmp.chg

// OUTPUTS:
incpost__ called
101
neg__ called
-101

Comparison overload

Comparison operators, namely <, >, <=, >=, == and != all refer to the same overload method: compare. Notice the absence of the "__" suffix. This is both because of historical reasons and because compare doesn't exactly overload operators, but serve a more complex purpose with a different semantic.

The compare method is bound to return a number less than zero, zero or greater than zero if the self item is respectively less than, equal to or greater than the comparand item, passed as parameter. Contrarily to mathematical operators, the compare method should not raise an error in case the items are not comparable: Falcon VM prefers to have a strategy to sort all the items, even when sorting has no physical reason. When the compare function hasn't any mean to determine a sorting order, it should return nil; this informs the virtual machine that the overload gave up, and that the default ordering algorithm should be applied: items of different kinds are ordered based on the value of their typeId() methods, and items of the same kind are checked based on the place they occupy in memory.


Navigation
Go To Page...

Loading

Elapsed time: 0.051 secs. (VM time 0.043 secs.)