Home > Software engineering >  Contains of Option[String] in Scala not working as expected?
Contains of Option[String] in Scala not working as expected?

Time:09-02

I just discovered something weird. This statement:

Some("test this").contains("test")

Evaluates to false. While this evaluates to true:

Some("test this").contains("test this")

How does this make sense? I thought the Option would run the contains on the wrapped object if possible.

EDIT:

I'm also thinking about this from a code readability perspective. Imagine you are seeing this code:

person.name.contains("Roger")

Must name be equal to Roger? Or can it contain Roger? The behavior depends if it's a String or Option[String].

CodePudding user response:

I recommend to check the docs and eventually the code of the API that you are you using. The docs detail what Option's contains does and how it works:

  /** Tests whether the option contains a given value as an element.
   *
   * This is equivalent to:
   * 
   * option match {
   *   case Some(x) => x == elem
   *   case None    => false
   * }
   * 
   *  // 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

CodePudding user response:

There's a principle in typed functional programming called "parametric reasoning". Broadly stated, the principle is that it's desirable to be able to have intuitions about what a function does just from looking at its type signature.

If we "devirtualize" (effectively turning it into a static method... this is actually a fairly common optimization step in object-oriented runtimes) Option's contains method has the signature:

def contains[A, A1 >: A](opt: Option[A], elem: A1): Boolean

That is, it takes an Option[A] and an A1 (where A1 is a supertype of A, if it's not an A) and returns a Boolean. Implicitly in Scala's typesystem, of course, we know that A and A1 are both subtypes of Any.

Without knowing anything more about what the types A and A1 are (A might be String and A1 might be AnyRef, or A and A1 might both be Int: whatever our intuition, it has to apply as much in either situation), what could we possibly do? We're basically limited to combinations of operations involving an Option[Any] and an Any which eventually get us to a Boolean (and, ideally, won't throw an exception).

For instance, opt.nonEmpty && opt.get == elem works: we can always call nonEmpty on an Option[Any] and then compare the contents using equality. We could also do something like opt.isEmpty || (opt.get.## % 43) == (elem.## % 57), but knowing that the contents of the Option and some other object have equal remainders in two different bases doesn't strike one as useful.

Note that in your specific case, because there's no contains method on an Any. What should the behavior be if we have an Option[Int]?

It might actually be useful, since we do have the ability to convert arbitrary objects into Strings via the toString method (thank you Java!), to implement a containsSubstring method on Option[A]:

def containsSubstring(substring: String): Boolean =
  nonEmpty && get.toString.contains(substring)

You could implement an enrichment class along these lines:

object Enrichments {
  implicit class OptionOps[A](opt: Option[A]) extends AnyVal {
    def containsSubstring(substring: String): Boolean =
      opt.nonEmpty && opt.toString.contains(substring)
  }
}

then you only need:

import Enrichments.OptionOps

Some("test this").containsSubstring("test")  // evaluates true

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

// Option(p).containsSubstring("Roger") would also work, assuming Person doesn't override toString...
def isRoger(p: Person): Boolean = p.name.containsSubstring("Roger")
  • Related