cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Check out the JMP® Marketplace featured Capability Explorer add-in
Choose Language Hide Translation Bar
Craige_Hales
Super User
How to use Define Class

The following is mostly right; I may have missed something or misunderstood something; read the linked doc (it is too short) and fire away in the comments - thanks. 

 

Bad analogy: a class is a cookie cutter. An instance is a cookie made from the cookie cutter. 

 

Problem with analogy: I'm also going to call JSL's class a prototype. A prototype is an actual cookie. The prototype cookie could be eaten or modified, but you wouldn't normally want to do that. Instead, you should trace its outline into the cookie dough and cut that with scissors to make new instances of cookies. But if you do take a bite out of the prototype cookie, all future cookie instances will look bitten too (second example). Also, the _init_(...) method is not run for the prototype cookie, so it may be half-baked, but it has the right shape.

 

The documentation uses template rather than prototype. I prefer prototype over template; a prototype is a usable object while a template just shows where the spray paint should go. This subtle distinction does not matter for the first example. 

 

A simple example

 

This is a simple single-class example for bubble. The class prototype is made by DefineClass(). NewObject() makes 61 actual instances from the prototype. Each of the 61 instances has a radius and an x,y center. The prototype also provides methods to _init_(...), drift(), and draw(). A graph is displayed using the object draw method for each object. Then a loop runs, calling the object drift method for each object, and then telling the graph to redraw. The result is an animated graph like this:

 

 

The JSL for the graph:

 

// define a simple class (like a template) to represent a 2D bubble

Define Class("bubble",
	// class member variables
	m_radius = .; // class member variables are specified in
	m_xpos = .; // the class definition with initial values.
	m_ypos = .; // these values are copied into instances created by NewObject(...)
	// class methods
	_init_ = Method( {radius, x, y}, // called by NewObject(bubble(r,x,y)) below to initialize an instance
		this:m_xpos = x; // this: is the namespace of an instance
		m_ypos = y; // you don't need "this:" unless the member name is
		m_radius = radius; // the same as a parameter name
	);
	drift = Method( {}, // a method to move the bubble
		m_xpos = m_xpos * 0.995;// tentatively drift to center
		m_ypos = m_ypos * 0.995;
	);
	draw = Method( {}, // a method to draw the bubble
		Circle( {m_xpos, m_ypos}, m_radius )
	);
);

// make a bunch of instances of the bubble class, keep them in a list
bubblelist = {};
For( i = 1, i <= 61, i += 1,
	Insert Into( bubblelist, 
		// each instance of bubble is initialized with a random radius and position
		New Object( bubble( Random Uniform( .03, .8 ), Random Uniform( -.9, .9 ), Random Uniform( -.9, .9 ) ) ) 
	)
);

picno = 0; // need an ID for each picture

// make a window to display all the instances of the bubble class
New Window( "bubble class instances",
	gb = Graph Box( // the graphic script for the graph box scales and sizes the axes and...
		xaxis( {Add Ref Line( 0, "Solid", "Black", "", 1 )} ),
		yaxis( {Add Ref Line( 0, "Solid", "Black", "", 1 )} ),
		framesize( 600, 600 ),
		X Scale( -1, 1 ),
		Y Scale( -1, 1 ),
		// the graphic script tells each bubble to draw itself
		For( i = 1, i <= N Items( bubblelist ), i += 1,
			bubblelist[i]:draw()
		);
		text({-.9,-.9},char(picno)),
		<<onclose( // experimenting is simpler if the window stops the updates when the window closes
			keepRunning = 0;
			1;
		)
	)
);

// to make a video, set up a directory to hold the pictures
dir = "$temp\BubbleClassImages_DeleteMe\";
Delete Directory( dir ); // clean up previous results, if any
Create Directory( dir ); // create/re-create

// the while(forever) loop does two things on each pass: drift all the instances and update the graph
keepRunning = 1; // this flag is set to zero when the window is
While( keepRunning, // closed, stopping this loop
	For( i = 1, i <= N Items( bubblelist ), i += 1,
		bubblelist[i]:drift()
	);
	gb << inval << updatewindow; // tell the window it needs to redraw
	picno += 1; // displayed on graph and part of filename, if used
	// gb << save picture( dir || Char( (picno) + 1e6 ) || ".png" );
	Wait( .01 ); // a wait is needed to make sure the window actually redraws
);

