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"
ExampleClass onClassMessage:#example do:[ :self | |instance|
    "Set up initial property values"
    instance := self alloc init.
    instance setName:(self defaultName).
    instance setPropertyOne:(self defaultPropertyOne).
    instance
].

"Partially specified, partially default factory method"
ExampleClass onMessage:#exampleWithName: do:[ :self :name | |instance|
    instance := self alloc init.
    instance setName:name.
    instance setPropertyOne:(self defaultPropertyOne).
    instance
].

"Fully-specified factory method"
ExampleClass onMessage:#exampleWithName:propertyOne: do:[ :self :name :propertyOne | |instance|
    instance := self alloc init.
    instance setName:name.
    instance setPropertyOne:propertyOne.
    instance
].

Inheritance

Subclasses are created with the method newClass:parent:, which takes an as its argument an FSClass object or an Objective-C class object like NSMutableDictionary. You can also use subclass: to get a subclass directly from the parent. All methods (and, in the case of FSClass parents, properties with default values) are inherited from the superclass. To override a method implemented by a superclass, simply call onMessage:do: with the same selector and a different block:

Car := FSClass newClass:'Car'.

Car addProperty:'speed' withDefault:0.

Car onClassMessage:#car do:[ :self |
    "The class's only property has a default, so we can just return a new instance"
    "+alloc will take care of setting the property defaults; -init will call the NSObject init method"
    self alloc init.
].

Car onMessage:#drive do:[ :self |
    set setSpeed:50.
].

Car onMessage:#park do:[ :self |
    self setSpeed:0.
].

"Create a faster subclass of Car"
SportsCar := FSClass newClass:'SportsCar' parent:Car.

"Equivalent command to create a subclass would be:
    SportsCar := Car subclass:'SportsCar'.
"

SportsCar onClassMessage:#sportsCar do:[ :self |
    self alloc init
].


"Override drive to go faster"
SportsCar onMessage:#drive do:[ :self |
    self setSpeed:80.
].

"No need to override park"

Properties are also inherited by subclasses. Unlike with compiled classes like Java or C++, classes at different levels of an inheritance hierarchy cannot have properties with the same name. This is because all properties in F-Script are accessed through methods, and all methods are inherited. If you try to add a property to a subclass that conflicts with a property already declared in a superclass, it will throw an exception; the reverse is not true. In short, if class Child with property bar inherits from Parent with property foo, Parent addProperty:'bar' will work, but Child addProperty:'foo' will not.

An important point is that methods and properties can be added to a class at any time, and all subclasses will immediately inherit them (with the exception that fast-ivar classes cannot add new properties after class creation). Existing instances of the class or subclass will also be able to use the methods immediately. Continuing the example above:

ferrari := SportsCar sportsCar.

"Will throw an exception - refuel: isn't defined"
"ferrari refuel:15."

"We can add a new property and new method here"
Car addProperty:'fuel' withDefault:0.
Car onMessage:#refuel: do:[ :self :gallons |
    self setFuel:(self fuel + gallons).
].

"Now it is okay to refuel"
ferrari refuel:15.

"Will print '15'"
sys log:(ferrari fuel).

The default value will be propagated to existing instances on first access. Likewise, if onMessage:do: is used on a superclass to overwrite an existing method, all instances of that class (and subclasses) will automatically switch over to the new behavior.

Objective-C classes can be also be used as parents. For example, the FSClass bundle includes TestBaseClass, which can be used to demonstrate inheritance from a compiled base class:

"TestBaseClass is a simple class included in the FSClass bundle"
ClassM := FSClass newClass:'ClassM' parent:TestBaseClass.
ClassM addProperty:'blort'.
ClassM onMessage:#init do:[ :self | |newSelf|
    self doSuperMethod:#init currentClass:ClassM; setStr:'Hello world!'; setInteger:8; setBlort:'blort!'.
    self.
].
ClassM onMessage:#borkedString do:[ :self |
    'bork! ' ++ self str
].

str is a property inherited from TestBaseClass. Because all access to properties in F-Script is through methods, a superclass property that does not have an accessor method will not be useable from F-Script subclasses.

