FSClass

FSClass is a 'meta-class' that allows new classes to be created directly in F-Script programs, using F-Script code to implement methods, without adding any new keywords or syntax to the language. Classes created in this manner are full-fledged Cocoa classes, and instances of them can be used like any other Cocoa object. The 3.0 release of FSClass features numerous new bug fixes, a completely redesigned internal architecture, better handling of Objective-C categories, and compatibility with OS X 10.5 Leopard and F-Script 2.0.

Download FSClass 3.0

Download FSClass 2.0 (for F-Script 1.x and Mac OS X 10.4 only)

Sections:

  1. Introduction
  2. Adding Methods
  3. Properties
  4. Fast Instance Properties
  5. Key-Value Coding
  6. Constructors
  7. Inheritance
  8. Calling Superclass Methods
  9. Advanced Initializers and Factory Methods
  10. Class Names and Anonymous Classes
  11. Class Methods and Data
  12. Categories in F-Script
  13. Reflection Methods
  14. Using FSClass objects in Objective-C
  15. Future Plans
  16. Reference
  17. Download
  18. Legal and Miscellany
  19. Known Bugs
  20. Revision History

Introduction

FSClass is a "meta-class" that allows programmers to create new classes directly in F-Script, instead of having to write them in Objective-C. Methods are implemented with F-Script Blocks, and data is stored as Cocoa-compliant Key-Value properties.

FSClass creates 100% real Cocoa classes; if you use FSClass to create a class 'Foobar', then Foobar will be a fully legitimate class like NSObject or NSString. Method calling is very efficient: in most cases, the FSClass framework inserts only a handful of instructions between the invocation of the method in F-Script and the execution of the Block that implements the method.

To create a class in F-Script, we first create a new "class object", then add properties and methods directly to it. Individual objects are created by asking the class object for a new instance. No additional syntax or keywords are necessary, so the core F-Script language is unaffected and classes can be created programmatically. This approach will be familiar to anyone who has created classes in JavaScript using prototyping:

JavaScriptF-Script
// Declare the class and create a constructor
function Circle(r) {
    this.radius = r;
};
"Create the class"
Circle := FSClass newClass:'Circle'.
// accessor methods for radius - 
//  not strictly necessary
Circle.prototype.setRadius = function(r) {
    this.radius = r;
};

Circle.prototype.getRadius = function() {
    return this.radius;
};
"add radius property - automatically generates
    accessor methods"
Circle addProperty:'radius'.
    
"Create a class factory method"
Circle onClassMessage:#circleWithRadius:
    do:[ :self :radius | |instance|
        instance := self alloc init.
        instance setRadius:radius.
        instance
].
    
"Declare a constant"
Circle addClassProperty:'pi' withValue:3.14159.
// Geometry methods
Circle.prototype.area = function() {
    return Math.PI * this.radius * this.radius;
};

Circle.prototype.circumference = function() {
    return 2 * Math.pi * this.radius;
};
"Geometry methods"
Circle onMessage:#area do:[ :self |
    (self radius raisedTo:2) * Circle pi.
].

Circle onMessage:#circumference do:[ :self |
    self radius * 2 * Circle pi.
].
// class method - compare circles based on size
Circle.compare = function(circleA,circleB) {
    return circleA.compareTo(circleB);
};

// instance comparison method
Circle.prototype.compareTo = function(circle) {
    if (this.radius==circle.radius) {
        return 0;
    }
    else if (this.radius>circle.radius) {
        return -1;
    }
    else {
        return 1;
    }
}
"Comparison Method"
Circle onMessage:#compare: do:[ :self :otherCircle |
    (self radius) compare:(otherCircle radius)
].
// create and use a Circle
circle1 = new Circle(5.0);
circle1.setRadius(4.0);

alert("Circle area is : " + circle1.area);

circle2 = new Circle(5.0);

if (Circle.compare(circle1,circle2) > 0) {
    alert("circle2 is bigger than circle1");
}
else {
    alert("circle2 is bigger than circle1");
}
"create and use a Circle"
circle1 := Circle circleWithRadius:5.0.
circle1 setRadius:4.0.

sys log:'Circle area is: ' ++ (circle1 area description).

circle2 := Circle circleWithRadius:5.0.

((circle1 compare:circle2) = NSOrderedAscending) ifTrue:[
    sys log:'circle2 is bigger than circle1'.
]
ifFalse:[
    sys log:'circle1 is bigger than circle2'.
].

Note the following differences:

Methods

Here is an example of how to add methods to a class with FSClass:

Greeter := FSClass newClass:'Greeter'.

Greeter addProperty:'defaultGreeting'.

"factory creation method. sets a default greeting"
Greeter onClassMessage:#greeter do:[ :self | |instance|
    instance := self alloc init.
    instance setDefaultGreeting:'Hi there!'.
    instance
].

Greeter onMessage:#sayHello do:[ :self |
    sys log:'Hello!'.
].