My mental model for how it works: under the covers, DefineClass() keeps a hidden list of all the DefineClass() prototypes and they look a lot like instances. More about that in a moment; the example above accesses them through the NewObject() function. NewObject() clones a new instance of the prototype object and runs the _init_ method. 

 

What's this:?

In the example above I named the member variables with m_ prefix. That's not required, but it can be useful for both remembering what variables are member variables, and for preventing collisions with parameter names and local variable names. But I also used this:m_xpos on one of the _init_() method assignments. The this: namespace means "the current instance's member variable." So this:x = x; might be used if you want the member variable x and the parameter x to have the same name. m_ will be easier for the next person that maintains your code.

 

What's bubble:?

There's another namespace that you mostly don't need that will show why I said the DefineClass() prototypes in the hidden list look a lot like instances--the class name is a namespace. Using bubble:m_radius will access the prototype's m_radius, even from an instance method. If you think you need a static member variable, this is a way to make one...if you are careful.

 

Using the prototype for static values

 

The next example uses a static m_ID variable. As the comments suggest, I probably should have made a different variation, but this one is still useful because it explains the "if you are careful" warning. Here's what happens (my mental model, again):

When the _init_() below runs, the bubble2 class has already copied all of the prototype content into the new instance. The new instance has a copy of the prototype's m_id. _init_() then changes the m_id() in the prototype. The next instance gets the changed value. The other member functions have to choose between the m_id in the prototype (bubble2:m_id) and this:m_id (or just m_id with no namespace). Here, they use m_id which is a copy of a unique-to-the-instance integer. 

In the video, the last piece of the puzzle falls into place at 5:15 at the bottom edge of the screen. The black circles are bubble2s that are displaying their m_id value. They use the m_id to make sure to ignore themselves when studying all the near neighbors.

 

 

// twist is a scaling divisor for a shearing rotation that moves
// the outer edge faster.
// this value needs to get bigger with more beehive cells.
global:twist = 600; 
// choose the size to make them fit, but it also interacts with twist
// because the distance from the center grows
global:bubblesize = 0.057;

