shiuu
shiuu

Reputation: 163

Scala issue about option.contains()

It seems option.contains() does not always work as expected. I have the following code:

case class Person (name: String, nickName: Option[String])

val people = Seq(Person("Ned", Some("d")), Person("Alex", None))
val suspects = Map("c" -> 1, "d" -> 2)

val result1 = people.filter(_.nickName.contains(suspects.contains(_)))
val result2 = people.filter{ p =>
    p.nickName.contains{ n =>
        suspects.contains(n)
    }
}
    
println(result1)
println(result2)

You might expect result1 and result2 contains a person, but they are actually empty. Why?

It turns out the follow code works:

val result3 = people.filter{ _.nickName match {
        case Some(n) => suspects.contains(n)
        case None => false
    }
}
val result4 = people.filter( _.nickName.map(suspects.contains).getOrElse(false))
val result5 = people.filter( _.nickName.fold(false)(suspects.contains))

println(result3)
println(result4)
println(result5)

I have tried on both Scala 2.12 and 2.13, and results are the same. Why nickName.contains does not work?

Upvotes: 0

Views: 3127

Answers (1)

Boris Azanov
Boris Azanov

Reputation: 4481

Short answer on your question: it happens because you are using Option contains method wrong when trying to compare function (String => Boolean) with option value which is String.

But there are some difficulties with contains method in Option. Let's take a look at contains definition in scala source code:

/**@example {{{
 *  // Returns true because Some instance contains string "something" which equals "something".
 *  Some("something") contains "something"
 *
 *  // Returns false because "something" != "anything".
 *  Some("something") contains "anything"
 *
 *  // Returns false when method called on None.
 *  None contains "anything"
 *  }}}
 *
 *  @param elem the element to test.
 *  @return `true` if the option has an element that is equal (as
 *  determined by `==`) to `elem`, `false` otherwise.
 */
final def contains[A1 >: A](elem: A1): Boolean =
  !isEmpty && this.get == elem

We see that contains compares containing value of Option[A] with some passed element with type A1 that has A as a lower type bound [A1 >: A]. It means the type parameter A1 or the abstract type A1 refer to a supertype of type A. It means we can pass any supertype of String type to check contains for example Any. In your case you are trying to pass into contains function String => Boolean but take a look at Function1 definition:

trait Function1[@specialized(scala.Int, scala.Long, scala.Float, scala.Double) -T1, @specialized(scala.Unit, scala.Boolean, scala.Int, scala.Float, scala.Long, scala.Double) +R] extends AnyRef

it is a trait which extends AnyRef (and Any also). So for compiler it's a legal case, it checks what String is a subtype of Any and compiles code. In runtime it tries to compare String value and function String => Boolean (String != String => Boolean) and this is the reason that contains returns you always false and filter empty list.

So to solve your main problem - filter list correctly using map, it is better to use exists function:

val people = Seq(Person("Ned", Some("d")), Person("Alex", None))
val suspects = Map("c" -> 1, "d" -> 2)

val resultExist = people.filter { p =>
  p.nickName.exists {
    suspects.contains
  }
}
val resultExistUnsugar = people.filter { p =>
  p.nickName.exists {
    n => suspects.contains(n)
  }
}
println(resultExist) // List(Person(Ned,Some(d)))
println(resultExistUnsugar) // List(Person(Ned,Some(d)))

To protect type-safety your code I would recommend you to set concrete type parameter when you are using contains method:

// it will not compile because suspects.contains type is not String
val result1 = people.filter(_.nickName.contains[String](suspects.contains))
// it will not compile also by the same reason
val result2 = people.filter { p =>
  p.nickName.contains[String] {
    n => suspects.contains(n) 
  }
}

But we are still don't know why contains has type parameter bounds [A1 >: A]. To clarify this let's look at Option[+A] class definition:

sealed abstract class Option[+A] extends Product with Serializable { // ... }

Here we see that Option has covariance type parameter +A and this is the reason that we should add parameter bounds to contains method. See more in this thread why.

In conclusion:

  • Be careful using contains method in all containers not only in Option
  • It's better to bound some methods which can be type parametrized for type-safety and more predictable behavior
  • More about lower type bounds and covariance on docs.scala-lang

Upvotes: 5

Related Questions