How to Pattern Match on Scala 3 enums - scala

I want to pattern match to the new enum type in Scala 3.
With Scala 2 this is easy, because there is an abstract class Enumeration, that you can match on, like:
obj match {
case e: Enumeration => ...
case other => ...
}
How can I achieve this in Scala 3 as there is no common interface?
From the answers of this related question I tried:
case v: { def values: Array[?] } => ...
This compiles with a warning but when I run publishLocal it throws an exception:
[error] (api / Compile / doc) java.lang.reflect.InvocationTargetException

Ok this is what I came up:
case v: scala.reflect.Enum =>
val e:{ def values: Array[?] } = v.asInstanceOf[{ def values: Array[?] }]
e.values.map(_.toString).toList
This works and it is possible to publish. Still no idea why it is not possible with case v: { def values: Array[?] } => ...
So better solutions and explanations still welcome.

Related

Determining constructor parameter count and type via reflection in Scala

I have about a hundred small classes that inherit a trait. The classes are instantiated in a factory via reflection based on their names.
Class.forName(name).getConstructor().newInstance().asInstanceOf[Trait]
Now requirements have changed such that one and only one of the classes needs to take a parameter during construction. I am trying to change the factory to handle this new case. The REPL worksheet with my approach looks like this:
import java.lang.reflect.Constructor
trait T {
def p: Unit
}
class C1 extends T {
override def p = println("no ymd")
}
class C2(a: Array[String]) extends T {
override def p = println(s"year: ${a(0)}")
}
class C3(a: Array[Int]) extends T {
override def p = println(s"day: ${a(2)}")
}
val ymd = Array("2019","10","23")
val ymd_int = Array(2019,10,23)
def getT(c: Class[_]): T = {
c.getConstructors match {
case Array(c: Constructor[Array[String]]) => c.newInstance(ymd).asInstanceOf[T]
case Array(c: Constructor[Array[Int]]) => c.newInstance(ymd_int).asInstanceOf[T]
case Array(c: Constructor[_]) => c.newInstance().asInstanceOf[T]
case _ => throw new Exception("...")
}
}
getT(classOf[C1]).p
getT(classOf[C2]).p
getT(classOf[C3]).p
In the REPL, I'm using classOf instead of Class.forName because class names in the REPL are kind of wonky.
During compilation I'm getting the warnings:
Warning:(25, 23) non-variable type argument Array[String] in type
pattern java.lang.reflect.Constructor[Array[String]] is unchecked
since it is eliminated by erasure
case Array(c: Constructor[Array[String]]) =>
c.newInstance(ymd).asInstanceOf[T]
and
Warning:(26, 23) non-variable type argument Array[Int] in type
pattern java.lang.reflect.Constructor[Array[Int]] is unchecked
since it is eliminated by erasure
case Array(c: Constructor[Array[Int]]) =>
c.newInstance(ymd_int).asInstanceOf[T]
And then of course the calls to getT are failing because the three case statements look identical at run time and so all three calls are being handled by the first case.
Please Help.
Try
def getT(clazz: Class[_]): T = {
val constructor = clazz.getConstructors.head
(constructor.getParameterTypes.headOption.map(_.getSimpleName) match {
case None => constructor.newInstance()
case Some("String[]") => constructor.newInstance(ymd)
case Some("int[]") => constructor.newInstance(ymd_int)
case _ => throw new Exception("...")
}).asInstanceOf[T]
}

My function in Scala is returning the supertype and my type class can't handle it

