13

I'd like to pass an object to a function that accepts an argument with a projected type, and get Scala to deduce that the object's type comes from the object that encloses it. Here's some simple code to illustrate the difficulty:

trait Cult { cult_ =>
  case class CultLeader(personality: Personality) {
    val cult = cult_
    val follower = personality.attractFollower(this)
  }
  case class Follower(leader: CultLeader, name: String)
}

trait Personality {
  def attractFollower(leader: Cult#CultLeader) =
    leader.cult.Follower(leader, "Fred")   <-- THIS LINE FAILS TO COMPILE
}

In other words, a CultLeader’s Personality should attract a Follower to the same Cult as the CultLeader.

The Scala 2.11.2 compiler says:

TypeProjection.scala:11: error: type mismatch;
 found   : Cult#CultLeader
 required: leader.cult.CultLeader
    leader.cult.Follower(leader, "Fred")
                         ^

It compiles and runs correctly if I add a cast, like this:

    leader.cult.Follower(leader.asInstanceOf[leader.cult.CultLeader], "Fred")

That seems clumsy and it introduces run-time checking for something that ought to be deducible at compile-time. At least I have a workaround. How can I get the Scala compiler to deduce that leader's type is in fact leader.cult.CultLeader?

I'd prefer not to pass cult as another argument to attractFollower. In my actual code, that could result in a lot of ugly passing around of the cult parameter—when it really shouldn't need to be passed at all.

Ben Kovitz
  • 4,279
  • 1
  • 17
  • 40

1 Answers1

9

The simple way is:

trait Cult { cult_ =>
  case class CultLeader(personality: Personality) {
    val cult = cult_
    val follower = personality.attractFollower(this)
  }
  case class Follower(leader: Cult#CultLeader, name: String) // <-- Cult#CultLeader here
}

trait Personality {
  def attractFollower(leader: Cult#CultLeader) =
    leader.cult.Follower(leader, "Fred")   
}

// Exiting paste mode, now interpreting.

defined trait Cult
defined trait Personality

Here you're explicitly specifying that Follower can take any projection, which you're actually trying to force with asInstanceOf.


There is another way to do that:

trait Cult {
  case class CultLeader(personality: Personality) {
    def fl(name: String) = Follower(this, name)
    val follower = personality.attractFollower(this)
  }
  case class Follower(leader: CultLeader, name: String)
}

trait Personality {
  def attractFollower(leader: Cult#CultLeader) = leader.fl("Fred")
}

or

trait Cult {
  case class CultLeader(personality: Personality) { ld =>
    val follower = personality.attractFollower(this)
    case class Follower(name: String) { val leader = ld }
  }
}

trait Personality {
  def attractFollower(leader: Cult#CultLeader) = leader.Follower("Fred")   
}

UPDATE: This example could make it more clear why Scala is doing what she's doing:

trait Cult { cult_ =>
  case class CultLeader(personality: Personality) {
    val cult: Cult = cult_ //could be new Cult{} as well
    val l = this.asInstanceOf[cult.CultLeader] //We have to do asInstanceOf here because Scala have no glue (from type signature) that this cult is same as cult_
    val follower = personality.attractFollower(this)
  }

  case class Follower(leader: CultLeader, name: String)
}

trait Personality {
  def attractFollower(leader: Cult#CultLeader) = 
    leader.cult.Follower(leader.l, "Fred")   
} 

// Exiting paste mode, now interpreting.
defined trait Cult
defined trait Personality

And here is final solution which does what you want in correct way:

trait Cult { cult_ =>
  case class CultLeader(personality: Personality) { 
    val cult: cult_.type = cult_
    val l: cult.CultLeader = this
    val follower = personality.attractFollower(this)
  }

  case class Follower(leader: CultLeader, name: String)

}

trait Personality {
  def attractFollower(leader: Cult#CultLeader) = 
    leader.cult.Follower(leader.l, "Fred")   
}  

// Exiting paste mode, now interpreting.
defined trait Cult
defined trait Personality

The catch here is that cult_.type is path-dependent (projected).

dk14
  • 21,273
  • 4
  • 45
  • 81
  • Is there a way to require that a Follower be from the same Cult as the Follower's `leader`? – Ben Kovitz Jul 14 '15 at 00:00
  • Yes, that was your prvious solution - however you can't pass a leader of **any** cult to follower which requires a leader from **concrete** cult - there is no way to check it in compile-time as Scala doesn't know how exactly your `attractFollower` is gonna be called (it could be a code outside build module). So your solution is like passing `Any` when function requires `Int`. So no way to require concrete cult-projection and pass any projection simultaneusly - it's logically incorrect. – dk14 Jul 14 '15 at 01:06
  • Even though `attractFollower` can be called from anywhere, shouldn't it be able to create a Follower of the same Cult as the leader that was passed to `attractFollower`? – Ben Kovitz Jul 16 '15 at 15:39
  • I've added several other ways – dk14 Jul 16 '15 at 17:58
  • Hmm, #2 looks good. Apparently the trick is to call the constructor from source code that physically sits inside the enclosing class definition. #3 makes Follower path-dependent on the CultLeader object, which I don't think is right since a Follower should be able to switch between CultLeaders, or a different Personality should attract different kinds of Followers, etc. I'm mainly seeking understanding of the type-inference problem and how to get around it in general. #2 might do it; I'll experiment with my real application code (which has nothing to do with cults). – Ben Kovitz Jul 21 '15 at 07:22
  • @BenKovitz I'm glad that your real application code has nothing to do with cults :) – dk14 Jul 21 '15 at 07:43
  • @BenKovitz I've updated the answer with final solution and explanation – dk14 Jul 25 '15 at 10:23