I just added fallback methods to Rogue, where fallback methods are getters and setters that are invoked unless a property of the same name already exists.
The Gritty Details
Up until now access methods (getters and setters) have always trumped direct access to properties, but fallback methods are a way to reverse the priority.
One typical use of access methods is to guard, validate, or trace access to properties, and in those cases we do want the methods to take precedence as normal. For example:
class Document PROPERTIES title : String METHODS method title->String if (@title) return @title return "Untitled" endClass println Document().title # prints: Untitled
Another valid use, however, is to define an API without imposing implementation specifics. The following toy example demonstrates:
class Collection METHODS method count->Int32 [abstract] endClass class IntList : Collection PROPERTIES data = Array<<Int32>>(10) count : Int32 METHODS method add( value:Int32 ) data[count] = value ++count method count->Int32 return @count endClass
count() getter here is great for polymorphism but bad for efficiency. Without "manually" specifying
@count instead of
count in the
add() method, the getter is going to be called each time
count is accessed.
I realized an easy solution: create a method attribute that allows the getter method to be called only if no property exists. Here's the modified version in class IntList:
method count->Int32 [fallback] return @count
Now let's see how this works:
local a = IntList() : Collection println a.count # calls the getter local b = IntList() println b.count # accesses 'count' directly
By happenstance this was super simple to implement in my existing compiler infrastructure. My logic for resolving a generic "access" into a method call or a property read looks like this:
Look for methods named "x", converting this access into a call if it exists but not throwing an error if it doesn't.
Look for locals and properties named "x", converting this access appropriately if found.
Look for methods named "x", throwing an error if not found.
Originally the only point of Step 3 was to throw an appropriate error message - I needed that error message logic built in to attempted calls for other reasons and so I didn't want to duplicate it unnecessarily. Now it handles fallback methods. I added this bit of logic to my
method resolve_call( ..., error_on_fail:Logical ) local m = find_method( ... ) ... if (not error_on_fail and m.is_fallback ... and not m.type_context.is_aspect) return null
It basically says "if it's not essential that we succeed and this is a fallback method, return no match". This allows the compiler to check for properties and locals, but when no other matches are found and we're on the second
resolve_call() that throws an error on failure, go ahead and allow the fallback method.