Exhaustive pattern matching when deprecated sealed trait instance present - scala

Suppose the following scenario
sealed trait Status
case object Active extends Status
case object Inactive extends Status
#scala.deprecated("deprecated because reasons")
case object Disabled extends Status /
Considering Disabled object cannot be removed and given val status: Status = getStatus there is either one of this problems:
Fails with match not exhaustive:
status match {
case Active => ???
case Inactive => ???
}
Fails with deprecated value being used
status match {
case Active => ???
case Inactive => ???
case Disabled => ???
}
Losing compile time safety
status match {
case Active => ???
case Inactive => ???
case _ => ???
}
Can type-safety exhaustive match be achieved in this scenario?

I think option 2 is the way to go. But to make it work you have to disable the warning selectively. This is supported starting with Scala 2.13.2 and 2.12.13
#annotation.nowarn("cat=deprecation")
def someMethod(s: Status) = s match {
case Active => "Active"
case Inactive => "Inactive"
case Disabled => "Disabled"
}

There may be some confusion here between "deprecated" and "unavailable".
The Disabled value is deprecated but it is still available for use. And since it is still available, it must still be supported in the code because it can still be used. So option 2 is the only acceptable solution because it is the only one that can processes Disabled appropriately with type safety. Option 1 does not process Disabled and option 3 loses type safety.
Also note that none of these code samples "fail", they just generate warnings. So the solution is to suppress the deprecation warnings as described in the other answer.

Related

Scala Type System Help: Let a failure cascade up the stack

