Slag:Augments and Aspects
Augments are a way to achieve piece-wise class definitions in Slag. You can write part of a class in the original definition and then add more methods and properties to it in a separate place with an augment.
Aspects are like Java interfaces that can include properties and method definitions (code). They have a set of rules that allow aspect code to either replace or merge with existing code.
Model/View Separation Example
class Actor( Vector2 position, Vector2 velocity ) method update: println( "updating actor" ) endClass ... augment Actor method draw: println( "drawing actor" ) endAugment ... local Actor actor(pos,v) actor.update actor.draw
- Defining properties and methods in an augment has the same effect as defining them in the original class.
- You may not redefine existing methods in an augment - however, that's possible with #Overlaying Augments.
- For some templated class DataBank<<$DataType>>:
- "augment DataBank" applies the augment to all template instances.
- "augment DataBank<<String>>" only applies the augment to the template instance specialized with type String.
- "augment DataBank<<primitive>>" only applies the augment to template instances specialized with a primitive datatype.
- primitive in the example above can be the following:
- primitive - Any primitive data type (Real64, Real32, Int64, Int32, Char, Byte, Logical)
- numerical - Any primitive except Logical
- real - Real64 or Real32
- integer - Int64 or Int32
- compound - Any compound type
- reference - Any reference type (object type)
- * - Any type (e.g. "augment TileMap<<*,primitive>>")
- Aspects are a way to allow a class to have multiple types.
- They are similar to Java interfaces except that they can include new properties and actual code.
- Where Java classes implement interfaces, Slag classes incorporate aspects.
- A variable of a given aspect type can reference any object that incorporates that aspect.
Dice Example v1
Consider the following code:
class RollTest1 method init: local Die d6(6) local Die d4(4) println( d6.roll ) # prints: 1..6 println( d4.roll ) # prints: 1..4 local Dice attacker(), defender() attacker.add(d6,d6) defender.add(d4,d4,d4) println( attacker.roll ) # prints: 2..12 println( defender.roll ) # prints: 3..12 endClass class Die( Int32 sides ) method roll.Int32: return random_Int32(1,sides) endClass class Dice() PROPERTIES dice() : Die METHODS method add( Die d ).Dice: # Note: methods that return their own class type allow automatic # call chaining, where writing add(a,b,c) translates into # add(a).add(b).add(c). dice.add(d) return this method roll.Int32: local var sum = 0 forEach (d in dice) sum += d.roll return sum endClass
A Dice object stores a list of Die objects. Both Dice and Die can have roll().Int32 called on them. What would it take to allow a Dice object to store other Dice objects as well as individual Die objects?
Die and Dice would have to have a common base class that defines roll(), but a Dice is not a Die and a Die is not a Dice so regular inheritance doesn't make sense. However, both Die and Dice are rollable so we can make that a common aspect that both classes share.
Dice Example v2
We'll define aspect Rollable as something that has a roll() method. We'll incorporate that aspect into both Die and Dice classes, and finally we'll have class Dice store a list of Rollable objects which will let it store either single Die objects or Dice objects because now a Die object is a Rollable and a Dice object is a rollable.
class RollTest2 method init: local Dice dice_2d6(), dice_2d6_d4() dice_2d6.add(Die(6),Die(6)) dice_2d6_d4.add( dice_2d6 ) dice_2d6_d4.add( Die(4) ) println( dice_2d6_d4.roll ) # prints: 3..16 endClass aspect Rollable method roll.Int32: abstract endAspect class Die( Int32 sides ) : Rollable method roll.Int32: return random_Int32(1,sides) endClass class Dice() : Rollable PROPERTIES dice() : Rollable METHODS method add( Rollable d ).Dice: dice.add(d) return this method roll.Int32: local var sum = 0 forEach (d in dice) sum += d.roll return sum endClass
Aspects With Code
Aspect methods may include code as a default definition that can be redefined in the class.
EventListener Example v1
In the example below, an "EventListener" has multiple methods with default definitions. Two extended classes each redefine a different one of those methods. This is very similar to inheritance, but keep in mind that "AlphaListener" and "BetaListener" both extend class object and only incorporate EventListener. The important differences will explained a little later on.
class Test method init: local EventListener listeners() listeners.add( AlphaListener(), BetaListener() ) forEach (listener in listeners) listener.on_alpha # prints: # AlphaListener on_alpha! # EventListener on_alpha! forEach (listener in listeners) listener.on_beta # prints: # EventListener on beta! # BetaListener on_beta! endClass aspect EventListener method on_alpha: println( "EventListener on_alpha!" ) method on_beta: println( "EventListener on beta!" ) endAspect class AlphaListener() : EventListener method on_alpha: println( "AlphaListener on_alpha!" ) endClass class BetaListener() : EventListener method on_beta: println( "BetaListener on_beta!" ) endClass
EventListener Example v2
When you redefine an aspect method you can include the original definition as well by writing "insertUnderlying". In the modified example below, the redefined event callbacks each print something and include the original definition as well:
class Test method init: local EventListener listeners() listeners.add( AlphaListener(), BetaListener() ) forEach (listener in listeners) listener.on_alpha # prints: # AlphaListener on_alpha! (AlphaListener) # EventListener on_alpha! (AlphaListener) # EventListener on_alpha! (BetaListener) forEach (listener in listeners) listener.on_beta # prints: # EventListener on beta! (AlphaListener) # EventListener on_beta (BetaListener) # BetaListener on_beta! (BetaListener) endClass aspect EventListener method on_alpha: println( "EventListener on_alpha!" ) method on_beta: println( "EventListener on beta!" ) endAspect class AlphaListener() : EventListener method on_alpha: println( "AlphaListener on_alpha!" ); insertUnderlying endClass class BetaListener() : EventListener method on_beta: insertUnderlying; println( "BetaListener on_beta!" ) endClass
Underlying and Overlaying Aspect Methods
Aspect methods can be underlying (the default setting) or overlaying, which determines which method definition replaces which when there are multiple definitions. Underlying methods are useful as default implementations and overlaying methods are useful for adding an extra bit of code to the beginning or end of a method definition.
A toy example:
class Dishes : Mexican, FruitSnack, SpringMix method init: grapes salad beans peaches # prints: # Grapes # Spring Mix # with Italian dressing # Burritos # Peaches # and Cream method salad: insertUnderlying println( " with Italian dressing" ) method beans: println( "Beans" ) method peaches: println( "Peaches" ) endClass aspect SpringMix method salad: println( "Spring Mix" ) endAspect overlaying aspect Mexican method beans: println( "Burritos" ) endAspect aspect FruitSnack method grapes: println( "Grapes" ) overlaying method peaches: insertUnderlying println( " and Cream" ) endAspect
You can incorporate as many aspects into a class as you like. In cases where there are multiple definitions of the same method, the following rules govern which methods are the "top-level" methods.
For this class declaration:
class Alpha ... aspect Aspect1 ... aspect Aspect2 ... ... class Beta : Aspect1, Alpha, Aspect2
The methods are layered in the following order:
- Overlaying methods from Aspect2 (topmost)
- Overlaying methods from Aspect1
- Methods defined in Beta
- Underlying methods from Aspect2
- Underlying methods from Aspect1
- Methods inherited from Alpha (bottommost)
- The order you incorporate aspects matters but where you list the base class doesn't matter.
- If an overlaying method in Aspect2 contains an "insertUnderlying" command, the code from the same method in "Aspect1" will be merged (if defined) and so on.
- If there is no underlying code in a call to insertUnderlying then nothing is inserted - you can add insertUnderlying without knowing or caring if there actually is underlying code.
- When insertUnderlying would insert a method inherited from the base class, insertUnderlying is turned into a prior call instead ("method update: insertUnderlying" -> "method update: prior.update").
- Merging aspect methods creates a single method containing all of the merged code.
- Consider the following: "method update: println("pt1"); insertUnderlying; println("pt2")". If the underlying code contains a return command, the method will end without "pt2" ever having a chance to print out. For this reason the standard approach to writing overlaying code should be to perform all your tasks first and then call insertUnderlying.
A common design pattern in Slag is to augment a class (often a library class someone else wrote) to incorporate an aspect which changes the behavior of a method, like so:
overlaying aspect SubstringTrace method substring( Int32 first_index, Int32 last_index ).String: println( "DEBUG: substring indices are $..$" (first_index,last_index) ) insertUnderlying endAspect augment String : SubstringTrace;
An overlaying augment is a shorthand notation for accomplishing the same thing:
overlaying augment String method substring( Int32 first_index, Int32 last_index ).String: println( "DEBUG: substring indices are $..$" (first_index,last_index) ) insertUnderlying endAugment