Equals on scalajs facade

I am converting rdflib.js a Typescript library to ScalaJS using Scalably Typed. I have come across a problem because the typescript code has a method

  equals (other: Term): boolean

which translates to this

   def equals(other: Term): Boolean

This creates the following problems:

  1. the def equals(other: Term): Boolean signature is I think not quite right to be picked up by Scalajs for calls to ==. I can use term1.equals(term2) and get true but get false for term1 == term2. But I also don’t get assertEquals(a, b) to work in munit.
  2. I can’t add the signature for def equals(other: Any) so that it can call the other more narrow sig as there is a compiler error stating that equals is final when I do that, related to the change in ScalaJS 0.6.33 I believe. I also tried injecting the right equals dynamically, but that did not seem to work.
  3. Adding a == extension method on an opaque type for the class does not help: it does not get called. (Adding === does get called, but then that method is not used by unit tests or scala’s collection libraries).

I may be missing something obvious, as I have only just got back to using ScalaJS in the Scala 3.1.0-RC2 compiler.

There was a Stack Exchange Question on this topic, but the author was only able to provide a partial answer, namely by overloading equals to use another symbol, namely ==

 @JSName("equals")
  def ==(that: MyType): Boolean = js.native

That does not work for me

  • because I am accessing these types via opaque types, and I am having trouble reassigning == in extension methods as mentioned in 3 above
  • but furthermore, I would like the equals to be useable in collections code and munit assertEquals, …

There’s nothing you can do. == means equals(Any): Boolean, and that always means eq for JS types. It cannot be overridden.

If you want to customize == for your class, it must not extend js.Any.

Thanks for that clarification.
Why is that? Is it a restriction imposed by JS?

So how do I get to use an equals method defined in a JS library in my Scala code?

I want this equals to be used by munit and collection code in Scala.

I started off hoping that the new Scala 3 multiversal Equality’s CanEquals trait would come with a def ==(other: T): Boolean method. Something like the Cats Eq typeclass. But it does not.

Having that would of course require all the Scala collection libs to be rewritten to search for equality in such a given. But it would have allowed me to specify the equals I needed separately.

So I seem to be able to get it partially to work.
First I specified the following in the facade

   override def equals(other: Term): Boolean = js.native
   @JSName("equals")
    override def ==(other: Term): Boolean = js.native

I have a specialisation of an trait where these are specified

override opaque type Node <: Matchable = rdfTp.Term
override opaque type URI <: Node = rdfTp.NamedNode

and then for the case of URIs I have tried to set the following

extension(uri: URI)
	def ==(other: Any) =
		println("using extension == on URI in rdflib ops")
		uri.equals(other)
	def !=(other:  Any) =
		println("using extension != on URI in rdflib ops")
		!(uri.equals(other))
	override  def ===(other: RDF.URI[R]) =
		println("using extension === on URI in rdflib ops")
		uri.equals(other)
	override def !==(other: RDF.URI[R]) =
		println("using extension !== on URI in rdflib ops")
		!(uri.equals(other))

And I have the following munit tests

test("FOAF Prefix") {
	assert(!(foaf.age !== URI(foafPre("age"))))
	assert(foaf.age === URI(foafPre("age")))
	assert(!(foaf.age != URI(foafPre("age"))))
	assert(foaf.age == URI(foafPre("age")))
	assertEquals(foaf.knows, URI(foafPre("knows")))
	assertEquals(foaf.homepage, URI(foafPre("homepage")))
}

The first two tests using === and !== give the right result, and get called.
So that means the underlying equals in the JS object is getting called.

But I have not been able to get the == and != methods to be called, even after trying some obvious options. So the last 4 tests fail. (I found that assertEquals is implemented in terms of !=.)

I also tried of course without the == and != extension methods hoping it could pick up the == method in the object.

So perhaps the problem is really one of trying to override == on opaque types…

yes, the Scala 3 doc on opaque types states that

unless another overloaded == or != operator is defined for the type.

That does not make it clear if the

  1. overloaded operator is to be defined in the extension method or on the underlying object
  2. what the signature of the method should be

It does seem indeed that one can’t override the == in opaque types. I adapted a bit the following code from @velvetbaldmime to test it

class IntObj(val i: Int) {
  override def equals(obj: Any): Boolean =
    println("in IntObj.equals(obj: Any)")
    false

  def equals(other: IntObj): Boolean =
    println("in IntObj.equals(other: IntObj)")
    other.i == i
}

object Hello {
  opaque type Hello = IntObj
  def apply(i: Int): Hello = IntObj(i)

  extension (ih: Hello)
    def ==(other: Any): Boolean =
      println("we are in ==(other: Any)")
      other match
        case io: IntObj => ih.i >= (2 * io.i)
        case _ => true
    def ==(other: Hello): Boolean =
      println("we are in ==(other: Hello)")
      ih.i >= (2 * other.i)
}

object test {
  import Hello.*

  def main(a: Array[String]): Unit =
    val x: Hello = Hello(5)
    val y: Hello = Hello(50)
    val z: Hello = Hello(25)

    println(y == x)
    println(x == z)
}

compiling it with scala test.scala I could then run it with scala test to get the result wrong result, with only as output to the console

> scala test
in IntObj.equals(obj: Any)
false
in IntObj.equals(obj: Any)
false

One decompiling the source code we get for the Hello class:

public final class Hello$ implements Serializable
{
    public static final Hello$ MODULE$;
    