I've been wrestling with this beast for a few days and need some guidance. My original code is too large and cumbersome so I've tried to create all the pieces here and it (almost) compiles, i'm getting some errors with my made up code below.
What i'm trying to do is let a "Failure" cascade up the chain to my API layer. The domain is around connecting to a backend to get a list of equipment health checks by MAC address. An account has multiple equipments, and an equipment has multiple mac addresses. Only the primary macs will get a successful response from the backend system.
Update 1: By Failure here I mean connection issues to the backend client. an Unknown mac (i.e. a mac that's not found/resolved) is not considered a failure. It should be reported as a Success.
Here's what I've recreated thus far for your pleasure to emulate my system. Line 18 and 19 can be toggled to see different conditions.
import scala.util._
trait EquipmentStatus { val mac: String }
case class Offline(mac: String) extends EquipmentStatus
case class Online(mac: String) extends EquipmentStatus
case class Unknown(mac: String) extends EquipmentStatus
case class EquipmentHealth(mac: String, status: EquipmentStatus)
case class Account(number: Int, equipments: List[Equipment])
case class Equipment(macs: List[String]) {
def primaryMacs = macs.filter(_.endsWith("00"))
}
object StatusChecker {
def checkBatchStatuses(macs: List[String]):
Try[List[EquipmentStatus]] =
//Success(macs.map(Online(_)))
Failure(new Exception("Connection Timed Out"))
}
object DeviceService {
def getMacsByAccount(macs: List[String], equipments: List[Equipment]): Try[List[EquipmentHealth]] = {
for {
mac <- macs
equipment <- equipments.filter(_.macs.contains(mac))
statuses <- StatusChecker.checkBatchStatuses(equipment.primaryMacs)
} yield resolveStatus(statuses, mac)// ######### HOW DO I CONVERT/COLLECT Try[EquipmentHealth] to Try[List[EquipmentHealth]] AND ALSO ALLOW Try[Exception()] TO PROPAGATE UP AS WELL?
}
def resolveStatus(statuses: List[EquipmentStatus], mac: String): Try[EquipmentHealth] = {
statuses.partition(_.mac == mac) match {
case (Nil, Nil) => Success(EquipmentHealth(mac, Unknown(mac)))
case (List(one), Nil) => Success(EquipmentHealth(mac, one))
case _ => Success(EquipmentHealth(mac, Unknown(mac)))
}
}
}
val equipments = List(Equipment(List("mac100", "mac222")), Equipment(List("mac333", "mac400")))
val exampleAcc = Account(1234, equipments)
DeviceService.getMacsByAccount(List("mac222"), exampleAcc.equipments)
In my code base, Try is actually a custom Boxed (Either) type that contains Succeeds and Fails. My for comprehension skills are lacking. What I'd like is to go from Try[EquipmentHealth] to Try[List[EquipmentHealth]].
Am I making this too complicated? is there an easier way that I'm not seeing?
Don't you just want to have List[Try[EquipmentStatus]] rather than Try[List[EquipmentStatus]] ? The former doesn't allow recovering from individual failures. A comprehension always yields an iterable-like result, you can't return a Try[List[Something]] from it.
In order to give a more detailed answer, I'd need you to clarify the expected behaviour. It's quite strange that your DeviceService.resolveStatus can only result in a success, and that's mainly because you do not keep any information about which queries failed, so you're unable to decide between "this lookup has failed" and "this MAC address is unknown". I think the unknown case should be removed and we always assume that if no success was returned, then it was a failure. Otherwise, you need to store a bit more information, like for example List[(String, Try[EquipmentStatus]] where the first element of the tuple is the MAC address that was queried (or, for better performance, using a map which keys are the addresses).
The current signature of getMacsByAccount is
def getMacsByAccount(macs: List[String], ...): Try[List[EquipmentHealth]]
This allows a single Success/Failure result and you cannot check each mac independently. If you want to track the error status of each mac then you need to return List[Try[EquipmentHealth]] rather than Try[List[EquipmentHealth]]:
def getMacsByAccount(macs: List[String], ...): List[Try[EquipmentHealth]]
Implementing this requires a simple change to getMacsByAccount:
def getMacsByAccount(macs: List[String], equipments: List[Equipment]): List[Try[EquipmentHealth]] =
for {
mac <- macs
equipment <- equipments.filter(_.macs.contains(mac))
statuses = StatusChecker.checkBatchStatuses(equipment.primaryMacs)
} yield
statuses.flatMap(resolveStatus(_, mac))
Note: At the moment resolveStatus always returns Success in which case it might as well just return EquipmentHealth. If you change this, change the flatMap above to map.
Edit after comments
If you just need a single failure or success, then wrap the whole thing in a Try and unpack the inner Try values using get:
def getMacsByAccount(macs: List[String], equipments: List[Equipment]): Try[List[EquipmentHealth]] =
Try {
for {
mac <- macs
equipment <- equipments.filter(_.macs.contains(mac))
statuses = StatusChecker.checkBatchStatuses(equipment.primaryMacs).get
} yield resolveStatus(statuses, mac).get
}
If any of the Try values is Failure then get will throw an exception which is caught by the outer Try.
Of course it would be simpler if checkBatchStatuses and resolveStatus just threw an exception on failure rather than returning Try.

Matching against Value Classes in Akka

I've created the Value Class
final class Feature(val value: Vector[Double]) extends AnyVal
To match against that class in scala:
val d = new Feature(Vector(1.1))
s match {
case a:Feature => println(s"a:feature, $a")
case _ => println("_")
}
This works correctly, but in Akka, with the same class above, in the receive method this is not working:
def receive = LoggingReceive {
case a:Feature =>
log.info("Got Feature: {}", a)
case _ => println("_")
}
When I execute the code, although I am sending a Feature, the case statement that is being executed is case _ => println("_"), but, If I change the code to this:
def receive = LoggingReceive {
case a:Feature =>
log.info("Got Feature: {}", a)
case b:Vector[_] =>
log.info("GOT FEATURE")
case _ => println("_")
}
case b:Vector[_] is executed.
Akka documentation mentions:
The recommended way to instantiate actor props uses reflection at runtime to determine the correct actor construc-
tor to be invoked and due to technical limitations is not supported when said constructor takes arguments that are
value classes. In these cases you should either unpack the arguments or create the props by calling the constructor
manually:
But do not mention nothing about matching against Value classes
Update
Thanks to YuvalItzchakov for the help. The Actor's code is as below:
Actor that receive the message:
def receive = LoggingReceive {
case Feature(a) =>
log.info("Got feature {}", a)
// ....
}
Actor sending the message:
def receive = LoggingReceive {
// ..
case json: JValue =>
log.info("Getting json response, computing features...")
val features = Feature(FeatureExtractor.getFeatures(json))
log.debug(s"Features: $features")
featureListener.get ! features
// ..
}
Due to the nature of how value classes work, both your examples will cause the allocation of Feature. Once due to run-time checking in your pattern matching example, and the other due to the signature of receive which requires Any as the input type.
As the docs for Value Classes specify (emphasis mine):
Allocation Summary
A value class is actually instantiated when:
a value class is treated as another type.
a value class is assigned to an array.
doing runtime type tests, such as pattern matching.
This means that if you're seeing a Vector[_] type, that means your actually passing a concrete vector from some place in the code.

Selector of pattern match being exhaustive

Looking at the Scala doc for sealed classes, it says:
If the selector of a pattern match is an instance of a sealed class, the compilation of pattern matching can emit warnings which diagnose that a given set of patterns is not exhaustive, i.e. that there is a possibility of a MatchError being raised at run-time.
I don't quite understand what they meant in this paragraph. My understanding is that if a switch case, doesn't cover all the possibilities, then we'll get a warning at compile time, saying we might get an error at run time. Is this correct?
I find it strange, because how can we cover ALL the scenarios in a switch case? We would have to match all possible strings, which is just silly, so I take it my understanding is incorrect. Someone care to elucidate, please?
What the paragraph is saying is that in-case you have a fixed hierarchy structure like this:
sealed trait Foo
class Bar extends Foo
class Baz extends Foo
class Zab extends Foo
Then when you pattern match on it, the compiler can infer if you've attempted to match on all possible types extending the sealed trait, in this example:
def f(foo: Foo) = foo match {
| case _: Bar => println("bar")
| case _: Baz => println("baz")
| }
<console>:13: warning: match may not be exhaustive.
It would fail on the following input: Zab()
def f(foo: Foo) = foo match {
^
f: (foo: Foo)Unit
Note the beginning of the document says:
A sealed class may not be directly inherited, except if the inheriting
template is defined in the same source file as the inherited class.
This is unique to Scala, and can't be done in Java. A final class in Java cannot be inherited even if declared inside the same file. This is why this logic won't work for String in Scala, which is an alias for java.lang.String. The compiler warning may only be emitted for Scala types that match the above criteria.
I find it strange, because how can we cover ALL the scenarios in a switch case? We would have to match all possible strings
Yes, if the selector has type String (except it isn't a sealed class, because that's a Scala concept and String is a Java class).
which is just silly
No. For strings, you just need a catch-all case, e.g.
val x: String = ...
x match {
case "a" => ...
case "b" => ...
case _ => ...
}
is an exhaustive match: whatever x is, it matches one of the cases. More usefully, you can have:
val x: Option[A] = ...
x match {
case Some(y) => ...
case None => ...
}
and the compiler will be aware the match is exhaustive even without a catch-all case.
The wildcard character allows us to cover all the scenarios.
something match {
case one => ...
case two => ...
case _ => ...
}
It is selected whenever all other cases don't match; that is, it is the default case. Further information here.

Scala : cannot check match for unreachibility even with default value

I'm working on a compiler for a simple language for a course and I ran into this warning. The pattern matching in question is simple so I don't get why this pops up. Here's the pattern matching in question :
(tcExpr(lhs), tcExpr(rhs)) match {
case (TInt, TInt) | (TString, TString) | (TBoolean, TBoolean) | (TIntArray, TIntArray) | (TObject(_), TObject(_)) => TBoolean
case _ => TError
with tcExpr signature being def tcExpr(expr: ExprTree, expected: Type*): Type, ExprTree representing an expression and the Type being case objects. All except TObject() are simple objects, and TObject() takes a symbol representing a class as argument.
What I don't get is that there are not that many possibilities. Why can't the compiler figure it out ? Is there something I have overlooked, or misunderstood ?
Thank you very much for your answers,
Nicolas
I could reproduce "warning: Cannot check match for unreachability." with the following code (I don't know how many types you have. I took 32 + 1 as upper bound):
trait Type
case object T0 extends Type
case object T1 extends Type
case object T2 extends Type
case object T3 extends Type
case object T4 extends Type
...
<generated code, 32 types in total>
...
case object T31 extends Type
def foo: Type = T0
(foo, foo) match {
case (T0,T0) | <total 32 types> | (T31,T31) => println("true")
case _ => println("false")
}
The reason seems to be that it simply runs out of memory trying to check all the possible cases. The advice given by the warning is to add the option -Dscalac.patmat.analysisBudget=off when running the compiler. Just tried to compile with this option: it works, no more warnings.
EDIT: Just found out by bisection that without the option, it stops working with >= 5 types... This is not much memory indeed.
My proposal would be to just remove the eternally long enumeration, and replace it by a short and concise equation:
(foo, foo) match {
case (Obj(_), Obj(_)) => println("true")
case (x, y) if (x == y) => println("true") // Order is important, thx #Nico
case _ => println("false")
}
Then the warning disappears, and the code stops spilling over the 80 characters / line limit.

Does it make sense to return Try[Option[String]]?

I'm writing a method which has three possible return states:
The operation can have failed with an exception
The operation can have succeeded with a String result
The operation can have succeeded with no results
Does the return type Try[Option[String]] seem correct? Or is there a better alternative?
Lift's Box does a nice job of combining the semantics of Option and Try
A Box can be:
Full(value) => Some(value): Option
Empty => None: Option
Failure(ex) => Failure: Try
http://scala-tools.org/mvnsites/liftweb-2.3/net/liftweb/common/Box.html
Your approach is perfectly fine. Another approach is to define your own case classes/objects
sealed trait Result
case class OperationFailed(e: Exception) extends Result
case class SuccessValue(str: String) extends Result
case object SuccessNoValue extends Result
Edit:
This approach may work better with pattern matching, specially if you are using Akka.
val f: Any => Any = { case x: Try[Option[String]] => x.get.get.toUpperCase }
f(Try(Some("x")))
f(Try(Some(5))) // Throws java.lang.ClassCastException
The above would show the following warning
warning: non-variable type argument Option[String] in type pattern
scala.util.Try[Option[String]] is unchecked since it is eliminated by
erasure