Greeter onMessage:#sayDefaultGreeting do:[ :self |
    sys log:(self defaultGreeting).
].

Greeter onMessage:#sayGreeting: do:[ :self :greeting |
    sys log:greeting.
].


"Create a Greeter by using the class factory method"
myGreeter := Greeter greeter.

"Print 'Hello!'"
myGreeter sayHello.

"Print 'Hi there!'"
myGreeter sayDefaultGreeting.


"Print 'Bonjour!'"
myGreeter sayGreeting:'Bonjour!'.


"Save default greeting and print 'Good Morning!'"
defaultGreeting := myGreeter defaultGreeting.
myGreeter setDefaultGreeting:'Good Morning!'.
myGreeter sayDefaultGreeting.

"Print 'Hi there!'"
myGreeter sayGreeting:defaultGreeting.

Blocks supplied as method implementations must take :self as an explicit first parameter, in the same way that methods are written in Perl and Python. In the case of class methods, :self will be the class object, just as in Objective-C. All following parameters should correspond to the slots for arguments in the selector. In the above example, the sayGreeting: message only has a slot for one argument (as it has only one colon), but the block supplied for it must take two parameters. self is only the customary name for the receiver parameter; you may use any name you like.

The method onMessageName:do: can be used when you have a string that represents a selector's name. This method lets you defer not only the implementation, but the actual name of the method until runtime. The section Class Names and Anonymous Classes shows how this method can be used to create custom classes at runtime.

Properties

Instance data for F-Script objects are always hidden behind accessor/mutator methods. You create a property by adding it to an existing class object. Instances can then use the Cocoa-style methods propertyName and setPropertyName: to access it:

NewClass := FSClass newClass.

NewClass addProperty:'foobar'.

"Use array messaging to add multiple properties at once"
NewClass addProperty: @ { 'propA', 'propB', 'propC' }.


inst := NewClass alloc init.

inst setFoobar:5.

"Logs '5' to the console"
sys log:(inst foobar).

Objective-C 2.0 uses the term "property" to refer to an instance variable declared in a specific way, with special attributes and a new access syntax. Properties created in F-Script are different; creating a property for an F-Script class only allocates space in the object and creates the accessor/mutator method pair. There is no special syntax, and none of the Objective-C 2.0 attributes (such as copy or readonly) can be applied.

The methods addProperty:withDefault:, addPropertiesWithDefaults:, and addPropertiesFromDictionary: allow you to specify default values that will be automatically filled in by alloc. These default values must be immutable, as they will initially be shared between all instances of a class (and subclasses) until they are replaced. For more information, see the FSClass reference.

As with most scripting languages, there is no support for access control: all properties are public, which may be a problem when you want to have greater control over the behavior of method accessors and mutators, such as the readonly and copy attributes available on Objective-C 2.0 properties. In this case, you can create a 'private' instance property (typically prefixed with an underscore), and then create a pair of publicProperty / setPublicProperty: methods that access the 'private' property indirectly. The following example shows how to create a property that performs filtering in the set method. This design pattern has the added advantage that publicProp will work properly with Key-Value Coding:

PrivatePropertyClass := FSClass newClass:'PrivatePropertyClass'.

"Add a 'private' property that isn't part of the published interface"
PrivatePropertyClass addProperty:'_privateProperty'.
PrivatePropertyClass addProperty:'_copiedProperty'.
PrivatePropertyClass addProperty:'_readOnlyProperty'.


"Add a pair of accessor/mutator methods that perform validation"
"Acts like the @dynamic directive in Objective-C 2.0"
PrivatePropertyClass onMessage:#publicProperty do:[ :self |
    self _privateProperty
].
PrivatePropertyClass onMessage:#setPublicProperty: do:[ :self :newValue |
    "Throw an exception if the new value is not a number between 0 and 10"
    (newValue isKindOfClass:(NSNumber class)) ifFalse:[
        'publicProperty must be a number' throw.
    ].
    ((newValue < 0) | (newValue > 10)) ifTrue:[
        'publicProperty must be a number between 0 and 10 inclusive' throw.
    ].

    self set_privateProperty:newValue.
].


"This accessor-mutator pair will copy the value passed to the mutator"
"Acts like the (copy) attribute in Objective-C 2.0"
PrivatePropertyClass onMessage:#otherProperty do:[ :self |
    self _copiedProperty
].
PrivatePropertyClass onMessage:#setOtherProperty: do:[ :self :newValue |
    self set_copiedProperty:(newValue copyWithZone:nil).
].


"This logical property does not have a mutator"
"Acts like the (readonly) attribute in Objective-C 2.0)"
PrivatePropertyClass onMessage:#readOnlyProperty do:[ :self |
  self _readOnlyProperty
].

Fast Instance Properties

