Search code examples
scalatype-mismatchpath-dependent-type

How to avoid awful type casts working with path dependent types?


I am new to Scala and dont know why i have to do an (unintuitive for me) type cast related to path dependent types in the following code. (I don't like getters, setters nor nulls, they are here to separate operations and disambiguate the source of errors)

// Module A public API
class ModA {
  trait A
}

// Module B public API that depends on types defined in Module A
class ModB(val modA: ModA) {
  trait B {
    def getA: modA.A;
    def setA(anA: modA.A);
  }
}

// One implementation of Module A
class ModAImpl extends ModA {
  class AImpl extends A
}

// One implementation of Module B
class ModBImpl(mod: ModA) extends ModB(mod) {
  class BImpl extends B {
    private[this] var privA: modA.A = _;
    override def getA = privA;
    override def setA(anA: modA.A) = privA = anA;
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    // wiring the modules
    val modAImpl = new ModAImpl;
    val modBImpl = new ModBImpl(modAImpl);

    // wiring objects
    val a = new modAImpl.AImpl;
    val b = new modBImpl.BImpl;
    b.setA(a); //don't compile and complain: type mismatch;  found: modAImpl.A  required: modBImpl.modA.A

    //i have to do this horrible and coutnerintuitive cast to workaround it
    b.setA(a.asInstanceOf[modBImpl.modA.A]);

    var someA: modAImpl.A = null;
    someA = b.getA; // don't compile with same reason
    someA = b.getA.asInstanceOf[modAImpl.A]; // horrible cast to workaround

    println(a == b.getA); // however this prints true
    println(a eq b.getA); // this prints true too
  }
} 

I have read about singleton types to inform the compiler when two types are the same, but I don't know how to apply this here. Thanks in advance.


Solution

  • Let's start by simplifying your code ridding out the unnecessary complexity.

    class Aout {
        class Ain
    }
    
    class Bout(val link: Aout)  {
        class Bin(val field: link.Ain)
    }
    
    object Main {
        def main(args: Array[String]): Unit = {
            // wiring outer object
            val aout: Aout = new Aout;
            val bout: Bout = new Bout(aout);
    
            // wiring inner object
            val ain: aout.Ain = new aout.Ain;
            val bin: bout.Bin = new bout.Bin(ain); //don't compile and complain: type mismatch;  found: aout.Ain  required: bout.link.Ain
        }
    }
    

    Answer:

    The compiler complains with a type mismatch error because he compares the paths of the two declared types involved in the assignment, and they are different. The compiler is not intelligent enough to notice that at that point aout eq bout.link. You have to tell him. So, replacing the line

    val ain: aout.Ain = new aout.Ain;
    

    with

    val ain: bout.link.Ain = new bout.link.Ain;
    

    solves the path-dependent type mismatch.

    Anyway, correcting the type's path in your original code is not enough because there is also an inheritance problem. One solution to that is to make the class ModBImpl know the ModAImpl class like this:

    class ModA {
        trait A
    }
    
    class ModB[M <: ModA](val modA: M) { // type parameter added
        trait B {
            def getA: modA.A;
            def setA(anA: modA.A);
        }
    }
    
    class ModAImpl extends ModA {
        class AImpl extends A
    }
    
    class ModBImpl(mod: ModAImpl) extends ModB(mod) { // changed type of `mod` parameter from `ModA` to `ModAImpl`
    
        class BImpl extends B {
            private[this] var privA: modA.A = _;
            override def getA: modA.A = privA;
            override def setA(anA: modA.A): Unit = privA = anA;
        }
    }
    
    object Main {
        def main(args: Array[String]): Unit = {
            val modAImpl = new ModAImpl;
            val modBImpl = new ModBImpl(modAImpl);
    
            val a: modBImpl.modA.AImpl = new modBImpl.modA.AImpl; // changed the path of the type
            val b: modBImpl.BImpl = new modBImpl.BImpl;
            b.setA(a); // here IntellijIde complains with a type mismatch error, but the ScalaIDE (eclipse) and the scala compiler work fine.
        }
    }
    

    If the rules of your business don't allow that the ModBImpl class has knowledge of the ModAImpl class, tell me so we can find another solution.