1

Please consider a family of plain and case classes, cross-compiled to JVM and Scala.js, which make mixed use of properties with automatically and manually defined accessor methods. Many come from external libraries but all inherit from a specific trait owned by the enquirer:

    trait ContextuallyMutable { ... }

How could one enforce a system-wide constraint on all setter/mutator methods of every class that inherits from ContextuallyMutable?

The constraint in question divides execution contexts into two types: owner and client. If invoked from an owner context, a setter executes normally, but in client contexts it initiates a remote procedure call.

As a crude example, consider a simple case class:

    case class PropTest(var i:Int) extends ContextuallyMutable

Which prompts the Scala compiler to generate a setter/mutator method something like:

    def i_=(i0:Int):Unit = this._i = i0

To satisfy the constraint, the setter method should instead read something like:

    def i_=(i0:Int):Unit = {
      if (isOwnerContext) { // prohibit direct mutation in client contexts.
        this._i = i0 
      } else tell(this, i0)  // fire and forget message to owner context
    }

In a smaller, less collaborative project, one could imagine writing constraint enforcement into every setter/mutator of every class by hand:

    case class PropTest(private var _i:Int) extends ContextPropped {
      def i:Int = _i
      def i_=(i0:Int):Unit = {
        if (isOwnerContext) wrapped.i = i0
        else tell(this, i0)
      }
    }

... but one should rather not imagine such tedious, error prone, boilerplate lest one inflict like tedium on the maintainers of contributory libraries.

Besides, manually overridden accessor methods conflict with established serialization specs because of Scala's conventional use of the underscore: private var _propName:Type for manually defined properties.

{ "i":42 }  /* rewards intuition while */  { "_i":42 } // frightens it away

Without altering the source code of a Scala.js class, how could one imbue its setter/mutator methods with alternative behaviors conditioned on invocation context?

In broader terms: Does the Scala.js ecosystem offer any way(s) to augment or replace the auto-generated setter/mutator methods for property fields of scala classes?

Edit: Further research has uncovered a few possibilities and related sub-questions:

  • Interceptors from Dependency Injection frameworks. Can Scala.js versions of MacWire's interceptors satisfy this constraint? The documentation didn't readily clarify whether interceptors can target all setter/mutators of all inheritors of a given trait. If MacWire, or another Scala.js compatible DI framework, can provide this functionality then IOC looks like a relatively painless solution, except for the bad manners of adding heavy framework dependencies into libraries.
  • Wrappers. The following works, except that it requires refactoring every reference to every instance of PropTest and forces users of this library to consciously distinguish between Client and Owner contexts.
    case class PropTest(var i:Int): extends ContextPropped

    class PropTestWrapper(private val wrapped:PropTest){
      def i:Int = wrapped.i
      def i_=(i0:Int):Unit = {
        if (isOwnerContext) wrapped.i = i0
        else tell(this, i0)
      }
    }
  • Subclass. Extending the original type addresses the issue pretty well except that it depends on a lot of tedious boilerplate, extends case classes, and doesn't compile.
    class PropTestSubclass(var _i:Int) extends PropTest(_i) {
      // Compiler Error: mutable variable cannot be overridden
      override def i_=(i0:Int):Unit = {
        if (isOwnerContext) wrapped.i = i0
        else tell(this, i0)
      }
    }
  • Wrapped Subclass or DIY Proxy. More attractive still, by combining a wrapper with a subclass, we maximize type compatibility between the wrapper and wrapped class. However, we also combine all of penalties we prefer to avoid: compiler errors, tedium, and boilerplate, and add the taboo violating practice of extending case classes:
    class PropTestProxy(private val wrapped:PropTest) extends PropTest(wrapped.i) {
      // Compiler Error: mutable variable cannot be overridden
      override def i_=(i0:Int):Unit = {
        if (isOwnerContext) wrapped.i = i0
        else tell(this, i0)
      }
    }

Of course, these compiler errors disappear after rewriting the original case class as:

    case class PropTest(private var _i:Int) extends ContextPropped {
      def i:Int = _i
      def i_=(i0:Int):Unit = this._i = i0
    }

... but, as alluded to earlier: this intervention requires more typing than a complete rewrite of the entire project from scratch.

Another approach looks more directly to the concept of a Delegate/Proxy instance. The main challenge with proxy classes seems to stem from a sort of type merge problem. An ideal proxy will have no user distinguishable difference from the type it mediates.

  • Java's Proxy class and its InvocationHandler. Perfect except that it only proxies Java interfaces and depends on reflection which Scala.js cannot support.
  • Scala's Proxy trait. Limited only to hashCode, equals, and toString methods and deprecated.
  • Delegate Macro compiler plugin by cmhteixeira. Extremely close, but, like Java's Proxy class, works only on interfaces (also traits) and only facilitates manual method override. Perhaps in conjunction with other macros, this could contribute to a beautiful solution, but maybe the type gap between a class and a proxy of that class will never close completely.

In the Scala ecosystem, can a delegate/proxy ever take the place of its target in a fully type safe way? What would cmhteixeira say?

Other compiler plugin solutions:

  • better-tostring by Jakub Kozłowski and polyvariant points to the possibility of rewriting automatically generated toString methods at compile time, and explicitly not manually overridden ones, but with 0 knowledge of compiler plugins, the enquirer cannot estimate the feasibility of addressing this design challenge by developing a custom Scala compiler plugin. Any thoughts, Jakub Kozłowski?

Thank you for any consideration!

Ben McKenneby
  • 481
  • 4
  • 15

0 Answers0