Search code examples
jsonscalafunctional-programmingbooleanscala-option

Scala: Does Option[Boolean] makes sense?


So an Option[Int] or Option[String] or for that matter Option[A] results in Some(A) or None, but a Boolean is different as it inherently represents dual states (true/false), does it makes sense to have Option[Boolean]? I face this frequently when a JSON response should not include the boolean field based on certain business logic.

Any thoughts?


Solution

  • Optionality is an orthogonal concern to what type your data is. So yes, Option[Boolean] makes just as much sense as say Option[Int].

    Let's talk about your particular use case.


    If the business rule says the field (say) isPrimeTime has to be of type Boolean, but is optional, then you should model it with an Option[Boolean].

    None in this context indicates absence of the value, Some(true) would mean "present and true", Some(false) would mean "present and false". This would also allow you to add defaults in your code like shown below:

    val isPrimeTime = json.getAs[Option[Boolean]]("isPrimeTime").getOrElse(false)
    

    You can employ various Option combinators for other common use cases that arise in such settings. (I would let your imagination work on that.)


    If your domain has a lot of these "tristate" fields, and if the third state indicates some domain-specific idea, something not evident from the context, I'd strongly advise creating your own sum type. Even though you can technically still use Option[Boolean], that may not be good for your sanity. Here is an example.

    sealed trait SignalValueIndication
    case class Specified(value: Boolean) extends SignalValueIndication
    case object DontCare extends SignalValueIndication
    
    // Use
    val signalValue = signalValueIndication match {
      case Specified(value) => value
      case DontCare => chooseOptimalSignalValueForImpl
    }
    

    This would seem like this would be a huge wastage, since you're losing the combinators available on Option. That's partly right. Partly because since the two types are isomorphic, writing bridges wouldn't be that hard.

    sealed trait SignalValueIndication {
      // ...
      def toOption: Option[Boolean] = this match {
        case Specified(value) => Some(value)
        case DontCare => None
      }
    
      def getOrElse(fallback: => Boolean) = this.toOption.getOrElse(fallback)
    }
    
    object SignalValueIndication {
      def fromOption(value: Option[Boolean]): SignalValueIndication = {
        value.map(Specified).getOrElse(DontCare)
      }
    }
    

    This is still significant duplication, and you have to judge on a case-to-case basis whether the added clarity makes up for it.

    A similar advice was made by HaskellTips on twitter recently, and a similar discussion ensued. You can find it here.


    Sometimes the presence or absence of a field is a decision you can encode into structure of your data. Using Option would be wrong in such cases.

    Here's an example. Imagine there is a field differentiator for which the following two kind of values are allowed.

    // #1
    { "type": "lov", "value": "somethingFromSomeFixedSet" },
    
    // #2
    { "type": "text", "value": "foo", "translatable": false }
    

    Here, assume the translatable field is mandatory, but only for `"type": "text".

    You could model it with Option thus:

    case class Differentiator(
      _type: DifferentiatorType, 
      value: String, 
      translatable: Option[Boolean]
    )
    

    However a better way to model that would be:

    sealed trait Differentiator
    case class Lov(value: String) extends Differentiator
    case class Text(value: String, translatable: Boolean) extends Differentiator
    

    Yaron Minsky's Effective ML talk has a section on this. You may find it helpful. (Although he uses ML to illustrate the points, the talk is quite accessible and applicable to Scala programmers as well).