doSuperMethod:currentClass: is a special method used for accessing superclass methods; it works with Objective-C parents just as with FSClass parents. See the next section for more details, with an important note about using superclass -init methods that take primitive types as parameters.

Note that many Foundation Kit classes, such as NSString, are actually abstract fronts to class clusters, so direct inheritance from these classes will not work. To have the same effect, you can use Apple's recommended approach for "inheriting" from class clusters like NSString: create an Objective-C object as a member variable, and use wrapper methods that redirect appropriate messages to it (an example can be seen in the source code for the FSFile class in the fscript command-line interpreter). If you only want to add methods to the class, use a category instead.

Calling Superclass Methods

F-Script 2.0 introduces a reserved super keyword, but FSClass needs additional context that this keyword does not provide. Particularly, the super-class method invocation needs to know the class of the current Block. This information, which is also needed in languages like Objective-C, Java, and C++, is normally provided by the compiler. Because the F-Script interpreter does not know about FSClass, you must supply this context manually, as the second argument to the method doSuperMethod:currentClass:. The first argument should be the selector of the method you want to call, and any arguments can be passed using the keywords with:, similar to the syntax for executing blocks.

Up to this point, all of our class factories have performed initialization inside the factory method, which prevents using superclass initialization methods. A solution is to separate instantiation and initialization by creating a private _init method that is called from the class factory to initialize the object. This init method can then call the superclass init method, and so on up the class hierarchy. The superclass initialization method is sometimes called the designated initializer.

It is very important that the top-level initializer code use the Cocoa method init, to ensure that the object is properly registered with the Cocoa runtime system.

The following example shows how to create a subclass and call superclass methods, including initialization methods:

SuperClass := FSClass newClass:'SuperClass'.

SuperClass addProperty:'foo'.

"The initializer is private, hence the underscore.
 This method is the *designated initializer* for SuperClass"
SuperClass onMessage:#_initWithFoo: do:[ :self :fooValue |
    self init.  "Cocoa's init method should be called first"
    self setFoo:fooValue.
    self
].

"Only this class method should be called by users"
SuperClass onClassMessage:#superObjectWithFoo: do:[ :self :fooValue |
    "Actual initialization is handled in the _init method"
    self alloc _initWithFoo:fooValue
].

SuperClass onMessage:#doThing do:[ :self |
    sys log:'Hi from SuperClass! foo:' ++ (self foo description).
].

SuperClass onMessage:#doStuff do:[ :self |
    sys log:'Bonjour from SuperClass! foo:' ++ (self foo description).
].


"Create a subclass"
SubClass := FSClass newClass:'SubClass' withParent:SuperClass.

SubClass addProperty:'bar'.


"This initializer calls the superclass's init: method"
SubClass onMessage:#_initWithBar: do:[ :self :barValue |
    "Call the designated initializer with a default foo value"
    "We have to specify which class this Block is attached to"
    "  so that the runtime has the proper context"
    self := self doSuperMethod:#_initWithFoo: with:5.
    self setBar:barValue.
    self
].

"Only this class factory method should be called by users"
SubClass onClassMessage:#subObjectWithBar: do:[ :self :barValue |
    "Actual initialization is handled in the _init method"
    self alloc init _initWithBar:barValue
].


"Execute superclass's doThing first"
SubClass onMessage:#doThing do:[ :self |
    self doSuperMethod:#doThing currentClass:SubClass
    sys log:'Hi from SubClass! bar:' ++ (self bar description)
].


sub := SubClass subObjectWithBar:10.

" Prints 'Bonjour from SuperClass! foo:5"
sub doStuff.

"Prints 'Hi from SuperClass! foo:5' 'Hi from SubClass! bar:10'"
sub doThing.

This syntax for using superclass methods has changed from earlier versions of FSClass. The old system (using the method super:) did not work properly and conflicts with a new reserved keyword. It has been completely removed.

For more information on using factory methods, initializers, and inheritance, see Apple's text The Objective-C Programming Language.

