Implementation of extensions doesn't match expectation or documentation

This may be obvious to everyone, but for my own sake and for clarity of communication I want to point out that there are at least 3 different things at play here:

  1. right-associativity (i.e. implied parenthetical grouping of equal-precedence operators)
  2. right-receptivity (to make up a term… i.e. which operand is the functional “receiver” of the binary operation)
  3. symbolic operators as aliases (vs. independent functions/methods)
  4. Perhaps there’s also something here about infix vs prefix notation, but I can’t quite put my finger on the exact connection at the moment, so maybe I’m reading into things.

As far as I can tell, there is no suggestion of removing the concept of right-associativity, only right-receptivity. And the proposed syntax for it seems to imply a preference for symbolic infix operators to be externally defined aliases for alphanumeric prefix operations on the type.

To be clear on my perspective, I’m pro- keeping right-associativity as a concept. How we have it is worth discussing, I suppose. But I am also very much pro- right-receptivity. I believe it encourages a significantly better encapsulation of responsibility on the authorship side and a more elegant syntax on the calling side in many cases.

The example that actually prompted my bringing this up at all might be illustrative of my thinking (yes, I’m aware there may be better ways to do this, but keep in mind I’m simplifying quite a bit to fit into the context of a forum post):

I wanted to encapsulate the equivalent of this logic:

class Envelope(val msg: Message):
  @targetName("opTransform")
  def ?:(shouldTransform: Boolean): Option[String] = 
    if shouldTransform then
      Some(msg.transform)
    else
      None

class MessageFilter(...):
  private def shouldPass: Boolean = ???

  def getMessage(envelope: Envelope): Option[String] = shouldPass ?: envelope

Maybe this example is overly simplified, but hopefully it is clear that my goal was to keep the knowledge of how to transform encapsulated in the Envelope type.

This, obviously works, as is, given right-associativity behavior on classes. But imagine that Envelope is from a library, so I neither have access to the class nor to its companion to add an extension (on Boolean?) there. But I also don’t want to add envelope transformation as a feature of all Booleans, even in the scope of my MessageFilter. I really would like to keep the transformation logic confined to Envelopes. So I attempted this:

extension (e: Envelope)
  def ?:(shouldTransform: Boolean) = ... //Same implementation as above

But now my calling syntax (shouldPass ?: envelope) doesn’t compile. I either have to write it envelope ?: shouldPass or move the implementation of ?: to an extension (b: Boolean) clause. Swapping the operands at the call site is ugly. Imagine that getting an envelope is the final result of a series of maps, flatMaps, whatever. Now the critical boolean control operator is hidden at the end, easily missed. And moving the implementation to Boolean is just plain strange. There is no reason for any Boolean instances in scope to have any concept of what an Envelope is.

I know that this example is weird in other ways (?: being both wrapping and transformation, etc) but those are just artifacts of me trying to minimize the example. My point is really about encapsulation of logic, organization of code, and clarity at the call site. Losing right-receptivity would harm every one of those aspects in this case, in my opinion.

Finally, to briefly touch on symbolic operators as aliases… I rather like the @targetName annotation with a symbolic method name being sufficient, but I’m not going to get too bent out of shape over a different way of handling symbolic & alphanumeric names for the same operation. I will say, though, that I really don’t like moving what seems like a legitimate member of the type out of the class and into an extension on the companion. I could maybe live with it (though not my preference) if all symbolic operators had to be there, but it would seem very odd indeed if ++ could be on the class but :: had to be in the companion.

And if we’re moving all symbolic operators to the companion, I think we need a different syntax than the current extension syntax. The way extension (t: T) looks to me is like it is extending the type with additional methods. Not that it is providing the left operand to operators specified after an intervening def. And, for what it’s worth, as I’ve pointed out elsewhere, all of the Scala 3 language docs provided to developers seem to reinforce my interpretation of what extension (t: T) means, regardless of what the original intention may have been.