// use a defined class to represent each cell in the beehive.
Define Class("bubble2",
	m_radius = .; // in the video, all beehive cells have the same radius
	m_xpos = .; // the beehive cell's position
	m_ypos = .;
	//
	// this is answering the question "can a JSL Class have a static member variable?"
	// this could have been done more intuitively for this example by passing the
	// index of the creation loop to the _init_ code, but here it is...initialize the
	// "static" value to 1...
	m_id = 1; // keep reading to understand the two-faced nature of this variable...
	// What actually happens: when newObject(bubble2(...)) makes a new instance, the _init_(...)
	// is called. 
	// First, the m_id is cloned from the prototype to the instance. The first instance gets m_id==1.
	// Second, the bubble2: namespace causes the prototype class to increment the
	// prototype's m_id. The next instance gets the new value from the prototype, m_id==2.
	// this is a little odd, coming from a C++ -like language; in the drift method below,
	// "other:m_id != m_id" both m_id values are regular non-static instance members.
	// if you want the static behaviour, use the bubble2:m_id approach (bubble2 is the class
	// name and access the singleton in the prototype class.)
	// Normally you *never* want to use the class name as a name space qualifier because
	// you set up the class correctly and initialize via _init_( parameters ). And normally
	// a static value is also a constant value, so using a copy is just fine. But if
	// multiple instances need to communicate via a singleton static, AND you always use
	// the namespace, this is a reasonable place to keep the static.
	// Having written all that, I now dislike this particular example because it is using
	// a confusing combination of bubble2:m_id and instance values of m_id.
	
	_init_ = Method( {radius, x, y},
		m_radius = radius;
		this:m_xpos = x; // this: (or nothing) operates on an instance
		m_ypos = y;
		bubble2:m_id++; // bubble2: operates on the prototype. The next newobject(bubble2(...)) uses the modified prototype
	);
	distanceBetweenCenters = Method( {other}, 
		/*return*/ Sqrt( (m_xpos - other:m_xpos) ^ 2 + (m_ypos - other:m_ypos) ^ 2 )
	);
	distanceBetweenEdges = Method( {other}, // negative: overlaps
		/*return*/ distanceBetweenCenters( other ) - (m_radius + other:m_radius)
	);
	drift = Method( {}, 
		// bubbles should pull to 0,0 AND move away from near bubbles
		angle = ATan( m_ypos, m_xpos );
		radius = Sqrt( m_ypos ^ 2 + m_xpos ^ 2 );
		shear = global:twist; // also depends on number of circles
		shrink = .99950;
		// radius/shear creates a tiny amount of rotation, more on the edges. That rotational
		// shearing helps the circles bounce about. /400 is not enough and /100 is too much.
		// radius*shrink pulls all the circles to the origin. It is balanced by push, below.
		x = Cos( angle + radius / shear ) * radius * shrink;
		y = Sin( angle + radius / shear ) * radius * shrink;
		// check neighbors
		{neighbors, distances} = kdt << kNearestRows( 7, m_id );
		tension = 0;
		ntension = 0; // calculate a tension, or stress, for contour to use as a color
		For( iN = 1, iN <= N Items( neighbors ), iN += 1,
			i = neighbors[iN];
			other = bubblelist[i];
			If( other:m_id != m_id, // <<<<<<<<<<<<<<<< NOT the bubble2:m_id value !!!
				dist = distanceBetweenEdges( other );
				// if the dist is zero, it is perfect. if the other guy is far away, ignore him.
				If( Abs( dist ) < global:bubblesize * 0.8,
					tension += dist ^ 2; // this will be a RMS value at the bottom
					ntension += 1;
				);
				If( dist < 0, //push
					angle = ATan( m_ypos - other:m_ypos, m_xpos - other:m_xpos );
					x += Cos( angle ) * Abs( dist ) ^ .4 * .05;
					y += Sin( angle ) * Abs( dist ) ^ .4 * .05;
				);//
			, //
				Beep()
			);
		);
		m_xpos = x;
		m_ypos = y;
		If( ntension == 0,
			ntension = 1;
			tension += (global:bubblesize / 2) ^ 2;
		);
		Sqrt( tension / ntension ); // RMS
	);
	draw = Method( {},
		Transparency( .8 );
		Circle( {m_xpos, m_ypos}, m_radius, "FILL" );
		Transparency( 1 );
		Text( center justified, {m_xpos, m_ypos - m_radius * .2}, Char( m_id ) );
	);
);

bubblelist = {};
n = 12; // how many "rings" of hex cells
m = -26 + 21 * n + 3 * (n - 3) ^ 2; // yes, I used JMP to get this formula

For( i = 1, i <= m, i += 1,
	Insert Into(
		bubblelist,
		New Object( bubble2( Random Uniform( global:bubblesize, global:bubblesize ), Random Uniform( -.9, .9 ), Random Uniform( -.9, .9 ) ) )
	)
);

// need a table for contour plot, and it will include 100 values in a circle around the hive
ring = 100;
dt = New Table( "tension",
	Add Rows( m + ring ),
	New Column( "x", Numeric, "Continuous", Format( "Best", 12 ), Set Values( [] ) ),
	New Column( "y", Numeric, "Continuous", Format( "Best", 12 ), Set Values( [] ) ),
	New Column( "tension", Numeric, "Continuous", Format( "Best", 12 ), Set Values( [] ) )
);
For( i = 1, i <= ring, i += 1,
	angle = 2 * Pi() * i / ring;
	dt:x[m + i] = 1.25 * Sin( angle );
	dt:y[m + i] = 1.25 * Cos( angle );
	dt:tension[m + i] = 0;
);
cplot = dt << Contour Plot(
	X( :x, :y ),
	Y( :tension ),
	Show Data Points( 0 ),
	Fill Areas( 1 ),
	Label Contours( 0 ),
	Transform( "Range Normalized" ),
	Color Theme( "Green Yellow Red" ),
	Specify Contours( Min( 0 ), Max( .01 ), N( 50 ) )
	
);
Report( cplot )[framebox( 1 )] << Frame Size( 1920, 1080 ) << maximizewindow( 1 ) << backgroundcolor( "black" );

