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:

  1. Look for methods named "x", converting this access into a call if it exists but not throwing an error if it doesn't.

  2. Look for locals and properties named "x", converting this access appropriately if found.

  3. 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.