If your class inherits from an Objective-C parent, you can use doSuperMethod:currentClass: to call methods of the parent class's initializer, as well as other overridden methods. There is one critical caveat as of FSClass 2.1: objects will not be translated to primitive types by the F-Script runtime, as they will with normal methods. E.g., an instance of NSNumber will not be converted into an int; this effectively makes superclass methods with primitive type arguments impossible to use. This limitation will be fixed in a future release; at the moment, it may be avoided by using the simple method init and then using property setters, if available, to perform the object-primitive conversion.

Up to six arguments can be passed with the doSuperMethod:currentClass:with:with:... syntax. For more arguments, use the method doSuperMethod:currentClass:valueWithArguments:, similar to that for blocks:

obj doSuperMethod:#noArgMethod                       currentClass:MyClass.
obj doSuperMethod:#oneArgMethod:                     currentClass:MyClass with:1.
obj doSuperMethod:#twoArgMethod:arg:                 currentClass:MyClass with:1 with:2.
obj doSuperMethod:#threeArgMethod:arg:arg:           currentClass:MyClass with:1 with:2 with:3.
obj doSuperMethod:#fourArgMethod:arg:arg:arg:        currentClass:MyClass with:1 with:2 with:3 with:4.
obj doSuperMethod:#fiveArgMethod:arg:arg:arg:arg:    currentClass:MyClass with:1 with:2 with:3 with:4 with:5.
obj doSuperMethod:#sixArgMethod:arg:arg:arg:arg:arg: currentClass:MyClass with:1 with:2 with:3 with:4 with:5 with:6.

obj doSuperMethod:#twoArgMethod:arg: currentClass:MyClass withArguments:{ 1, 2 }.
obj doSuperMethod:#eightArgMethod:arg:arg:arg:arg:arg:arg:arg: currentClass:MyClass withArguments:{ 1, 2, 3, 4, 5, 6, 7, 8 }.

If you supply the wrong number of arguments to doSuperMethod:currentClass, or call it with a selector that the object's superclass does not implement, it will throw an exception.

Class Clusters

In Objective-C, a factory method can actually return an object of a derived class. NSString use this ability to return instances of private subclasses that are optimized for different cases: for example, using different storage layouts when initializing with UTF-8 data versus ASCII. This design pattern is called a class cluster, and is used heavily throughout the Foundation and Application Kit frameworks.

Classes created with FSClass can also implement private class clusters. In the following example, the factory method for Rectangle actually returns a Square in the case that the rectangle has sides of equal length:

Rectangle := FSClass newClass:'Rectangle'.

Rectangle addProperty:'length'.
Rectangle addProperty:'height'.

Rectangle onMessage:#area do:[ :self |
    (self height) * (self length)
].

Rectangle onMessage:#perimeter do:[ :self |
    (self height * 2) + (self length * 2).
].

"Compute angle of lower-left diagonal"
Rectangle onMessage:#lowerLeftDiagonalAngle do:[ :self |
    (self height / self length) arcTan.
].


"Subclass of Rectangle that has specialized geometry methods"
Square := FSClass newClass:'Square' parent:Rectangle.

"Angle of a square's diagonal is always pi / 4"
Square addClassProperty:'rightAngle' withValue:(3.14159 / 4).

Square onClassMessage:#squareWithSide: do:[ :self :sideLength | |square|
    "Set size and width so that accessors will work as expected"
    square := self alloc init.
    square setLength:sideLength.
    square setHeight:sideLength.
    square
].

Square onMessage:#area do:[ :self |
    self length raisedTo:2
].

Square onMessage:#perimeter do:[ :self |
    self length * 4
].

"Return hard-coded 90degree angle"
Square onMessage:#lowerLeftDiagonalAngle do:[ :self |
    Square rightAngle
].



"If the user creates a rectangle with equal sides, secretly
 return a Square instead"
Rectangle onClassMessage:#rectangleWithLength:height: do:[ :self :length :height |
    (length = height) ifTrue:[
        "Return a Square instead"
        Square squareWithSide:length
    ]
    ifFalse:[ |rect|
        rect := self alloc init.
        rect setLength:length.
        rect setHeight:height.
        rect
    ]
].


