How to select union type branch in a for comprehension?

How does one narrow down from a union type to one of the component types in a for comprehension?

I’m trying to convert from using an Option of some (concrete) type T to using a union type that “unions” a type representing being empty with that original type T. (See OldCellState vs. NewCellState in the code below.)

Specifically, I’m trying to convert the logic at the line labeled with “#1” (a for-comprehension generator(?) that limits processing to only non-empty cell and gets the ball color in a variable for further use).

What the right/normal/usual way to convert that for a union type?

My conversion attempt at the line labeled “#2” fails compilation with the error message “value withFilter is not a member of Example.NewCellState”. I partially understand that desugaring a for comprehension can involve a withFilter call, but I don’t even know if I’m on the right starting path here.

object Example {

  enum BallColor { case Black, White }

  type OldCellState = Option[BallColor]

  case object Empty
  type NewCellState = Empty.type | BallColor

  val oldCellStates:  List[OldCellState] = ???
  val newCellStates:  List[NewCellState] = ???

  for {
    oldCellState <- oldCellStates
    ballColor <- oldCellState                   // #1
  } yield ballColor: BallColor

  for {
    newCellState <- newCellStates
    // "value withFilter is not a member of Example.NewCellState":
    case ballColor @ BallColor <- newCellState  // #2
  } yield ballColor: BallColor

  for {
    newCellState <- newCellStates
    ballColor <-                                // #3
        newCellState match
          case Empty            => None
          case color: BallColor => Some(color)
  } yield ballColor: BallColor

}

I can get things to work by using the approach labeled “#3”, it seems that there’s got to be a less verbose way than that.

(Additionally, #3 defeats one purpose for using the union type in the first place–to eliminate allocating a Some instance wrapping a BallColor for each non-empty cell. It really defeats that purpose, since it allocates (or seems to) a Some instance for each execution of that code for a non-empty cell (and, in the project this is extracted from, there’ll be a lot more executions of that code than the number of cells).)

So, any suggestions?

Thanks.

Hi there :wave:

I think that using Empty is simply “renaming” None here so it’s not all that different in the end. Moreover, using Option is arguably more sensible and logical and easier to understand for what you are trying to accomplish (filtering missing values).

Since newCellStates is a list that can contain Empty or a BallColor, the for-comprehension cannot know which is Empty and which isn’t. A Union type alias does not provide such filtering mechanism. You could first filter out the Emptys from the list manually, outside of the for with the pattern matching you used.

for comprehensions are implemented for types that are sometimes called “monads”, not for Union type aliases. Option[T] is a monad, so it has implementations of all the methods that for needs. It’s tailor-made for for. This does not fit the intended use and design of a Union type alias.

Otherwise you need to turn your NewCellState into a proper class or trait, instead of just a type alias, and provide whatever methods it needs. This would provide the necessary “filtering mechanism”. So you move the pattern matching logic into that implementation. Arguably this is overkill.

What is needed for for? You need map, flatMap and withFilter. (Union type aliases do not come with this, they are not designed that way.) The desugaring of for is explained in detail on Week 1 of Functional Program Design.

I’d argue that using Option is still a better choice. (Not sure why some users have an aversion to Option? It’s nice, use it!)

Another design issue is that, the union type makes it “one of three things”: Black, White or Empty. Is that the idea? Or should there be an Empty / NonEmpty distinction as well? Then you can make your life much easier.

Just my two cents. (Just to reiterate, there is no provided way of using for with union type aliases. The 3 methods are needed. See Coursera course.)