0

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.

1 Answers1

1

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]])
  }
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • I believe the simples solution would be to make a simple ADT for the possible types for attributes and rules. – Luis Miguel Mejía Suárez Sep 26 '20 at 15:19
  • @LuisMiguelMejíaSuárez What if we do `filter{ case x: Property[t] => x.conformsTo(ruleMap(x.name).asInstanceOf[Rule[t]]) }`? – Dmytro Mitin Sep 26 '20 at 15:30
  • I really do not know too much about type patterns, I believe that works but in my mind it is unsafe. – Luis Miguel Mejía Suárez Sep 26 '20 at 15:37
  • Thank you for the answers. @LuisMiguelMejíaSuárez - are you able to elaborate, you mean an algebraic data type? – hdevone Sep 26 '20 at 15:38
  • @hdevone yeah something like `sealed trait ValidType` - `final case class DoubleType(d: Double) extends ValidType`, etc and then `trait Property[T <: ValidType] {` & `trait Rule[T <: ValidType] {` - That way in the filter you can match both the **property** and **rule** to determine they both have the same type, but it can be quite verbose. – Luis Miguel Mejía Suárez Sep 26 '20 at 16:05
  • @LuisMiguelMejíaSuárez Do you mean something like that https://scastie.scala-lang.org/iD7fNrGKTaebDGJCcUTX6Q ? – Dmytro Mitin Sep 27 '20 at 15:28
  • 1
    @DmytroMitin yeah, as I said, your alternative is probably safe and obviously shorter. But being honest I would do what you did in that Scastie since that way I am sure it is safe. Note that, such a decision is my personal one, mainly driven my ignorance and _(somewhat)_ fear to type checks; not saying OP should do that. – Luis Miguel Mejía Suárez Sep 27 '20 at 16:13
  • Thank you for all your answers. I guess I'll make do with the manual type check approach until union types come with scala 3 :). – hdevone Sep 28 '20 at 11:42
  • @hdevone Union types can help with `Property[Any]` or `Property[ValidType]` -> `Property[Double | String]` but they can't help with ordinary list and map not preserving individual element types so that you have type mismatch `attributes: Seq[Property[Double | String]]` vs. `ruleMap: Map[String, Rule[Double with String]]`. Even with union types you have mismatch union type vs. intersection type. – Dmytro Mitin Sep 28 '20 at 12:11