Revised April 2010
F-Script provides a handy syntax for dynamically creating Cocoa classes on the fly. This is useful for quickly experimenting, prototyping and using Cocoa interactively, as well as for developing sophisticated programs. You can type a class definition in the F-script console, hit return and immediately start playing with your new class. This article provides a quick introduction to this feature.
In order to define a class, we must give it a name and specify a superclass. Here is an example where we define Buddy as a subclass of NSObject. You can type this code in F-Script and get the Buddy class dynamically created:
Buddy : NSObject {}
As is, this definition is very simple, but not very useful: our new class doesn't define any instance variable or method (though it inherits some from NSObject, its superclass).
Fortunately, we can dynamically redefine our class. This time, we will specify some instance variables (say firstName and lastName) along with an initialization method and a description method:
Buddy : NSObject
{
firstName lastName
- initWithFirstName:first lastName:last
{
self := super init.
self ~~ nil ifTrue:
[
firstName := first.
lastName := last
].
^ self
}
- description
{
^ 'Hello, I am your buddy ' ++ firstName ++ ' ' ++ lastName
}
}
The class definition contains the list of instance variables, followed by method definitions. self and super have the same meaning as in Objective-C and Smalltalk. The caret (^) is the return instruction.
Let's play with our class. We will instantiate it and assign the newly created instance to a variable that we will then evaluate. The interactive session in the F-Script console looks like this:
> john := Buddy alloc initWithFirstName:'John' lastName:'Doe'
> john
Hello, I am your buddy John Doe
As in Objective-C, the name of an instance methods is preceded by a minus sign and the name of a class methods is marked with a plus sign. For example we can add the following class method to our Buddy class:
+ buddyWithFirstName:first lastName:last
{
^ self alloc initWithFirstName:first lastName:last
}
We can then use it as follows:
> mary := Buddy buddyWithFirstName:'Mary' lastName:'Doe'
> mary
Hello, I am your buddy Mary Doe
Local variables in methods are defined enclosed by vertical bars and separated by spaces. For example, |foo bar| defines two local variables named foo and bar. Such variables are automatically initialized to nil. We can rewrite our description method to show the use of a temporary local variable:
- description
{
|fullName|
fullName := firstName ++ ' ' ++ lastName.
^ 'Hello, I am your buddy ' ++ fullName
}
Since our newly defined class is automatically registered in the Cocoa runtime, we can use it with all our standard tools. For instance, entering sys browse:john in F-Script will open the graphical object browser on the instance we created earlier.
Figure1. Browsing an instance of the Buddy class.
As usual, class methods will appear when browsing class objects (e.g. typing sys browse:Buddy).
Figure 2. Browsing the Buddy class object.
Sometimes, we don't want to define or redefine a class from scratch, but just incrementally add methods to an existing class. To do that, we just have to specify the name of the class followed by an opening brace, the list of methods and a closing brace. For example, here is how we add a print method to the NSObject class.
NSObject
{
- (void)print
{
stdout print:self description
}
}
If a method being added already exists in the class, it will be replaced by the new method.
In addition to instance variables, we can also define class instance variables. Class instance variables are to classes what instance variables are to instances (breathe calmly and read this sentence again). They represent private data associated with class objects. They can only be accessed in class methods. The defining class and each subclass get their own storage.
To define a class instance variable, we put the annotation "(class instance variable)" after its name. In the following example, we use a class instance variable to define a class that keeps track of how many times it has been sent an alloc message:
MyClass : NSObject
{
"Define the class instance variable that will keep track of allocations"
allocationCount (class instance variable)
"The +initialize method is handy to initialize class instance variables"
+ (void)initialize
{
allocationCount := 0
}
"Perform the allocation"
+ alloc
{
allocationCount := allocationCount + 1.
^ super alloc
}
"Return how many times the class has been sent an alloc message"
+ allocationCount
{
^ allocationCount
}
}
When we define a method, it gets automatically registered in the Objective-C runtime. Indeed, from the point of view of the runtime, the new method is just like any other Objective-C method. Among other things, this means that it can be invoked from Objective-C code.
This also manifests in the syntax of F-Script itself. From the outside, nothing looks more like an Objective-C method than a F-Script method:
Objective-C method
- (float) doSomethingWithFoo:(int)x bar:(Bar *)y
{
... Objective-C code ...
}
F-Script method
- (float) doSomethingWithFoo:(int)x bar:(Bar *)y
{
... F-Script code ...
}
In the example above, we make use of explicit typing in the method signature. This is particularly useful when we want to hand out a F-Script object to some Objective-C code that call us back by invoking a method taking or returning non-object values. Indeed, the method we define must know what are the types of its arguments and return value in order to handle them correctly when invoked.
Of course, since F-Script is a pure object language, actual values passed as arguments are automatically mapped to objects, and the returned object is automatically mapped to the data type promised to the caller by the signature. The F-Script code inside our methods only has to deal with objects.
Here is a list of types currently supported:
We can also use a class name followed by a
*, as in Objective-C (e.g. NSString *). To specify pointers we can put as many * as needed after a type (e.g. unsigned int **). Finally, we use void to indicate that a method returns nothing.
By default, in the absence of an explicit type, id is assumed, as in Objective-C.
If you dynamically replace an existing method by a new one (by redefining a class or by using a category), the signature of the new method must be compatible with the signature of the existing one (an exception signaling a programming error will be raised if this is not the case). That is, they must have the same return type and argument types. Note that all object types are considered similar; for example, an argument declared as an NSString * can be declared as an NSNumber * in the new signature.
We define a Rectangle class. A rectangle can be initialized with a color and asked to draw itself. This example demonstrates subclassing an Objective-C class (here, NSObject), defining an instance variable (color), a class method and two instance methods. This dynamically creates a native Cocoa class that can then be used from F-Script and Objective-C.
Rectangle : NSObject
{
color
+ rectangleWithColor:aColor
{
^ self alloc initWithColor:aColor
}
- (void)draw
{
color set.
(NSBezierPath bezierPathWithRect:(400<>100 extent:100<>100)) fill
}
- initWithColor:aColor
{
self := super init.
self ~~ nil ifTrue:
[
color := aColor
].
^ self
}
}
We can then instantiate a rectangle and draw it:
r := Rectangle rectangleWithColor:NSColor magentaColor.
r draw
Here is what it looks like in the F-Script console:
Figure 3. Defining and using the Rectangle class.
The following program illustrates subclassing the NSView class. It displays a circle that can generate psychedelic effects by displaying flashes of colors. Here is a screenshot of the application:
Figure 4. A PsychedelicCircleView in action.
The main component of the program is PsychedelicCircleView, a subclass of NSView. This graphical component takes care of displaying the animated circle and handles mouse events, allowing the user to start and stop the psychedelic process. To that end, it implements the standards drawRect: and mouseDown: methods.
Below is the code for our NSView's subclass. To execute it, all you have to do is to copy/paste it in the F-Script console. Then copy/paste the code in charge of putting the component on screen (provided further below).
"Define the PsychedelicCircleView class, a subclass of Cocoa's NSView"
PsychedelicCircleView : NSView
{
"Instance variables"
timer "A NSTimer object used to animate the view"
message "The message to display"
attributes "A dictionary holding drawing attributes"
"Define the designated initializer"
- initWithFrame:(NSRect)frame
{
self := super initWithFrame:frame.
self ~~ nil ifTrue:
[
|font| "A local variable to hold the font object"
"Determine the font to use. We use Synchro LET if available, the default user font otherwise"
font := NSFont fontWithName:'Synchro LET' size:40.
font == nil ifTrue:[ font := NSFont userFontOfSize:40 ].
"Initialize the attributes dictionary used to draw the message"
attributes := #{NSFontAttributeName -> font}.
"Initialize the message to display"
message := ' Click to start/stop\nexpansion of consciousness'.
].
^ self
}
"Define the method invoked by Cocoa to draw the view"
- (void) drawRect:(NSRect)aRect
{
"Define local variables"
|red green blue size x y|
"Generate random values for color components"
red := 10 random / 9.
green := 10 random / 9.
blue := 10 random / 9.
"Set the color and draw the circle"
(NSColor colorWithCalibratedRed:red green:green blue:blue alpha:1) set.
(NSBezierPath bezierPathWithOvalInRect:self bounds) fill.
"If the psychedelic mode is not active, draw a message"
timer == nil ifTrue:
[
"Compute coordinates of the message in order to have it centered"
size := message sizeWithAttributes:attributes.
x := self bounds extent x / 2 - (size width / 2).
y := self bounds extent y / 2 - (size height / 2).
"Draw the message"
message drawAtPoint:x<>y withAttributes:attributes.
].
}
"Define the method invoked by Cocoa when the view is clicked"
- (void) mouseDown:(NSEvent *)theEvent
{
timer == nil ifTrue:
[
"Create a NSTimer object to put the psychedelic process in motion"
timer := NSTimer scheduledTimerWithTimeInterval:0.01 target:[self setNeedsDisplay:YES] selector:#value userInfo:nil repeats:YES
]
ifFalse:
[
"Stop the psychedelic process"
timer invalidate.
timer := nil.
self setNeedsDisplay:YES.
]
}
}.
Now that our view class is defined, we can put it in a window and play with it:
"Instantiate and configure a window"
window := NSWindow alloc initWithContentRect:(0<>0 extent:700<>700)
styleMask:NSTitledWindowMask + NSClosableWindowMask + NSMiniaturizableWindowMask + NSResizableWindowMask
backing:NSBackingStoreBuffered
defer:NO.
window setBackgroundColor:NSColor blackColor; setReleasedWhenClosed:NO; setTitle:'Psychedelic Circle'; center.
"Instantiate and configure a psychedelic circle"
circle := PsychedelicCircleView alloc initWithFrame:window contentView bounds.
circle setAutoresizingMask:NSViewWidthSizable + NSViewHeightSizable.
"Put the circle view in the window"
window contentView addSubview:circle.
"Put the window onscreen"
window makeKeyAndOrderFront:nil.
The code above should open a window and display the psychedelic circle.
This is a good opportunity to experiment with live class redefinition. While the psychedelic circle is flashing, you can modify the program and see your modifications take effect immediately. For example, in the drawRect: method, replace the call to bezierPathWithOvalInRect: by a call to bezierPathWithRect:. Now, if you look at the flashing circle, you'll notice that it has turned into a flashing rectangle.
Note that, at the time of this writing, all the users of this program (i.e. me and my cat) are not experiencing an expansion of their consciousness by looking at the psychedelic circle. In fact, 50% of the users declare that the only thing this program expands is their headaches. The other 50% try compulsively to catch flies while producing strange sounds, just as usual.
Copyright © 2009-2010 Philippe Mougin