    private Hello$() {
    }
    
    static {
        MODULE$ = new Hello$();
    }
    
    private Object writeReplace() {
        return new ModuleSerializationProxy((Class)Hello$.class);
    }
    
    public IntObj apply(final int i) {
        return new IntObj(i);
    }
    
    public boolean $eq$eq(final IntObj ih, final Object other) {
        Predef$.MODULE$.println((Object)"we are in ==(other: Any)");
        boolean b;
        if (other instanceof IntObj) {
            final IntObj io = (IntObj)other;
            b = (ih.i() >= 2 * io.i());
        }
        else {
            b = true;
        }
        return b;
    }
    
    public boolean $eq$eq(final IntObj ih, final IntObj other) {
        Predef$.MODULE$.println((Object)"we are in ==(other: Hello)");
        return ih.i() >= 2 * other.i();
    }
}

so that shows that the == methods find their way into the byte code.
But they are not called from the main method, which only calls equals.

 public void main(final String[] a) {
        final IntObj x = Hello$.MODULE$.apply(5);
        final IntObj y = Hello$.MODULE$.apply(50);
        final IntObj z = Hello$.MODULE$.apply(25);
        final Predef$ module$ = Predef$.MODULE$;
        final IntObj intObj = y;
        final IntObj obj = x;
        boolean b = false;
        Label_0060: {
            Label_0059: {
                if (intObj == null) {
                    if (obj != null) {
                        break Label_0059;
                    }
                }
                else if (!intObj.equals(obj)) {
                    break Label_0059;
                }
                b = true;
                break Label_0060;
            }
            b = false;
        }
        module$.println((Object)BoxesRunTime.boxToBoolean(b));
        final Predef$ module$2 = Predef$.MODULE$;
        final IntObj intObj2 = x;
        final IntObj obj2 = z;
        boolean b2 = false;
        Label_0100: {
            Label_0099: {
                if (intObj2 == null) {
                    if (obj2 != null) {
                        break Label_0099;
                    }
                }
                else if (!intObj2.equals(obj2)) {
                    break Label_0099;
                }
                b2 = true;
                break Label_0100;
            }
            b2 = false;
        }
        module$2.println((Object)BoxesRunTime.boxToBoolean(b2));
    }

Now this is in the output from the Java version which I think is the same as for the JS.

So the problem is again that there does not seem to be a way to specify the equals method for opaque types, which seems like a pretty useful thing to be able to do.

== delegating to equals(Any): Boolean, and the latter being overridable in any subclass, works because equals(Any): Boolean is a standard method of every object in the Scala world. In JavaScript, there no such universal notion of an overridable equality method. Therefore, when we have a JS type, the only thing we can do for == is to test the reference equality, eq.

You might ask why we can’t follow the facade types and take their own equals methods. That’s because facades are just that: facades. At run-time, they have no meaning. If I hold a value x of a JS type, there is absolutely no way to test-and-decide what facade type describes it. Therefore, I cannot look for a custom equals method in a facade type. All I know is that I have a JS value in my hands, and no universal JS way to perform structural equality, so I always use reference quality.

You cannot. MUnit and collection code in Scala use the Any.==(Any): Boolean method which always delegates to Any.equals(Any): Boolean. And as stated above, for JS types the latter is always equivalent to eq.

Hello, what about using the mathematical equivalence <=> ?

If I understand your question correctly, you are looking for a method name for your “custom equals method”. If I misunderstood something, feel free to correct me.

You could add a new method but then you would have the problem of how that integrates with the existing Scala libraries. They use the def equals(x: Any): Boolean when comparing for equality. So if you have an <=> method but none of the code in the major libraries uses it, then it is not much use.

One can implement that as with Cats, but it would not be very useful if the Set and HashMap and other libraries never use the new Equals methods, be It your <=> or the Eq.eqv method.

This is really tricky to grasp.

  1. I have a JS library that has its own equals methods which its libs use for equality comparisons.
  2. we have Scala librariest like HashSet that need an equality function on the objects they store to work.

Could one not monkey-patch the JS code to add some method with a long name that is completely unlikely to lead to a name clash and that gets called when Scala code calls an equals(o: Any) method? This method would then delegate to the right equals method as specified in the facade?

I guess that has been thought of, but I was just wondering…

One JS library that defines its own equals methods on some of its data types does not mean that suddenly there is universal equality available for JS types. Even for those types, as we cannot know whether the right-hand-side is an instance of the same class. So even when you have

trait Foo extends js.Object {
  def equals(that: Foo): Boolean
}

and let’s say you have some x and y typed as Any. How would you possibly know to call that method? First you’d have to tell that x is actually an instance of Foo. There is no way to do that. So you’re already stuck.

OK let’s say we have that hypothetical __scalaEquals method monkey-patched on all instances of Foo. Then sure you’d be able to tell whether x has such a method, and call it with y. But how would the implementation of __scalaEquals tell whether y is in fact an instance of Foo, so that it can then compare its fields? Again, it cannot, for the same reason that we could not tell whether x was a Foo. So you’re stuck again.

As you can see, even if we did add a magic __scalaEquals method, it would not solve all issues. That’s quite hypothetical anyway, because Scala.js doesn’t do magic. Scala.js leaves JS types alone, and for JS types, it uses JS semantics. The language and its interoperability stands its grounds because it preserves the JS semantics of JS types. Any deviation from that would endanger the integrity of the language, and that is not something I would be willing to do.

1 Like