You really haven't shorted out anything. You still run the MO; you just don’t capture the values.
In addition, the standard IO monad does not define a filter (or withFilter ), so you cannot use protective devices in your understanding.
Now, if you want only what you said (same logic, just more DRY), you can always assign a temporary variable to understand:
for { a <- io b <- shortCircuit(io, a == 1) continue = b.map(_ == 1).getOrElse(false) c <- shortCircuit(io, continue) d <- shortCircuit(io, continue) e <- shortCircuit(io, continue) } yield …
But if you really want a short circuit, you have to somehow break things up. There is one possibility here, assuming you just want to pack everything into an array, so the return type is simple, and your companion IO object has an apply method that you can use to create something that just returns a value:
io.flatMap(a => if (a == 1) IO(() => Array(a)) else io.flatMap(b => if (b == 1) IO(() => Array(a,b)) else for { c <- io d <- io e <- io } yield Array(a,b,c,d,e) ) )
If your return types are more complex, you may need to work more with specifying types.
FWIW, it’s worth noting the punishment you pay for storing things wrapped in monads; without, the same logic would be (for example):
io() match { case 1 => Array(1) case a => io() match { case 1 => Array(a, 1) case b => Array(a, b, io(), io(), io()) } }
And if you allow a refund, you get:
val a = io() if (a == 1) return Array(a) val b = io() if (b == 1) return Array(a, b) Array(a, b, io(), io(), io())
It is also possible, in principle, to decorate the IO monad with additional methods that help a little, but the standard withFilter will not work, so you won’t be able to use syntactic sugar for understanding.