I'm trying to go functional, but when working on real world problems I'm struggling, I need helps with a few basics. I like the idea of a type class and to add more implicit types in future.
trait Query {
def queryDetails: QueryDetails
}
case class LocalQueryType(queryDetails: QueryDetails) extends Query
case class JdbcQueryType(queryDetails: QueryDetails) extends Query
def queryTypeFactory(queryDetails: QueryDetails): Query = {
queryDetails.platform match {
case c if queryDetails.platform.contains("-file://") => LocalQueryType(queryDetails)
case _ => JdbcQueryType(queryDetails)
}
}
Then I have a type class that looks for the local or Jdbc types, but it doesn't work as it is receiving only Query type.
I've tried using generics like:
def queryTypeFactory[T<:Query](queryDetails: QueryDetails): T = {
queryDetails.platform match {
case c if queryDetails.platform.contains("-file://") => LocalQueryType(queryDetails)
case _ => JdbcQueryType(queryDetails)
}
}
Adding Type Class:
trait QueryTask[A] {
def runQuery(a: A): String
}
object QueryTask {
def apply[A](implicit sh: QueryTask[A]): QueryTask[A] = sh
object ops {
def runQuery[A: QueryTask](a: A) = QueryTask[A].runQuery(a)
implicit class ShowOps[A: QueryTask](a: A) {
def runQuery = QueryTask[A].runQuery(a)
}
}
implicit val localQuery: QueryTask[LocalQueryType] =
instance(localQueryType => s"running local: ${localQueryType.queryDetails.command} on platform: ${localQueryType.queryDetails.platform}")
implicit val jdbcQuery: QueryTask[JdbcQueryType] =
instance(jdbcQueryType => s"running jdbc: ${jdbcQueryType.queryDetails.command} on platform: ${jdbcQueryType.queryDetails.platform}")
def instance[A](func: A => String): QueryTask[A] =
new QueryTask[A] {
def runQuery(a: A): String = func(a)
}
The idea is to not use the usual OO factory or strategy pattern.
Type class approach seems not to work in your use case.
Implicits are resolved at compile time. So in order to decide which instance you need QueryTask[LocalQueryType] or QueryTask[JdbcQueryType] compiler has to know whether type A is LocalQueryType or JdbcQueryType at compile time.
But it seems you decide that depending on whether queryDetails.platform.contains("-file://") or not i.e. at runtime.
It seems you need usual pattern mathing. You should use type class pattern when it's necessary.

Weird failure of Scala's type-check

My application dictates need of an argument provider trait that can be added to any class to allow passing of arbitrary number of arguments of any type with it.
trait Arg
case class NamedArg(key: String, value: Any) extends Arg
// I extend my classes with this trait
trait ArgsProvider {
val args: Seq[Arg]
lazy val namedArgs: Map[String, Any] = {
args.filter(_.isInstanceOf[NamedArg]).
map(_.asInstanceOf[NamedArg]).
map(arg => arg.key -> arg.value).toMap
}
...
}
Then I can extract the NamedArgs from args of ArgsProvider using their key as follows
trait ArgsProvider {
...
/*
* Method that takes in a [T: ClassTag] and a (key: String) argument
* (i) if key exists in namedArgs: Map[String, Any]
* - Returns Some(value: T) if value can be casted into T type
* - Throws Exception if value can't be casted into T type
* (ii) if key doesn't exist in namedArgs
* Returns None
*/
def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
namedArgs.get(key).map { arg: Any =>
try {
arg.asInstanceOf[T]
} catch {
case _: Throwable => throw new Exception(key)
}
}
}
...
}
Even though it may seem highly unintuitive and verbose, this design works flawlessly for me. However, while writing certain unit tests, I recently discovered a major loophole in it: it fails to perform type-checking. (or at least that's what I infer)
To be more specific, it doesn't throw any exception when I try to type-cast the provided arg into wrong type. For example:
// here (args: Seq[NamedArg]) overrides the (args: Seq[Arg]) member of ArgsProvider
case class DummyArgsProvider(args: Seq[NamedArg]) extends ArgsProvider
// instantiate a new DummyArgsProvider with a single NamedArg having a String payload (value)
val dummyArgsProvider: DummyArgsProvider = DummyArgsProvider(Seq(
NamedArg("key-string-arg", "value-string-arg")
))
// try to read the String-valued argument as Long
val optLong: Option[Long] = dummyArgsProvider.getOptionalTypedArg[Long]("key-string-arg")
While one would expect the above piece of code to throw an Exception; to my dismay, it works perfectly fine and returns the following output (on Scala REPL)
optLong: Option[Long] = Some(value-string-arg)
My questions are:
Why is type-check failing here?
Under what circumstances does Scala's type-checking fail, in general?
Can this design be improved?
I'm using
Scala 2.11.11
SBT 1.0.3
As #AlexeyRomanov has remarked, as/isInstanceOf[T] don't use a ClassTag.
You can use pattern matching instead, which does check with a ClassTag, if there's one available:
trait ArgsProvider {
/* ... */
def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
namedArgs.get(key).map {
case arg: T => arg
case _ => throw new Exception(key)
}
}
}
Or you can use methods of ClassTag directly:
import scala.reflect.classTag
def getOptionalTypedArg[T: ClassTag](key: String): Option[T] = {
namedArgs.get(key).map { arg =>
classTag[T].unapply(arg).getOrElse(throw new Exception(key))
}
}
You're having problem with type-erasure: The Option[Long] actually store the String "value-string-arg" and doesn't care about its type which as been erased.
However, if you do optLong.get it will then try to cast it to a Long which is the expected output. And you'll get the ClassCastException
Just a little comments:
replace
val namedArgs: Map[String, Any] = {...}
by
val namedArgs: Map[String, Any] = args.collect{
case NameArg(k, v) => k -> v
}(collection.breakout)
Also, in your getOptionalTypedArg, don't catch all Throwable. It is bad practice (you could catch OutOfMemoryError and other Fatal errors, which you should not). In your case, you want to catch a ClassCastException. In other case where you don't know exactly which Throwable, try using NonFatal