"This line creates a Rectangle"
rect1 := Rectangle rectangleWithLength:4 height:5.
sys log:rect1.

"This line actually creates a Square"
rect2 := Rectangle rectangleWithLength:4 height:4.
sys log:rect2.

Class Names and Anonymous Classes

When creating a class with FSClass newClass:, you can specify the name that will be used for the class by the Objective-C runtime. This cannot conflict with the name of any existing class, so code like NewClass := FSClass newClass:'NSObject' will throw an exception because NSObject is, of course, the name of an existing class. If you know the name of a class that was created with FSClass, you can retrieve it by using previouslyCreatedClass := FSClass getClass:'ClassName'. If the string is the name of an Objective-C class, FSClass will return a proxy for it (see Objective-C Class Proxies and Categories).

If you omit a name (by either supplying nil or using the argument-less newClass method), the new class is given a random, unique name. If you lose the variable to which the class was originally bound, you won't be able to get that class back. A similar concept in Java is the anonymous inner class, which implements an interface but has no name of its own:

public class MyOptionPane extends JOptionPane {
  // ... button member declarations ...
    protected void buildOptionPane() {
      button1 = new JButton();
      button2 = new JButton();
        
      // Create a single instance of an anonymous event listener
        button1.addActionListener(
          new java.awt.event.ActionListener() {
                public void actionPerformed(java.awt.event.ActionEvent e) {
                  // do something
                }
          }
      );
      
      // .. repeat for each button
    }
}

In languages like Objective-C and F-Script, the dynamic method dispatching system obviates much of the need for such constructions. However, anonymous classes can be useful for other purposes. Say that we have a configuration file in simple key=value format, one pair per line. We would like to read in this file at the beginning of our program and put it in a dictionary. However, constantly calling dict objectForKey:'keyName' is cumbersome; wouldn't it be better to make a single object that had a separate method for each key, so that we could directly call config keyName instead? The file below exports a single function that does just that: it creates a new, anonymous class with one method for each configuration key, and uses closures to bind that method to the key's value. Because we don't know the key names ahead of time, we use onMessageName:do: to supply the name of a selector instead of the selector itself. The function then returns a single instance of the class. This example uses the FSFile class from the fscript command-line program.

"
    ConfigFile.fs
    This function reads in a configuration file in simple 'key=value' format,
    then creates a singleton instance with keys as methods
"

ConfigFile := FSClass newClass:'ConfigFile'.


ConfigFile onClassMessage:#readFile: do:[ :self :filename | |file data newClass|
  "Read in the lines of the file and split them into pairs"
  file := FSFile open:filename mode:'<'.
  data := file readlines split:' *= *'.
  file closeFile.
  
  "Create a new, anonymous class"
  newClass := FSClass newClass.
  
  "For each config line in the file, add a new method to
   the class that returns the corresponding value"
  "We could use addProperty: for this, but we want them to
   be read-only"
  "The values will always be strings, so it is up to the
   user to convert them into booleans/numbers/dates/etc."
  [ :fileLine |
        newClass onMessageName:(fileLine at:0) do:[
            fileLine at:1
      ].
  ]
  value: @ data.
  
  "return an instance of the anonymous class"
  newClass alloc init
].

Because the class was never given a name, we have no way to retrieve it, so there can only be a single instance (the one returned from the function). We can use this file like so (assuming that we have sys import the from the command-line fscript program):

#!/usr/bin/fscript

sys import:'ConfigFile'.

config := ConfigFile readFile:'normal.conf'.

((config verbose) = 'true') ifTrue:[
    ....
].

sys log:(config greeting).

Note that we must manually check the truth value of verbose, since F-Script does not perform automatic type conversion. For another approach (valid only with property list files), see the reference entry for addPropertiesFromDictionary:.

The class object of an instance can also be retrieved with the Foundation getClass method:

"Create an instance of a blank, anonymous class"
myInstance := FSClass newClass alloc init.

"This line will throw an exception because foo: is not defined for this class"
[ myInstance foo:5 ]
onException:[ :e |
    sys log:('Caught an exception: ' ++ e description).
].