Report( cplot )[framebox( 1 )] << xaxis(
	{Format( "Fixed Dec", 12, 3 ), Min( -1.4*1920/1080 ), Max( 1.4*1920/1080 ), Inc( 0.5 ), Label Row(
		{Show Major Grid( 0 ), Show Major Labels( 0 ), Show Major Ticks( 0 ), Show Minor Grid( 0 ), Show Minor Labels( 0 ), Show Minor Ticks( 0 )}
	)}
);
Report( cplot )[framebox( 1 )] << yaxis(
	{Format( "Fixed Dec", 12, 3 ), Min( -1.4 ), Max( 1.4 ), Inc( 0.5 ), Label Row(
		{Show Major Grid( 0 ), Show Major Labels( 0 ), Show Major Ticks( 0 ), Show Minor Grid( 0 ), Show Minor Labels( 0 ), Show Minor Ticks( 0 )}
	)}
);

// here is the script that makes the bubbles draw

Report( cplot )[framebox( 1 )] << addgraphicsscript(
	Fill Color( "black" );
	text Color( "white" );
	pen Color( rgbcolor(64,64,64) );
	For( i = 1, i <= N Items( bubblelist ), i += 1,
		bubblelist[i]:draw()
	);
	hline(0);
	vline(0);
);
cplot << onclose(
	keepRunning = 0;
	Close( dt, nosave );
	1;
);



Wait( 0 );
//dir = "$temp\ClassWithStatic" || Char( global:twist ) || "\";
//Delete Directory( dir );
//Create Directory( dir );
//Write( Convert File Path( dir, "windows" ) );
picno = 0;
keepRunning = 1;
While( keepRunning,
	xy = J( N Items( bubblelist ), 2, . );
	For( i = 1, i <= N Items( bubblelist ), i += 1,
		xy[i, 1] = bubblelist[i]:m_xpos;
		xy[i, 2] = bubblelist[i]:m_ypos;
	);
	// the kdtable makes it faster to find near neighbors
	kdt = KDTable( xy );
	For( i = 1, i <= N Items( bubblelist ), i += 1,
		dt:tension[i] = bubblelist[i]:drift();
		dt:x[i] = bubblelist[i]:m_xpos;
		dt:y[i] = bubblelist[i]:m_ypos;
	);
	Report( cplot )[framebox( 1 )] << inval << updatewindow;
	picno += 1;
//	Report( cplot )[framebox( 1 )] << save picture( dir || Char( (picno) + 1e6 ) || ".png" );
	Wait( .01 );
);

 

Both examples above could just as easily (maybe easier) have been built with rows of a data table to represent objects and columns to represent member variables and JSL user functions for the methods. The use of classes, especially without any subclassing, might make your JSL harder for someone else to follow if they are not comfortable with classes in JSL.

Speaking of Functions, the doc for DefineClass mentions them too. As far as I can figure, they bring almost nothing to the table and should probably be avoided. I think every instance of a class is probably getting a separate copy of every method and every member variable. If you define a Function() and store it in a member variable, you are just making something else to copy that (maybe) could have been in a more global namespace with only a single copy. But if there will only be a single (or very few) instances and you really need a function for the class that is unaware of anything except its parameters, well maybe a function in a class is what you need.

Moving on to the final example, let's do some subclassing and look at the last namespace, super:, which I had to hunt for (see comments in JSL).

 

Intuitive example: a base class might be a vehicle, subclasses (or derived classes) are trucks and cars. A truck might replace the base class navigation method with one that knows to avoid residential streets. A car might take advantage of HOV lanes. A list of vehicles will each do the right thing when told to navigate from A to B.

 

Define Class has an interesting argument: baseclass(...). In the example below, shape will be the base class and square and circle will be derived from shape. The example makes a list of squares and circles and displays them.

two squares, two circles, two outlined, two filledtwo squares, two circles, two outlined, two filled