When you create a class in Objective-C, the instance variables are packed into a C-like structure that makes property access very fast. In 64-bit applications, the property access is slightly slower, but has the advantage that adding ivars to parent classes will not break child classes.

Objects of FSClass-created classes are different, in that they store all of their properties in a dictionary, similar to objects in other scripting languages like Perl and Ruby. This makes them much more flexible, as you can add properties to a class after it has been created. However, it introduces significant overhead to accessing the properties: every call to myObj foo requires the FSClass runtime to convert the message name to a string, look up that key in an NSDictionary, and possibly convert the value before returning it.

FSClass 2.1 introduced an optional solution to this problem with 'fast-ivar' classes. If you know the names of all instance properties at the time of class creation, you can specify them in the call to newClass:

FastIvarClass := FSClass newClass:'FastIvarClass' properties:{'propA', 'propB', 'propC'}.

myFastInstance := FastIvarClass alloc init.

myFastInstance setPropA:5.

Instances of FastIvarClass will have their member variables stored directly inside the object, just like Objective-C objects. The accessor methods for these properties will look directly in the object for the values; this reduces the access and mutation overhead to a fraction of their normal costs. Otherwise, instances of FastIvarClass will work exactly as instances of regular FSClasses.

Default values can be added after class creation using the method setDefaultValue:forProperty:. Alternately, property names and defaults can be specified at class creation by passing a dictionary to the method FSClass newClass:name propertiesWithDefaults:propsDictionary. Attempting to add properties to a fast-ivar class with the methods -addProperty: or -addProperty:withDefault: will throw exceptions.

Using fast-ivar classes has an additional debugging benefit: fast ivars will show up in tools that list member variables, like object browsers or XCode's gdb interface.

Regular and fast-ivar classes can be mixed through inheritance: if a fast-ivar class inherits from a regular class, the parent class's properties will stay in the dictionary, but the child's properties will be accessed directly.

Key-Value Coding

FSClass properties are Key-Value Coding compliant, so they can also be accessed with the indirect KVC methods:

KVTestClassA := FSClass newClass:'KVTestClassA'.
KVTestClassA addProperty:'propA'.

KVTestClassB := FSClass newClass:'KVTestClassB'.
KVTestClassB addProperty:'propB'.

kvInstance := KVTestClassA alloc init.

"These two lines are equivalent:"
kvInstance setPropA:5.
kvInstance setValue:5 forKey:'propA'.

"These two lines are equivalent:"
sys log:(kvInstance propA).
sys log:(kvInstance valueForKey:'propA').



"We can also do key paths"
subValue := KVTestClassB alloc init.

kvInstance setPropA:subValue.

"These two lines are equivalent:"
kvInstance propA setPropB:10.
kvInstance setValue:10 forKeyPath:'propA.propB'.

"These two lines are equivalent:"
sys log:(kvInstance propA propB).
sys log:(kvInstance valueForKeyPath:'propA.propB').

"This line will result in a runtime violation because the method is not defined:"
[ kvInstance setFoo:'a string' ]
onException:[ :e |
    sys log:('Caught an exception: ' ++ e description).
].

"This line will result in a call to valueForUndefinedKey:,
    which will then throw an NSUnknownKeyException"
[ kvInstance setValue:'a string' forKey:'foo' ]
onException:[ :e |
    sys log:('Caught an exception: ' ++ e description).
].

To maintain Key-Value Coding compliance, property names should follow the KVC guidelines. Other forms of key-value codings - such as one-to many relationships - can be implemented by defining the proper methods, as in Objective-C. See Apple's Key-Value Coding Programming Guide for more details.

Constructors

As shown above, once a new class has been created, instances should be created by calling factory methods:

ExampleClass := FSClass newClass:'MyClass'.

"add properties and methods to ExampleClass"
"..."

ExampleClass onClassMessage:#newExample do:[ :self | |instance|
    "Physically instantiate the object using regular alloc init"
    "FSClass 3.0 uses garbage collection, so autorelease is no longer necessary"
    instance := self alloc init.

    "Perform any necessary setup"
    "..."

    "Return the new instance"
    instance
].


"Create a new instance"
myInstance := ExampleClass newExample.

To create a new object, use the standard Cocoa methods alloc init. FSClass 3.0 uses the garbage collection system introduced in Leopard and supported by F-Script 2.0, so autorelease is no longer necessary.

If you are implementing a subclass, instantiation becomes a little bit more complicated; see the sections Inheritance and Calling Superclass Methods for more information.

Classes can have multiple factory methods that use some default and some specified property values. Here are example default and specific factories written in F-Script:

ExampleClass := FSClass newClass:'ExampleClass'.

"Set the default name and property as class data"
ExampleClass addClassProperty:'defaultName' withValue:'default name'.
ExampleClass addClassProperty:'defaultPropertyOne' withValue:5.

"Instance variables"
ExampleClass addProperty:'propertyOne'.
ExampleClass addProperty:'name'.

"Default factory method