Type Erasure in Scala

I'm rather confused about what's happening here:
import scala.collection.immutable._
object Main extends App {
sealed trait Node
sealed trait Group
case class Sheet(
val splat: String,
val charname: String,
val children: ListMap[String, Node],
val params0: ListMap[String, Param], //params0 to separate sheet-general parameters
val note: Option[Note]
) extends Node with Group
case class Attributes(val name: String) extends Node with Group
case class Param(val name: String, val value: String) extends Node
case class Note(val note: String) extends Node
I've got three versions of a replace function - the last one is the one I'm actually trying to write, the others are just debugging.
class SheetUpdater(s: Sheet) {
def replace1[T <: Group](g: T): Unit = {
s.children.head match {
case (_, _:Sheet) =>
case (_, _:Attributes) =>
}
}
}
This version throws no warnings, so apparently I have access to the type of s.children at runtime.
class SheetUpdater(s: Sheet) {
def replace2[T <: Group](g: T): Unit = {
g match {
case _:Sheet =>
case _:Attributes =>
}
}
}
Neither does this version, so apparently the details of g's type are also available at runtime...
class SheetUpdater(s: Sheet) {
def replace3[T <: Group](g: T): Unit = {
s.children.head match {
case (_, _:T) => //!
case (_, _:Attributes) =>
}
}
}
... but even so, this ends up throwing me the dreaded Abstract type pattern T is unchecked since it is eliminated by erasure warning. What's going on here?
In Scala, generics are erased at runtime, which means that the runtime type of List[Int] and List[Boolean] is actually the same. This is because the JVM as a whole erases generic types. All this is due because the JVM wanted to remain backwards compatible way back when generics were first introduced...
There is a way around this in Scala using a ClassTag, which is an implicit parameter that then can be threaded around with whatever generic you are using.
You can think of : ClassTag as passing the type of the generic as an argument. (It is syntactic sugar for passing an implicit parameter of type ClassTag[T].)
import scala.reflect.ClassTag
class SheetUpdater(s: Sheet) {
def replace3[T <: Group : ClassTag](g: T): Unit = {
s.children.head match {
case (_, _:T) => //!
case (_, _:Attributes) =>
}
}
}
Newer answers of this question have more details.

Why doesn't my validation throw exception when it checks for the input type?

My method:
protected final def validatePayload[T](payload: Option[Payload]) = {
payload match {
case None => throw new IllegalArgumentException("Payload was None.")
case Some(p) => p.resource match {
case None => throw new IllegalArgumentException("Resource was None.")
case Some(resource) => resource match {
case temp: T =>
case _ => throw new IllegalArgumentException("Resource is not the right type.")
}
}
}
}
Payload:
case class Payload(id: String, resource: Option[Any])
Usage:
validatePayload[String](Some(Payload("id", Some(5))))
I'd expect this to throw the illegal arg since I'm telling it to accept String and I'm passing in a Int. Why is it not?
My objective is to validate the payload been sent to an actor, the actor should only react to a specific type of resource and nothing else.
How can I fix this to accomplish that?
ClassTag
The simplest case is when you can use a ClassTag (the limitation for this case is given below). For that case you can simply add a context bound to the function type definition, and it just works:
import scala.reflect.ClassTag
protected final def validatePayload[T : ClassTag](payload: Option[Payload]) = {
// everything else is the same...
}
// Throws an error
validatePayload[String](Some(Payload("id", Some(5))))
At runtime it's pretty much equivalent to Java's instanceof operator and a type cast.
TypeTag
But the ClassTag doesn't work for generic types. For example, sequences with different element types aren't distinguished:
// Doesn't throw
validatePayload[Seq[String]](Some(Payload("id", Some(Seq(1,2,3)))))
If you need to distinguish generic types, you'd have to use TypeTags. You must know the type of resource, when you are creating the payload, and the payload must store its Type or the TypeTag of its type.
Here is an example:
import reflect.runtime.universe._
case class Payload[T](id: String, resource: Option[T])(implicit val tag: TypeTag[T])
def validatePayload[T : TypeTag](payload: Option[Payload[_]]) = {
payload match {
case None => throw new IllegalArgumentException("Payload was None.")
case Some(p) => p.resource match {
case None => throw new IllegalArgumentException("Resource was None.")
case Some(resource) => resource match {
case temp if p.tag.tpe <:< typeOf[T] =>
case _ => throw new IllegalArgumentException("Resource is not the right type.")
}
}
}
}
Now it will distinguish generics:
// Throws an error
validatePayload[Seq[String]](Some(Payload("id", Some(Seq(1,2,3)))))
But TypeTags rely on types known at compile time. So if resource has type Any before you create Payload with it, the validatePayload[T] will not throw only if T is Any. And there are some other quirks:
// Doesn't throw
validatePayload[Seq[Int]](Some(Payload("id", Some(List(1,2,3)))))
// Throws, although resource *is* a List[Int] at runtime
validatePayload[List[Int]](Some(Payload("id", Some(Seq(1,2,3)))))
cast from shapeless
A more robust method is provided by a third-party library shapeless. Here is an example:
import shapeless.Typeable
import shapeless.syntax.typeable._
def validatePayload[T : Typeable](payload: Option[Payload]) = {
payload match {
case None => throw new IllegalArgumentException("Payload was None.")
case Some(p) => p.resource match {
case None => throw new IllegalArgumentException("Resource was None.")
case Some(resource) => resource match {
case temp if temp.cast[T].isDefined =>
case _ => throw new IllegalArgumentException("Resource is not the right type.")
}
}
}
}
Both don't throw now:
validatePayload[Seq[Int]](Some(Payload("id", Some(List(1,2,3)))))
validatePayload[List[Int]](Some(Payload("id", Some(Seq(1,2,3)))))
Due to type erasure you can't check type like this, but ClassTag is a workaround.
case class Payload(id: String, resource: Option[Any])
import scala.reflect.ClassTag
def validatePayload[T: ClassTag](payload: Option[Payload]) = {
payload flatMap (_.resource) filter { res =>
val c = implicitly[ClassTag[T]].runtimeClass
c.isInstance(res)
} getOrElse (throw new IllegalArgumentException("Invalid payload"))
}
I simplified the code, if you don't need custom errors, it's less verbose for me at least. Although if you want to stick to your code, only important parts from your problems view is declaring that the type T needs and implicit ClassTag[T] which is declared like this [T: ClassTag] and check if the type is valid here:
val c = implicitly[ClassTag[T]].runtimeClass
c.isInstance(res)
Here is a test
scala> validatePayload[String](Some(Payload("id", Some("a"))))
res3: Any = a
scala> validatePayload[String](Some(Payload("id", Some(5))))
java.lang.IllegalArgumentException: Invalid payload
at $anonfun$validatePayload$3.apply(<console>:20)
at $anonfun$validatePayload$3.apply(<console>:20)
at scala.Option.getOrElse(Option.scala:121)
at .validatePayload(<console>:20)
... 33 elided