"Retrieve the class and add another method to it"
(myInstance class) onMessage:#foo: do:[ :self :fooValue |
    sys log:'Foo value: ' ++ (fooValue description).
].

"This line will now run without any problems"
"Prints 'Foo value: 5'"
myInstance foo:5.

Class Methods and Data

Besides factory methods, classes can also have functions and data attached directly to them. This example shows how to create a counter that keeps track of how many instances have been created:

Car := FSClass newClass:'Car'.

Car addProperty:'type'.

"Counter - would be a static variable in C++/Java"
Car addClassProperty:'count' withValue:0.
Car onClassMessage:#createdCarCount do:[ :class |
    class count
].

Car onClassMessage:#carWithModel: do:[ :self :type | |car|
    Car setCount:(Car count + 1).
    car := self alloc init.
    car setType:type.
    car
].

car1 := Car carWithModel:'sedan'.
car2 := Car carWithModel:'SUV'.

"Prints '2 cars have been created'."
sys log:(Car createdCarCount description) ++ ' cars have been created'.

Like instance properties or methods, class properties and methods are inherited by subclasses.

Categories in F-Script

One of the most useful language features of Objective-C is categories: the ability to add methods to a class piecemeal, after the class has been declared, even after it has been compiled! In FSClass 3.0, all classes, including those written in Objective-C, have the same method-addition abilities. This includes Cocoa classes for which the source code is not available:

"This code uses the regular expression method -replace:with: from the fscript Unix tool."
NSString onMessage:#bork do:[ :self |
    ((self replace:'W' with:'V') replace:'w' with:'v') replace:'o' with:'oo'
].

"Prints 'Helloo voorld!'"
out println:('Hello world!' bork).

The method -bork will now be available to all instances of NSString, even through the NSString source code is not available, and is in a different language.

Objective-C classes have all the introspection and modification methods as those created in F-Script, with the exception that you cannot add instance variables; calling +addProperty: or +addProperty:withDefault: will throw exceptions.

The ability to add arbitrary methods to Foundation classes in F-Script is extremely powerful, especially when combined with F-Script's flexible syntax. For example, many languages like LISP, Ruby, and PHP have a 'pairing operator', which takes two objects and returns a simple object that pairs them together. With a class proxy, we can add that capability to NSObject and make it available to every single object in the Cocoa codesphere:

"Create a simple Pair class"
Pair := FSClass newClass:'Pair' properties:{'first', 'second'}.
Pair onClassMessage:#pair:with: do:[ :self :first :second | |newPair|
    newPair := self alloc init.
    newPair setFirst:first; setSecond:second.
    newPair
].
"Simple description: (obj1, objc2)"
Pair onMessage:#description do:[ :self |
    '(' ++ self first description ++ ', ' ++ self second description ++ ')'
].

"Add a pairing operator to NSObject, and thus to all classes"
NSObject onMessage:#operator_equal_greater: do:[ :self :second |
    Pair pair:self with:second.
].


"Now we can pair any two objects together"
myPair := 1 => 5.

"Prints '(1, 5)'"
sys log:myPair description.

"Like Foundation collection objects, Pairs are heterogeneous"
myMixedPair := 'hello!' => 5.

(Note that, due to F-Script's strict left-to-right precedence rules, the pairing arrow cannot be chained to create a LISP-style linked list).

Proxies can make Foundation classes much more convenient to use. One problem with Objective-C is that method names tend to be verbose; a simple dictionary lookup/set in most languages is only a few characters, but requires the long methods -objectForKey: and -setObject:forKey: in Cocoa. This makes dictionary-heavy Cocoa code much more cumbersome than equivalent code in Perl or Python. The following example shows how we can use F-Script categories to add abbreviations to NSDictionary, including a subscript operator and a set operator that takes a Pair:

"Add a C++ style subscript operator - will be inherited by NSMutableDictionary"
NSDictionary onMessage:#operator_hyphen_greater: do:[ :self :key |
    self objectForKey:key
].

"Add a set: method that takes a pair"
NSMutableDictionary onMessage:#set: do:[ :self :pair |
    self setObject:(pair second) forKey:(pair first).
].


