So I have a documentation annotation that looks like this:
case class DocProp(name: String = "", dataType: Class[_] = classOf[Object])
The idea is that by default we'll introspect the field and look at the ID and return type, but in some cases that needs overriding.
I want to put it on the fields of my model case classes, so I redeclare it as a type with #field:
type DocProperty = DocProp #field
And then use it in the following different scenarios:
case class MyModel(
// 1. use reflection to inspect the property
#DocProperty
prop1: Int,
// 2. override the name
#DocProperty(name = "myProp")
prop2: String,
// 3. override the return type
#DocProperty(dataType = classOf[String])
prop3: Option[String],
// 4. override everything
#DocProperty("myOtherProp", classOf[Number])
prop4: Float,
// 5. don't document this one at all
hiddenProp: String
)
I've finally managed to dig through layers of runtimeMirrors and whatnot to get down to the Symbols for the values and their associated annotations, and dig out just the DocProp annotations:
// val mc = classOf[MyModel] // passed in
import scala.reflect.runtime.{universe => ru}
val mirror = ru.runtimeMirror(mc.getClassLoader)
val members = mirror.classSymbol(mc).asType.typeSignature.members
val allProps = (for (m <- members) yield {
val a8ns = m.annotations
val a8n = a8ns.find(a => a.tpe <:< ru.typeOf[DocProp])
a8n match {
case Some(found) => Some((m, found))
case _ => None
}
}).flatten
This gets me an iterable of (Symbol, Annotation) for just the annotated properties (I still need the Symbols for the reflective cases).
But I'm now deep in a thicket of reflect.runtime.universe.Trees, and it's not obvious to me how to get out. How do I get from
reflect.runtime.universe.Tree = classOf[java.lang.Number]
to an actual Class[Number] value? How do I get from
reflect.runtime.universe.Tree = doc.this.DocProp.<init>$default$1
to the empty string, or even to 1?
So far there's no easy way of achieving that. Utilizing ToolBox.eval is the workaround that comes to mind. There's also a similar issue in our JIRA that talks about getting values of Java annotations: https://issues.scala-lang.org/browse/SI-6423.
Related
I have a simple method to retrieve a nested key from a hashmap. I need to pattern match on Map[String,Any] so that I can keep iterating into the nested data until I get to a flat value:
def get(map: Map[String, Any], key: String): Any = {
var fields: mutable.Seq[String] = key.split('.')
var currentKey: String = fields.head
var currentValue: Any = map
while (fields.nonEmpty && currentValue.isInstanceOf[Map[String, Any]]) {
currentKey = fields.head
fields = fields.drop(1)
currentValue match {
case m: Map[String, Any] => currentValue = m.getOrElse(currentKey, None)
case _ =>
}
}
if (fields.nonEmpty) None else currentValue
}
It works when I use it only within scala, but if it gets called from java, I get the error non-variable type argument String in type scala.collection.immutable.Map[String,Any].
I've seen some other solutions that require you to refactor code and wrap the map in a case class, but that would be very disruptive to all the code that relies on this method. Is there any simpler fix?
You cannot pattern match on Map[String,Any] because of type erasure. The compiler will have warned of this. This code is just matching on Map[_,_] so it will succeed with any key type, not just String.
So the method is inherently buggy and it appears that calling from Java is exposing bugs that did not emerge when using Scala.
Since you are not using this from Java yet, I would switch to a typesafe implementation for the Java code and then migrate the legacy code to this version as quickly as possible. While this may be disruptive, it would be fixing a design error that introduced buggy code, so it should be done sooner rather than later. (Whenever you see Any used as a value type it is likely that the design went wrong at some point)
The typesafe version is not that difficult, here is an outline implementation:
class MyMap[T] {
trait MapType
case class Value(value: T) extends MapType
case class NestedMap(map: Map[String, MapType]) extends MapType
def get(map: Map[String, MapType], key: String): Option[T] = {
def loop(fields: List[String], map: Map[String, MapType]): Option[T] =
fields match {
case Nil =>
None
case field :: rest =>
map.get(field).flatMap{
case Value(res) => Some(res)
case NestedMap(m) => loop(rest, m)
}
}
loop(key.split('.').toList, map)
}
}
In reality MyMap should actually hold the Map data rather than passing it in to get, and there would be methods for safely building nested maps.
I have the following case class.
case class CustomAttributeInfo[T,Y](
attribute:MyAttribute[_],
fieldName:String,
valueParser:T => Y){}
The case class takes three values.
The last argument is a function that will parse an input of any type and return the part of the input we wish to keep.
(Imagine, for just one example, I pass in a jsonstring, convert to json object, and extract an Int).
The companion object will supply a range of functions that we can pass to the case class. The one shown here, simply takes the input as a string and returns it as a string (the most simple possible example).
object CustomAttributeInfo {
val simpleString = (s:String) => s
}
I create the case class as follows:
CustomAttributeInfo(MyAttribute(var1, var2), name, CustomAttributeInfo.simpleString)
Later, I call the function 'valueParser'
customAttributeInfo.valueParser(k)
Compilation error
Error:(366, 69) type mismatch;
found : k.type (with underlying type String)
required: _$13
case Some(info) => Some((info.attribute, info.valueParser(k)))
I am not a generics expert (obviously). I have done some reading, but I have not seen a discussion about a case like this. Any advice and explanation would be most welcome
You haven't provide enough information to answer your question.
The following code compiles.
If you still have compile error provide MCVE.
case class MyAttribute[_](var1: Any, var2: Any)
case class CustomAttributeInfo[T,Y](attribute:MyAttribute[_], fieldName:String, valueParser:T => Y) {}
object CustomAttributeInfo {
val simpleString = (s:String) => s
}
val var1: Any = ???
val var2: Any = ???
val name: String = ???
val customAttributeInfo = CustomAttributeInfo(MyAttribute(var1, var2), name, CustomAttributeInfo.simpleString)
val k = "abc"
customAttributeInfo.valueParser(k)
#Dmytro was right that a simple example compiled. In my actual codebase code, however, we needed to be specific about the type.
This worked:
object CustomAttributeInfo {
type T = Any
val simpleString = (s:T) => s.toString
}
I want to be able to create a class/trait that behaves somewhat like an enumeration (HEnum in the first snippet below). I can't use a plain enumeration because each enum value could have a different type (though the container class will be the same): Key[A]. I'd like to be able to construct the enum roughly like this:
class Key[A](val name: String)
object A extends HEnum {
val a = new Key[String]("a")
val b = new Key[Int]("b")
val c = new Key[Float]("c")
}
And then I'd like to be able to perform more or less basic HList operations like:
A.find[String] // returns the first element with type Key[String]
A.find("b") // returns the first element with name "b", type should (hopefully) be Key[Int]
So far I've been playing with an HList as the underlying data structure, but constructing one with the proper type has proven difficult. My most successful attempt looks like this:
class Key[A](val name: String)
object Key {
def apply[A, L <: HList](name: String, l: L): (Key[A], Key[A] :: L) = {
val key = new Key[A](name)
(key, key :: l)
}
}
object A {
val (a, akeys) = Key[String, HNil]("a", HNil)
val (b, bkeys) = Key[Int, Key[String] :: HList]("b", akeys)
val (c, ckeys) = Key[Float, Key[Int] :: HList]("c", bkeys)
val values = ckeys // use this for lookups, etc
def find[A]: Key[A] = values.select[A]
def find[A](name: String): Key[A] = ...
}
The problem here is that the interface is clunky. Adding a new value anywhere besides the end of the list of values is error prone and no matter what, you have to manually update values any time a new value is introduced. My solution without HList involved a List[Key[_]] and error prone/unsafe casting to the proper type when needed.
EDIT
I should also mention that the enum example found here is not particularly helpful to me (although, if that can be adapted, then great). The added compiler checks for exhaustive pattern matches are nice (and I would ultimately want that) but this enum still only allows a homogeneous collection of enum values.
For scala case class with number of parameters (21!!)
e.g. case class Car(type: String, brand: String, door: Int ....)
where type = jeep, brand = toyota, door = 4 ....etc
And there is a copy method which allow override with named parameter: Car.copy(brand = Kia)
where would become type = jeep, brand = Kia, door = 2...etc
My question is, is there anyway I can provide the named parameter dynamically?
def copyCar(key: String, name: String) = {
Car.copy("key" = "name") // this is something I make up and want to see if would work
}
Is scala reflection library could provide a help here?
The reason I am using copy method is that I don't want to repeat the 21 parameters assignment every time when I create a case class which only have 1 or 2 parameter changed.
Many Thanks!
FWIW, I've just implemented a Java reflection version: CaseClassCopy.scala. I tried a TypeTag version but it wasn't that useful; TypeTag was too restrictive for this purpose.
def copy(o: AnyRef, vals: (String, Any)*) = {
val copier = new Copier(o.getClass)
copier(o, vals: _*)
}
/**
* Utility class for providing copying of a designated case class with minimal overhead.
*/
class Copier(cls: Class[_]) {
private val ctor = cls.getConstructors.apply(0)
private val getters = cls.getDeclaredFields
.filter {
f =>
val m = f.getModifiers
Modifier.isPrivate(m) && Modifier.isFinal(m) && !Modifier.isStatic(m)
}
.take(ctor.getParameterTypes.size)
.map(f => cls.getMethod(f.getName))
/**
* A reflective, non-generic version of case class copying.
*/
def apply[T](o: T, vals: (String, Any)*): T = {
val byIx = vals.map {
case (name, value) =>
val ix = getters.indexWhere(_.getName == name)
if (ix < 0) throw new IllegalArgumentException("Unknown field: " + name)
(ix, value.asInstanceOf[Object])
}.toMap
val args = (0 until getters.size).map {
i =>
byIx.get(i)
.getOrElse(getters(i).invoke(o))
}
ctor.newInstance(args: _*).asInstanceOf[T]
}
}
It is not possible using case classes.
Copy method generated at compile time and named parameters handled on compile time to. There is no possibility to do it ar runtime.
Dynamic may help to solve your issue: http://hacking-scala.tumblr.com/post/49051516694/introduction-to-type-dynamic
Yes, you would need to use reflection to do that.
It is a bit involved, because copy is a synthetic method and you'll have to invoke the getters for all fields except the one you want to replace.
To give you an idea, the copy method in this class does exactly that, except using an argument index instead of name. It calls the companion object's apply method, but the effect is the same.
I'm a bit confused - how is the following not what you need?
car: Car = ... // Retrieve an instance of Car somehow.
car.copy(type = "jeep") // Copied instance, only the type has been changed.
car.copy(door = 4) // Copied instance, only the number of doors has changed.
// ...
Is it because you have a lot of parameters for the initial instance creation? In that case, can you not use default values?
case class Car(type: String = "Jeep", door: Int = 4, ...)
You seem to know about both these features and feel that they don't fit your need - could you explain why?
I have an enumeration in scala mapped to strings in JPA. For more comfortable coding, I defined implicit conversions between them. So I now can define value val person.role = "User", - person.role is the enumeration type "User" a String so there's the conversion. But when I try to compare these two, I always get false, because the def equals (arg0: Any) : Boolean takes Any so there's not any conversion triggered. I need some explicit conversion, but my plan was to be able to omit that, what do you think is the best practice | neatest solution here?
Expanding on Thomas's answer, if you're using the comparison to branch, using pattern matching may be more appropriate:
object Role extends Enumeration {
val User = MyValue("User")
val Admin = MyValue("Admin")
def MyValue(name: String): Value with Matching =
new Val(nextId, name) with Matching
// enables matching against all Role.Values
def unapply(s: String): Option[Value] =
values.find(s == _.toString)
trait Matching {
// enables matching against a particular Role.Value
def unapply(s: String): Boolean =
(s == toString)
}
}
You can then use this as follows:
def allowAccess(role: String): Boolean = role match {
case Role.Admin() => true
case Role.User() => false
case _ => throw ...
}
or
// str is a String
str match {
case Role(role) => // role is a Role.Value
case Realm(realm) => // realm is a Realm.Value
...
}
The Value("User") in your Enumeration is of type Val. And I believe it's implementation of equals does not compare the string name of the value. I think one heavy handed way of doing this is creating your own Enumeration and Val so that it returns true if the name match.
But in my code uses, not with JPA, I always convert the string into the MyEnumeration.Value. This is easy with things like:
object E extends Enumeration { val User = Value("User") }
scala> val a = E.withName("User")
a: E.Value = User
Note that when using withName, if the string does not match any name in the enumeration you get an exception.
Then always use the enumeration fields in your comparisons:
scala> a == E.User
res9: Boolean = true
If JPA only returns a string, and there is no way around it. Then I think the best option is to either convert the value to string and match string to string, or upgrade the string to a Val and compare Val. Mixing these types will not work for comparison, unless you you implement some kind of extension to the equals method, and that is tricky.