The following description is based on trial and error; I believe it is correct enough to be a helpful model to predict how something will work.

 

What's super:?

When a derived class is created, JMP runs to the end of the baseclass chain and works back to the top, creating base classes first, and in their own namespaces. The namespaces are linked together internally so that a derived class can reference a member variable by looking in its own namespace, then following the namespace chain down to the base class until finding the variable. The super: namespace just means skip forward once in the namespace chain without looking in the current namespace. That's why the JSL below is saying "don't use super: to call a base method if you don't need to"; the base method won't have access to the derived namespace so it can't call a derived function.

Multiple inheritance: if you are using "interfaces" and avoid duplicate names, it might work. Good luck. I'm not sure where a sibling baseclass fits into the chain of namespaces in the model above.

 

// demonstrates JMP class inheritance and overriding a method
// and calling the super class. A circle and a square class
// are derived from a shape class. Explains when to NOT use super:.
//
// I found super: in https://github.com/sassoftware/jsl-hamcrest/blob/master/Source/Core/Matchers/EqualTo.jsl
// it isn't documented anywhere else, but works great for calling base class _init_
// Also, https://github.com/sassoftware/jsl-hamcrest/blob/master/Source/Core/Utils.jsl
// shows a "self" implementation idea. This is a bad idea because it creates a circular reference
// that must be cleared before the object is lost/deleted...or it will leak.

// avoid making members and methods in base and derived classes with the same name.
// _init_, _to string_, _show_ are exceptions, but they must not try to use a derived 
// class member or method. (you would not normally want to, so no problem.)
//
// avoid needless upcast (super:); it cripples the base class because there is no downcast or
// virtual mechanism. Without the upcast, a baseclass function runs with access to the
// derived class members and methods. (see square's draw method calling docolor calling record.)
//
// it appears that the derived chain is searched *after* the current class; in a two class
// (base,derived) example that inverts the search order for members and methods when a
// derived class uses "super:" to call a base class method. If the base class calls a "virtual"
// function that exists in both base and derived, it won't get the derived one if "super:" was
// used but will if no scope is used. You only need "super:" typically when you want to call 
// the base class method from an overridden method of the same name. If the base class method
// isn't calling back to derived via a virtual, everything is good.

// deep details
//
// the output shows lines like this:
// record square=square(shape=(s= x=30 y=30 r=5 s=4 c=yellow f=1))
// record circle=circle(shape=(s= x=30 y=60 r=10 s=99 c=green f=0))
//
// the square and the circle both get the right answer via different
// paths. Because of way draw() is overridden by square and not by circle,
// the initial call, objects[window:i] << draw(), is calling either a
// derived member or a base member. In both cases, "this" is for the
// derived member. in square's draw(), I made an error, now commented
// out: /*super:*/docolor(), that caused docolor to run with "this" 
// for the base class, not the derived class. docolor then calls record().
// all three classes have record() members. The intent is to override the
// base class version ("never seen"). The error caused the base class
// version to run for square because "this" of docolor() was for the base class.

Define Class( "shape", // base class for both shapes
	m_Xcenter = .;
	m_Ycenter = .;
	m_radius = .;
	m_sides = .;
	m_color = .;
	m_filled = .;
	_init_ = Method( {x, y, radius, sides, color, fill},
		m_Xcenter = x;
		m_Ycenter = y;
		m_radius = radius;
		m_sides = sides;
		m_color = color;
		m_filled = fill;
	);
	_to string_ = Method( {},
		"shape=(s=" || 
		" x=" || Char( m_Xcenter ) || " y=" || Char( m_Ycenter ) || 
		" r=" || Char( m_radius ) || " s=" || Char( m_sides ) || 
		" c=" || Char( m_color ) || " f=" || Char( m_filled ) || ")"
	);
	_show_ = _to string_;
	
	// the following should NOT ever get called because square and circle override it
	// try changing square to call super:docolor and then you'll see this
	record=method({},show("never seen, overridden by square and circle"));
	
	// both shapes understand filled vs outlined
	// see note in square:draw method about calling docolor without super:
	docolor = Method( {},
		this:record(); // this: doesn't change anything, 
		If( this:m_filled, // it is mostly for disambiguating
			Fill Color( /*this:*/m_color );// parameter of same name
		, //
			Pen Color( this:m_color )
		)
	);
	
	// the base class has a draw method that circle uses;
	// square provides an override because this draws a 
	// diamond-oriented square
	draw = Method( {},
		docolor();
		p = J( m_sides, 3, 2 );//2==draw
		p[1, 3] = 1;//1==move
		p[m_sides, 3] = -2;//-2==draw and close
		For( i = 1, i <= m_sides, i += 1,
			p[i, 1] = m_Xcenter + m_radius * Cos( 2 * Pi() * i / m_sides );
			p[i, 2] = m_Ycenter + m_radius * Sin( 2 * Pi() * i / m_sides );
		);
		Path( p, m_filled );
	);
);

