Question about implicit conversion

This is fine when there are two methods, but what if there are twenty methods? Or two hundred methods? That would be a ton of boilerplate for what is a very common pattern.

“Two hundred method” implicit conversion/extension methods are not uncommon: e.g. String => Seq and Array => Seq in the standard library fall into this category. Not every day code to define, but certainly everyday code to use

IMO it is right that we separate extension methods from implicit conversions: before that, you often want one or the other, and get both by accident.

BUT there are occasional cases where you actually do want both, and we should provide some syntax to opt-in to getting both (basically the status quo). This is what we would use for String=>Seq, Array=>Seq, Int=>BigInt, and so on.

Duplicating the entire class public API as extension methods is not a good solution: it can easily be hundreds or thousands of lines of code that the library author would need to duplicate and keep in sync, down to copy-pasted scaladoc. It’s just asking for bugs to happen as new methods are accidentally added to one and not the other, bugfixes are made to one and not the other, and things inevitably diverge over time

5 Likes

Maybe the following should have the meaning “at least one is a BigInt”:

extension (x: into BigInt)
  def + (other: into BigInt): BigInt = x.add(other)

“Two hundred method” implicit conversion/extension methods are not uncommon: e.g. String => Seq and Array => Seq in the standard library fall into this category. Not every day code to define, but certainly everyday code to use

Note that you can use exports in extension method blocks. I believe they can address the problem if there are many methods. Maybe not always but often.

What about docs? I guess the docs are not “exported” so that scaladoc/Metals is exposing underlying doc strings?

That’s a good question. I don’t know what scaladoc does for exports. But it seems reasonable that it should pick the original docstring of the method for which it generated a forwarder. That’s an issue for exports in general, not just extension methods.

Found this issue by @markehammons

1 Like

Oh I didn’t know that. That should indeed mostly satisfy the use case. IMO it doesn’t need to be pretty, as long as some mechanism for avoiding O(n) duplicate boilerplate exists

2 Likes

It turns out I can omit the import of operators in this slightly modified design, with given instances provided as member fields of their own classes:

object r extends Numeric[BigInteger]:
  given instance: r.type = this
  given (Int => BigInteger) = BigInteger.valueOf(_)
  extension (x: BigInteger) def add(y: BigInteger) = x.add(y)

import r.{given}

object s extends PolyRing[BigInteger]:
  given instance: PolyRing[BigInteger] = this

import s.{given}

BigInteger("1") + 1
1 + BigInteger("1")
Poly(BigInteger("1")) + BigInteger("1")
BigInteger("1") + Poly(BigInteger("1"))
Poly(BigInteger("1")) + 1
1 + Poly(BigInteger("1"))

Here, the second import simply supersedes the first one and we do not need to chose which operator we want.

Notice that these given instances must be named and named the same. For instance the following does not work:

object r extends Numeric[BigInteger]:
  given instance1: r.type = this
...
object s extends PolyRing[BigInteger]:
  given instance2: PolyRing[BigInteger] = this
...

In summary, a lot of intuitive invariants are broken (extension methods of given instances are put in scope but differently than if imported, importing given instances is not the same as defining said instances, and has different meaning whether they are named or not, and depending of the name) just for the sake of limiting the burden on implicit resolution. As a result, the burden is on the developer and it makes the programming experience a real minefield.

What is it that prevents us from linking to such library as sat4j and explore the implicit solution space systematically and reliably?

What I meant is that for instance the above trick does not work in the classic approach:

import java.math.BigInteger
import scala.language.implicitConversions

trait Numeric[T]:
  extension (x: T) def + (y: T): T

object r extends Numeric[BigInteger]:
  given instance: r.type = this
  given Conversion[Int, BigInteger] = BigInteger.valueOf(_)
  extension (x: BigInteger) def + (y: BigInteger) = x.add(y)

import r.given

1 + BigInteger("1")
^^^
None of the overloaded alternatives of method + in class Int with types
...
match arguments (java.math.BigInteger)

This is yet another breach of invariant : how comes the two approaches are not equivalent?

Because supporting 1 + BigInteger("1") requires major acrobatics and lots of special casing. It’s amazing it works at all. If you are unhappy about the lack of equivalence then the default solution would be to outlaw it everywhere. Sorry I don’t have better news.

1 Like