hdevone
hdevone

Reputation: 1

Problem determining parameter type when calling a method

I'm sorry for the vague or possibly incorrect question, but I'm not even sure how to state my problem in one sentence.

I have 2 traits:

trait Property[T] {
  val name: String

  def conformsTo(rule: Rule[T]): Boolean
}

and

trait Rule[T] {
  def evaluate(valueToCheck: T): Boolean
}

I have a list of concrete classes implementing Property and a map of Rule implementations, where both parameterized types can either be a Double or a String. When traversing this list, for each Property instance I pass in a concrete Rule implementation to the conformsTo method, the compiler however errors with a type mismatch.

So in a broader sense, what I'm trying to achieve is to have a list of attributes that can be evaluated based on a given set of rules of different types, without splitting these attributes into separate lists by type.

I tried tinkering with upper/lower bounds and implicit types but ended up with nothing.

The complete code:




trait Rule[T] {
  def evaluate(valueToCheck: T): Boolean
}

case class GreaterThan(value: Double) extends Rule[Double] {
  override def evaluate(valueToCheck: Double): Boolean = {
    valueToCheck > this.value
  }
}

case class LessThan(value: Double) extends Rule[Double] {
  override def evaluate(valueToCheck: Double): Boolean = {
    valueToCheck < this.value
  }
}

case class Is(value: String) extends Rule[String] {
  override def evaluate(valueToCheck: String): Boolean = {
    valueToCheck == this.value
  }
}

case class isNot(value: String) extends Rule[String] {
  override def evaluate(valueToCheck: String): Boolean = {
    valueToCheck != this.value
  }
}

trait Property[T] {
  val name: String

  def conformsTo(rule: Rule[T]): Boolean
}

case class DoubleProperty(name: String, value: Double) extends Property[Double] {
  override def conformsTo(rule: Rule[Double]): Boolean = rule.evaluate(value)
}

case class StringProperty(name: String, value: String) extends Property[String] {
  override def conformsTo(rule: Rule[String]): Boolean = rule.evaluate(value)
}

object Evaluator extends App {

  val ruleMap = Map(
    "name1" -> GreaterThan(123),
    "name1" -> LessThan(500),
    "name2" -> GreaterThan(1000),
    "name3" -> Is("something"))
  val numericProperty = DoubleProperty("name1", 600)
  val numericProperty2 = DoubleProperty("name2", 1000)
  val stringProperty = StringProperty("name3", "something")
  val stringProperty2 = StringProperty("name4", "something")

  val attributes = List(
    numericProperty,
    numericProperty2,
    stringProperty,
    stringProperty2)

  val nonConforming = attributes
    .filter(x => ruleMap.contains(x.name))
    .filter(x => !x.conformsTo(ruleMap(x.name))).toList

}

the error:

type mismatch;
 found   : Product with Rule[_ >: String with Double] with java.io.Serializable
 required: Rule[(some other)<root>._1]
Note: Any >: _1 (and Product with Rule[_ >: String with Double] with java.io.Serializable <: Rule[_ >: String with Double]), but trait Rule is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
    .filter(x => !x.conformsTo(ruleMap(x.name))).toList

Thank you for any help.

Upvotes: 0

Views: 92

Answers (1)

Dmytro Mitin
Dmytro Mitin

Reputation: 51658

Firstly, based on error message it can make sense to extend Rule from Product with Serializable

https://typelevel.org/blog/2018/05/09/product-with-serializable.html

Then error message changes to:

type mismatch;
 found   : App.Rule[_1(in value $anonfun)] where type _1(in value $anonfun) >: String with Double
 required: App.Rule[<root>._1]

Secondly, you can make Rule contravariant

trait Rule[-T] extends Product with Serializable {
  def evaluate(valueToCheck: T): Boolean
}
type mismatch;
 found   : App.Rule[String with Double]
 required: App.Rule[_1]

and Property covariant

trait Property[+T] {
  val name: String
  def conformsTo(rule: Rule[T]): Boolean
}
type mismatch;
 found   : App.Rule[String with Double]
 required: App.Rule[Any]

The thing is that this not gonna work with List. Elements of attributes have types Property[Double] and Property[String]. But when you add them to a list they lost their individual types and become just Property[Any] (Property[Double | String] if there were union types in Scala 2).

Similarly, values of ruleMap have types Rule[Double] and Rule[String]. But when you put them with some keys into a map they lost their individual types and become just Rule[Double with String] (or even Rule[Nothing]).

If you didn't modify variances of Property and Rule then the types would be Property[_] and Rule[_] (actually, Property[_1] and Rule[_2]) where type parameters don't correspond to each other.

But the signature of conformsTo says that property.conformsTo(rule) compiles only when arguments have types Property[T] and Rule[T] for the same T, which you can't guarantee.

You could try to use HList instead of List and HMap instead of Map in order to keep individual types of elements. But then in order to make filter work you should know whether ruleMap.contains(x.name) at compile time. So you would have to make keys have singleton types and trait Property to depend on this one more type parameter trait Property[+T, S <: String with Singleton] { val name: S ...

Why Does This Type Constraint Fail for List[Seq[AnyVal or String]]

Scala: verify class parameter is not instanceOf a trait at compile time

How to make a typeclass works with an heterogenous List in scala

Use the lowest subtype in a typeclass?

flatMap with Shapeless yield FlatMapper not found


The easiest way to make your code compile is

val nonConforming = attributes
  .filter(x => ruleMap.contains(x.name))
  .filter{
    case x: Property[t] => !x.conformsTo(ruleMap(x.name).asInstanceOf[Rule[t]])
  }

Upvotes: 1

Related Questions