Extending `apply` to extension methods

Say we have:

trait BinaryOperator[A, B, X]:
  extension(left: A)
    apply(right: B): X

With an implementation of:

object + extends BinaryOperator[Double, Double, Double]:
  extension(left: Double)
    override def apply(right: Double): Double = left + right

At present, the only thing this does is allow us to write

import +.apply
val three = 1 apply 2

or

import +
val three = +(1)(2)

I find apply methods very powerful, because they let instance names become method names and can make code far cleaner. I find extension methods very powerful because they provide a way to bundle potentially very complex functionality in a way that can make it very easy to apply or extend. A great example is the whole family of methods you can get from things like extension[T: Foo](target: T).

Unfortunately, these two great language features have no real interaction with each other, at least that I know of.

I can think of two intuitive ways that apply could be interpreted in the context of extension methods:

  1. Extension methods named apply make the containing instance itself usable as an extension method. Using my example above, this would allow:
    import +; val three = 1 + 2 (and let’s just imagine that we don’t already have + for Doubles :wink:)
    This would be pretty consistent with the existing interpretation of general methods named apply - essentially letting us move from +(1)(2) to 1.+(2) thanks to the extension declaration.
  2. Extension methods named apply act as if they were methods named apply defined on the target class. This would translate to:
    import +.apply; val three = 1(2)
    A little more nonsensical in this specific example, but still something that would be useful in other cases.

First prize would be having access to both features using different keywords, but either of these would be a great features to have, and I think it would be a waste to leave the non-interaction between extension and apply the way that it is.

EDIT: @Sporarum has pointed out that number 2 is already available in the language - my bad!

Note that you can already do number 2:

extension (x: Int)
  def apply(y: Int) = x * y

4(1 + 2) // 12
1 Like

Thanks @Sporarum :man_facepalming: I did compile-test both cases to double check before going and posting this, but I must have made a syntax error somewhere. I do still think number 1 would be an extremely useful feature to have though!

As for number 1, I’m not really in favor of it, right now there is only one way to have a + b available through extension methods:

extension (a: A)
  def + (b: B) = ???

With the proposal, there would be another way to do it, and every time I need an extension method, I would have to choose between the two.

Personally, I like to a single standard way to do simple things, so that I can focus on how to do more complicated things using it

1 Like

I do support the “one right way to do things” principle, but I think this is a different thing. I don’t just want to define a “+” extension - I’d like to have extension methods that can depend on instance names the way regular methods can. For regular methods, we can do something like

class Foo():
  def apply(left: String)(right: String) = ???

val fooImpl = new Foo()
val bar = fooImpl("baz")("bash")

But there is no equivalent for extension methods, even though we can define extension methods on extendable or instantiable classes whose implementations can be radically different from each other.

The only current option (that I’m aware of!) is to alias the method, e.g.

extension (left: Double)
  def combine(right: Double) = ???
  def +(right: Double) = left combine right

But this is totally static and gets very boilerplate intensive in a use case like mine, where there might be a dozen overloads of that main combine method depending on the mixins in the particular implementation.

I don’t understand what you mean ^^’

Could you give a complete example of something you could not do with extension methods ?

1 Like

That’s not a terrible idea.

Normally, you expect f(x)(y) to be adapted to f.apply. I don’t know whether it would need a special marker extension object f.

The wrinkle is that you expect extensions from givens, where the name of the given is not meaningful (and may be anonymous).

Your example uses an import, which is optimizing for the awkward case, namely the named case.

@Sporarum Haha fair enough! I’ll admit what I’m trying to build is a little esoteric, but to simplify, let’s go back to that base BinaryOperator trait:

trait BinaryOperator[A, B, X]:
  extension(left: A)
    def combine(right: B): X

I might have a bunch of singleton instances of this:

object + extends BinaryOperator[Double, Double, Double]:
  extension (left: Double)
    override def combine(right: Double): Double = ???

object * extends BinaryOperator[Double, Double, Double]:
  extension (left: Double)
    override def combine(right: Double): Double = ???

Or even programmatically constructed instances like:

val op: BinaryOperator[Double, Double, Double] = makeOperator(context)

So say we have all of them in scope and properly imported. What does 1 combine 2 mean? It’s extremely ambiguous, because they all have the same signature for their combine method. I believe the compiler just doesn’t let you use extension methods when there’s ambiguity like this.

