Rogue Fallback Methods
Overview
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
Having the 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 Scope::resolve_call()
method:
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.