Allow setters with multiple arguments

Currently scala only allow a setter with single argument e.g.

class A:
  val _a: mutable.Seq = Seq.empty
  inline def f: Seq[String] = _a
  inline def f_=(s: Seq[String]) = ... // Allowed
  inline def f_=(i: Int, s: String) = _a(i) = s // Not allowed

Should scala add support for the third function signature? It will allow us to use:

val a: A = ???
a.f = Seq(...)
a.f(i) = "string"  // doesn't work currently

We can approximate this by making inline def f = ... return a class which defines a inline def update(i: Int, s: String) but since classes can’t be inlined currently, the resulting code is not fully inlined.
Also, we lose inline def f returning a meaningful value, and have to define a inline def apply() on the intermediate class, which has to called as a.f() instead of a.f

1 Like

I had to clean it up a smidge to get your example to compile, and what I found was kind of interesting: you can actually write the class almost as-is, it’s the call-site that fails.

import scala.collection.mutable

class A {
  val _a: mutable.ListBuffer[String] = mutable.ListBuffer.empty[String]

  def f: Seq[String] = _a.toSeq
  def f_=(s: Seq[String]) = { // Allowed
    _a.filter(_ => false)
    s.foreach(_a += _)
  }

  def f_=(i: Int, s: String) = _a(i) = s // Compiles, but does not desugar as one might hope.
  //def f_= (i: Int)(s: String) = _a(i) = s // Also compiles, and also does not desugar in the desired manner

  override def toString: String = s"A(${_a.mkString(" ")})"
}

@main def run(): Unit = {
  val a: A = new A()

  a.f = Seq("this", "is", "a", "test")
  println(a)

  // Fails with: value update is not a member of Seq[String] - did you mean Seq[String].updated?
  // a.f(1) = "was" // Does not compile

  // Works as one might expect
  a f_= (1, "was")

  // Fails with: expression does not take parameters
  // a f_= (1)("has been")

  println(a)
}

Scastie

1 Like

It makes sense that it works that way. The only reason the second doesn’t work is because it looks as if the Seq is being applied to 1. It works with a ., but of course, that’s not quite the same as having a setter with multiple arguments (Scastie). One thing the OP can do is make f an object with an update method instead.

import scala.collection.mutable

class A {
  val _a: mutable.ListBuffer[String] = mutable.ListBuffer.empty[String]

  def f: Seq[String] = _a.toSeq
  def f_=(s: Seq[String]) = { // Allowed
    _a.filter(_ => false)
    s.foreach(_a += _)
  }

  def f_=(i: Int, s: String) = _a(i) = s // Compiles, but does not desugar as one might hope.
  @annotation.targetName("asoifjdaphsdf")
  object F {
    def update(i: Int, s: String) = _a(i) = s
  }

  override def toString: String = s"A(${_a.mkString(" ")})"
}

@main def run(): Unit = {
  val a: A = new A()

  a.f = Seq("this", "is", "a", "test")
  println(a)

  // Fails with: value update is not a member of Seq[String] - did you mean Seq[String].updated?
  // a.f(1) = "was" // Does not compile

  // Works as one might expect
  a f_= (1, "was")

  // Fails with: expression does not take parameters
  // a f_= (1)("has been")
  a.F(1) = "has been"

  println(a)
}

Scastie

I faced this issue when trying to add a zero-overhead wrapper over a java library.
To achieve zero-overhead I was making all functions and parameters inline and using extension to add such inline methods to java classes.

As suggested, if I return an object from f with an update method, that object remains in final compiled bytecode since currently scala3 doesn’t allow inline classes.

I tried returning an opaque type instead of an object and add inline def update as an extension to opaque type but that also doesn’t work since opaque types and inline methods can’t be in the same scope due to an implementation restriction.

But as you already noticed yourself, this would conflict with the update method. If f were an Array then how does the compiler have to translate a.f(i) = "string"? It could be both a.f.update(i, "string") or a.f_=(i, "string").

It would also make this desugaring a lot more complex. Currently this is probably just a simple desugaring in the parser. If the code can desugar to 2 different things based on the context this would have to be done somewhere in the typer so the compiler can check which desugaring he has to pick. And then it could still be very confusing for readers of scala code.

What about a class extending AnyVal?

2 Likes

This was recently fixed. This change may be released soon.

1 Like

Thanks for the extends AnyVal suggestion. That worked as I had wanted. I had associated extends AnyVal with extension methods so it didn’t occur to me to use them as value classes too.

Regarding the recent fix for inline and opaque methods, I’m now getting another error with 3.0.2-RC1-bin-20210621-4aa7f90-NIGHTLY compiler version. Scastie - An interactive playground for Scala

object opq:
  opaque type Str = java.lang.String
  object Str:
    def apply(s: String): Str = s
  inline def lower(s: Str): String = s.toLowerCase
    //error: undefined: a.toLowerCase # -1: TermRef(TermRef(NoPrefix,val a),toLowerCase) at inlining
  extension (s: Str)
    inline def upper: String = s.toUpperCase
    //error: undefined: a.toUpperCase # -1: TermRef(TermRef(NoPrefix,val a),toUpperCase) at inlining

@main def main =
  import opq.*
  val a: Str = Str("aSd")
  println(a.upper)
  println(opq.lower(a))

Should I report this as a bug or is this an incorrect usage of inline and opaque? The error goes away if I remove inline from the method definition.

Seems like a good idea to report it.