The two options to resolve that ambiguity are (1) abandoning extension methods completely, which is not nice to use:

op.combine(1)(2)

Or (2), explicitly defining aliases for the combine method, which is very boilerplate intensive, and impossible in the case of programmatically constructed instances:

object + extends BinaryOperator[Double, Double, Double]:
  extension (left: Double)
    override def combine(right: Double): Double = ???
    def +(right: Double): Double = left combine right  // boilerplate!

object * extends BinaryOperator[Double, Double, Double]:
  extension (left: Double)
    override def combine(right: Double): Double = ???
    def *(right: Double): Double = left combine right  // boilerplate!

// no way for the makeOperator function to know how to alias the combine method
val op: BinaryOperator[Double, Double, Double] = makeOperator(context)

val three = 1 + 2  // fine
val twelve = 3 * 4  // fine
val contextDependentResult = 5 op 6  // does not compile, no way to do this without falling back to...
val ugly = op.combine(5)(6)

So there’s something that’s not possible, and for the part that is possible, the boilerplate can get intense.

Here’s an actual example of some method signatures from the project I’m working on:

  extension[L, M] (leftExpression: leftAssociate.Expression[L, M])
    def combine[R](right: R): leftAssociate.Expression[L, Expression[M, R]] = ???

  extension[L] (leftExpression: leftAssociate.Expression[L, X])
    @targetName("simplifyingLeftAssociatedCombine")
    def combine(right: B): leftAssociate.Expression[L, X] = ???

Imagine having to alias both of these for each of +, -, *, /, dotProduct, etc, in addition to a bunch of other similarly complex overloads.

Thanks!

A marker keyword makes some sense - it would allow keeping the apply special method name limited in effect, and introducing a new special method name wouldn’t be backwards compatible.

I agree anonymous givens are a bit of a wrinkle, but if the syntax for this feature was similar to how apply methods work, there would always be a static method name available for situations where you’re using anonymous givens. As for named givens, from a user perspective it seems ok for given names to be meaningful in a situation like this, but I don’t know if that would wreck deeper language things!

That said, this feature would sort of make the instance to which the method is attached into an extension method itself, so just placing such an instance in scope without the given keyword could potentially give you access to the method via the instance name.

I would propose you do the following:

trait BinaryOperator[A, B, X]:
  def apply(a: A, b: B): X

object add extends BinaryOperator[Double, Double, Double]:
  def apply(a: Double, b: Double): Double = ???

add(1, 2)
1 Like

I agree that I’ll probably end up having to settle on that for now, but that does mean abandoning extension methods completely, which is a shame. I do think that ideally an equivalent implementation using extension methods would also be valid, allowing that sweet sweet 1 add 2 syntax.

Unfortunately, is(it, do(hard to, math this way)).

3 Likes

Lisp is the way :wink:

1 Like

While messing around with this, I’ve found the aliasing solution, but with O(1) boilerplate:

object Plus extends BinaryOperator[Double, Double, Double]:
  ...

export Plus.combine as +

and

val op: BinaryOperator[Double, Double, Double] = makeOperator(context)
import op.combine as op

This shifts the boilerplate from the library to the implementation, which is not ideal, but it’s so much less boilerplate that it’s gotta be worth it. It also means that dynamically constructed extension method containers are usable, provided an extra bit of boilerplate at the call site.

I love Scala 3

This definitely makes what I’m suggesting a less pressing feature, but it still feels like one worth having. For regular methods, the equivalent boilerplate would be:

val myFunc = MyFunc(param)
import myFunc.apply as myFunc

myFunc(stuff)

And I think we’re all sufficiently used to the apply syntactic sugar to agree the above seems lame!

2 Likes

Oh, not sure that I’d recommend it, but you could also do the following:

import scala.language.dynamics

trait BinaryOperator[A, B, X, Name <: String & Singleton]:
  def combine(a: A, b: B): X
  extension (a: A)
    inline def applyDynamic(s: Name)(b: B) = combine(a, b)

case class Meter(d: Double) extends Dynamic

object + extends BinaryOperator[Meter, Meter, Meter, "+"]:
  override def combine(a: Meter, b: Meter): Meter = Meter(a.d + b.d)

val foo: Meter = Meter(1.0)

import +.*

foo + foo

Note that object + has passes "+" as parameter, but these do not need to be identical, the object should probably be named Add or Additive, or something