shayan
shayan

Reputation: 1241

Use implicits to infer an unknown type

I am trying to get the compiler to infer some type based on 2 other type parameters that are known. here's the example:

trait ReturnCount
trait ReturnsMany extends ReturnCount
trait ReturnsOne extends ReturnCount

class Query[R <: ReturnCount]{
    def join[R2 <: ReturnCount,R3 <: ReturnCount](q: Query[R2]): Query[R3]
}

As you can see here I want to be able to join two read queries (the details of composition for which is irrelevant). the resulting new query has to be a query that either ReturnsOne or ReturnsMany results. the rule of resolution is also pretty simple: Only if both queries ReturnsOne then the joined query also ReturnsOne in all other cases it ReturnsMany.

So:

val q1 = new Query[ReturnsOne]
val q2 = new Query[ReturnsMany]
val q3 = q1 join q2 //I don't want to have 
                    //to specify R3 because compiler should do it for me...

How can I hope to achieve this?

Upvotes: 3

Views: 141

Answers (1)

Jasper-M
Jasper-M

Reputation: 15086

This is how you would do such a thing for the case where those 3 traits are sealed. If they are open to extension it will probably become more complicated.

It boils down to the following. You have a trait Fancy with two input types A and B, and one output type Out. You define the implicit instances such that in the case where A and B are both ReturnsOne, Out will be ReturnsOne. In all other cases you fall back to a default case with a lower priority (because otherwise you will get ambiguity errors) where Out always is ReturnsMany.

scala> :paste
// Entering paste mode (ctrl-D to finish)

sealed trait ReturnCount
sealed trait ReturnsMany extends ReturnCount
sealed trait ReturnsOne extends ReturnCount

sealed trait Fancy[A, B] {
  type Out <: ReturnCount
}

object Fancy extends LowerPriority {
  type Aux[A,B,Out1 <: ReturnCount] = Fancy[A,B] { type Out = Out1 }

  implicit def returnsOne: Fancy.Aux[ReturnsOne,ReturnsOne,ReturnsOne] = 
    new Fancy[ReturnsOne,ReturnsOne] { type Out = ReturnsOne }
}

trait LowerPriority {
  implicit def returnsMany[A,B]: Fancy.Aux[A,B,ReturnsMany] = 
    new Fancy[A,B] { type Out = ReturnsMany }
}

class Query[R <: ReturnCount] { 
  def join[R2 <: ReturnCount](q: Query[R2])(implicit fancy: Fancy[R,R2]): Query[fancy.Out] = ??? 
}


// Exiting paste mode, now interpreting.

defined trait ReturnCount
defined trait ReturnsMany
defined trait ReturnsOne
defined trait Fancy
defined object Fancy
defined trait LowerPriority
defined class Query

scala> :type new Query[ReturnsOne].join(new Query[ReturnsOne])
Query[ReturnsOne]

scala> :type new Query[ReturnsOne].join(new Query[ReturnsMany])
Query[ReturnsMany]

scala> :type new Query[ReturnsMany].join(new Query[ReturnsMany])
Query[ReturnsMany]

scala> :type new Query[ReturnsMany].join(new Query[ReturnsOne])
Query[ReturnsMany]

In a real scenario your Fancy will probably also need a join method which will contain the actual implementations that your Query#join method delegates to:

class Query[R <: ReturnCount] { 
  def join[R2 <: ReturnCount](q: Query[R2])(implicit fancy: Fancy[R,R2]): Query[fancy.Out] =
    fancy.join(this, q)
}

This blog might be a good starting point to learn more about such patterns.

Upvotes: 3

Related Questions