"Now using a dictionary is much easier"
dict := NSMutableDictionary dictionary.

dict set: 'keyA'=>6.
dict set: 'keyB'=>'hello'.

sys log:'keyA: ' ++ (dict->'keyA') description.
sys log:'keyB: ' ++ (dict->'keyB') description.

For a longer example, see the file examples/Switch.fs in the FSClass sources directory. This file shows how a switch statement can be added to the F-Script language, solely by adding methods to existing Foundation classes.

Reflection Methods

FSClass classes are fully reflective. The following methods provide information about methods, classes, and inheritance relationships:

For more information, see the details in the FSClass reference page.

Using FSClass classes in Objective-C

Because classes created with FSClass are full participants in the Cocoa runtime system, they can be used from pre-compiled Objective-C code, although a little bit of indirection is needed. Say that we have created a FSClass named Foobarizer. The following code will get a reference to the class and create a new instance of it:

// We can't actually use the class name 'Foobarizer' because
// the compiler will complain that it is undefined
Class FoobarizerClass = NSClassFromString(@"Foobarizer");

// Create an instance - we have to pass an object as argument, not a plain int
id fooInstance = [FoobarizerClass foobarizerWithFoo:[NSNumber numberWithInt:10]];

// this line will produce a compiler warning that -foobarize: is undefined
id result = [fooInstance foobarize:@"a string"];

// we can also run methods indirectly
[fooInstance performSelector:@sel(setFoo:) withObject:[NSNumber numberWithInt:27]];

If you use FSClass objects inside Objective-C, remember that all FSClass method parameters are objects. You will have to wrap numbers and other primitives inside NSNumber or other classes if you wish to use them as arguments; the automatic conversion of the F-Script runtime is not available.

Also note that XCode will likely complain that it cannot find declarations of the methods that you use with the FSClass-derived class, unless they happen to already be implemented by another class. These warnings are annoying, but will not affect the functioning of the program (of course, if you did misspell a method name, your program will crash at runtime!) In the warning message, XCode will also say it is assuming that the return and argument types are all id, which in this case is accurate.

Reference

A complete reference to all FSClass methods is available here.

Download

Download the source code and compiled framework for FSClass here.

Legal and Miscellany

The FSClass bundle is copyright 2007-2008 Andrew Weinrich, and is released under the GNU GPL version 2. The full text of the license can be found in the file COPYING, included in the source download.

Thanks to Phillipe Mougin for creating the F-Script language and offering advice and assistance. Thanks also to everyone in the F-Script community who reported bugs in earlier versions.

If you find FSClass useful, you may also be interested in the fscript Unix tool, a command-line utility that runs F-Script programs and incorporates some useful additions for general-purpose scripting tasks. In particular, it has support for libraries that can be written with FSClass instead of Objective-C.

If you write a lot of F-Script code, you may find the experimental tool flint to be useful. flint is a type-checker and bug-finder for F-Script. It can find type problems, incorrect method names, use of uninitialized variables, and other problems in F-Script programs.

Known Bugs

None currently known.

Revision History

2008-01-15
Version 3.0
  • Now requires Mac OS X 10.5 Leopard; for earlier versions, use FSClass 2.1
  • Now requires F-Script 2.0; for earlier versions, use FSClass 2.1
  • Now compatible with 64-bit PowerPC and x86
  • Makes mandatory use of garbage collection
  • Changed the API for superclass method invocation to fix bugs and accommodate the new super keyword
  • Brand new internal architecture
  • Class methods and properties are now correctly inherited
  • Faster dispatching of class methods and class property access
  • Proxies for Objective-C classes no longer need to be created; the class objects may be used directly
  • Fixed problems with using fast-ivars classes in the fscript Unix interpreter
  • The class method newInstance is now deprecated; use alloc init instead
  • The methods parentClass, subClasses, className, and description have been removed in favor of the equivalent Foundation methods
2007-09-20
Version 2.1
  • Fixed a crash when using FSClass in an interactive interpreter
2007-08-16
Version 2.0
2007-02-07
Version 1.1
2007-01-18
Version 1.0