Define Class( "circle", baseclass( shape ),
	_init_ = Method( {x, y, radius, color, fill},
		super:_init_( x, y, radius, 99/*sides*/, color, fill )
	);
	_to string_ = Method( {},
		"circle(" || super:_tostring_() || ")"
	);
	_show_ = _to string_;
	record=method({},write("\!nrecord circle=",_show_()));
	// circle has no override for draw()
);

Define Class( "square", baseclass( shape ),
	_init_ = Method( {x, y, radius, color, fill},
		super:_init_( x, y, radius, 4/*sides*/, color, fill )
	);
	_to string_ = Method( {},
		"square(" || super:_tostring_() || ")"
	);
	_show_ = _to string_;
	record=method({},write("\!nrecord square=",_show_()));
	// square overrides the base class draw
	draw = Method( {},
		/*super:*/docolor();// don't up-cast, docolor can't down-cast and gets base class record()
		Rect(
			m_Xcenter - m_radius,
			m_Ycenter - m_radius,
			m_Xcenter + m_radius,
			m_Ycenter + m_radius,
			m_filled
		);
	);
);



objects = {};

Insert Into( objects, New Object( square( 30, 30, 5, "yellow", 1 ) ) );
Insert Into( objects, New Object( Circle( 30, 60, 10, "green", 0 ) ) );
Insert Into( objects, New Object( square( 60, 30, 15, "blue", 0 ) ) );
Insert Into( objects, New Object( Circle( 60, 60, 20, "red", 1 ) ) );


New Window( "objects",
	gb = Graph Box( framesize( 500, 500 ),
		For( window:i = 1, window:i <= N Items( objects ), window:i += 1, 
			//Show( objects[i] );
			objects[window:i] << draw(); // uses the base class or the derived class draw
		)
	)
);
//stop();
// animate the color of object 4, the big red circle
For( i = 0, i <= 1, i += (1 / 256), //show(i);
	objects[4]:m_color = HLS Color( i, .5, 1 );
	gb << inval << updatewindow;
	Wait( 0 );
);

 

What about this?

C++ has a value named "this" (other languages use "self" and "me") that is sometimes needed for an object to refer to itself. Not the same as this: namespace prefix. I don't think the JSL class offers a "this" within the class, and you might be tempted to make one like this:

x = newObject(myClass(17));
x::self = x;

or something close to that. Then a myClass method could use "self" to do do things that require a class instance, such as cloning.

Scripting index showing messages that can be sent to a class instanceScripting index showing messages that can be sent to a class instance

Keeping a member variable in a class instance that contains the (pointer to) class instance can create a memory leak. This brings us to a new topic...topics actually.

 

When is an instance deleted?

An instance is deleted when there are no references to it. In the section above, a "self" reference inside an instance will prevent the instance from being deleted, even if you've lost all your handles to the instance. There is no way to clean it up, other than closing JMP.

 

When is an instance copied?

Here's the best part: an instance is only copied if you use the <<clone method! As far as I can tell, there is no other way to copy an instance. This means instances are passed by reference. Yay! Way less clumsy to make a simple class to wrap a large matrix and pass an instance of the class to a user function, instead of having JMP make a copy of the matrix argument into the parameter value, and possibly returning another copy.

 

Last Modified: Jun 29, 2020 9:03 PM
Comments