Check if two types are equivalent through reflection in scala

55 Views Asked by At

I have some code:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

trait LogicUnit

trait Ezib extends LogicUnit
trait Zieb extends LogicUnit

object LogicUnit {

    @inline 
    def combine[A <: LogicUnit, B <: LogicUnit](arg1: A, arg2: B): Future[LogicUnit] = {

        if (arg1.isInstanceOf[Ezib] && arg2.isInstanceOf[Ezib]) return Future(new Ezib {})
        else if (arg1.isInstanceOf[Zieb] && arg2.isInstanceOf[Zieb]) return Future(new Zieb {})

        else if (arg1.isInstanceOf[Ezib] && arg2.isInstanceOf[Zieb]) return Future(new Zieb {})
        else return Future(new Ezib {})
    }
}

Since this code will be run a lot I am trying to optimize it a lot hence, I am trying to combine the first two lines of the combine function. To do so I figured the code would look something like this:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

trait LogicUnit

trait Ezib extends LogicUnit
trait Zieb extends LogicUnit

object LogicUnit {

    @inline 
    def combine[A <: LogicUnit, B <: LogicUnit](arg1: A, arg2: B): Future[LogicUnit] = {

        if (arg1.isInstanceOf[typeOf(arg2)]) return Future(arg1)

        else if (arg1.isInstanceOf[Ezib] && arg2.isInstanceOf[Zieb]) return Future(new Zieb {})
        else return Future(new Ezib {})
    }

}

Is there any way I can get the code to just check if the two types are the same so I don't have to match for all cases?

1

There are 1 best solutions below

2
On BEST ANSWER

There are a number of issues here:

It is unlikely that these tests are going to make any difference to the performance because creating and running a Future is a much more complex operation and will dominate the time.

The type parameters are unnecessary since the values are always treated as LogicUnit so the signature can just be:

def combine(arg1: LogicUnit, arg2: LogicUnit): Future[LogicUnit]

It is better to use match rather than isInstanceOf because the compiler then knows what the type of the argument is.

The first version does not depend on the type of arg1 so there is no need to test it:

def combine(arg1: LogicUnit, arg2: LogicUnit): Future[LogicUnit] =
  arg2 match {
    case _: Ezib => Future(new Ezib {})
    case _: Zieb => Future(new Zieb {})
  }

If you do need to test both arguments, nest the tests so that the first test is not repeated:

def combine(arg1: LogicUnit, arg2: LogicUnit): Future[LogicUnit] =
  arg1 match {
    case a1: Ezib =>
      arg2 match {
        case a2: Ezib => Future(???)
        case a2: Zieb => Future(???)
      }
    case a1: Zieb =>
      arg2 match {
        case a2: Ezib => Future(???)
        case a2: Zieb => Future(???)
      }
  }

This formulation is clear and readable, and it allows each branch to use a value of a1 and a2 that are known by the compiler to be the specific type that has been matched.

It may also be the case that using polymorphic versions of combine would improve performance:

def combine(arg1: Ezib, arg2: Ezib): Future[LogicUnit] = ???
def combine(arg1: Ezib, arg2: Zieb): Future[LogicUnit] = ???
def combine(arg1: Zieb, arg2: Ezib): Future[LogicUnit] = ???
def combine(arg1: Zieb, arg2: Zieb): Future[LogicUnit] = ???

def combine(arg1: LogicUnit, arg2: LogicUnit): Future[LogicUnit] =
  arg1 match {
    case a1: Ezib =>
      arg2 match {
        case a2: Ezib => combine(a1, a2)
        case a2: Zieb => combine(a1, a2)
      }
    case a1: Zieb =>
      arg2 match {
        case a2: Ezib => combine(a1, a2)
        case a2: Zieb => combine(a1, a2)
      }
  }

If the concrete type is known at the call site then the compiler can call the appropriate method directly rather than having to match the type.