So I found some esoteric code when upgrading an old Scala 2 library, the gist of it looks like this:
object MatchExtractTypeArg {
import scala.reflect.runtime.universe.TypeTag
def mapType(k: TypeTag[_], v: TypeTag[_]) = {
(k, v) match {
case (k: TypeTag[a], v: TypeTag[b]) =>
implicit val kkk = k
implicit val vvv = v
implicitly[TypeTag[Map[a, b]]]
}
}
def main(args: Array[String]): Unit = {
val t1 = implicitly[TypeTag[Int]]
val t2 = implicitly[TypeTag[String]]
val r = mapType(t1, t2)
println(r)
}
}
the pattern matching case (k: TypeTag[List[a]], v: TypeTag[List[b]]) is fairly difficult to understand. after some investigation, I could only speculate that the following unapply function was used on k & v to determine type a & b:
// (in scala.reflect.runtime.universe.TypeTag definition)
def unapply[T](ttag: TypeTag[T]): Option[Type] = Some(ttag.tpe)
After Scala 3 upgrade, the above code should becomes:
object MatchExtractTypeArg {
def mapType(k: Typeable[_], v: Typeable[_]) = {
(k, v) match {
case (k: Typeable[a], v: Typeable[b]) =>
given kk: Typeable[a] = k
given vv: Typeable[b] = v
summon[Typeable[Map[a, b]]]
}
}
def main(args: Array[String]): Unit = {
val t1 = summon[Typeable[Int]]
val t2 = summon[Typeable[String]]
val r = mapType(t1, t2)
println(r)
}
}
It apparently compiles without a problem, but when trying to figure out its mechanism, I found that the unapply method being defined under Typeable has an entirely different signature, and can't extract any type argument.
Am I not upgrading code properly? How could it be possible that the new extractor still works in Scala 3?
Suppose we have a generic type (for example, Seq[E]) and a concrete subtype (for example, Seq[Int]). How can we extract concrete type that corresponds to the type parameters of the abstraction type. In other words, how can we know E -> Int.
Below is a minimal code example that tests for the desired behavior. The extractTypeBinding function would perform the transformation in question.
import scala.reflect.runtime.{universe => ru}
class MyFuncs
object MyFuncs {
def fn1[E](s: Seq[E]): E = ???
def fn2[K, V](m: Map[K, V]): Int = ???
}
object Scratch {
def extractTypeBinding(genType: ru.Type, typeParam: ru.Type)(concreteType: ru.Type): ru.Type = ???
def getArgTypes(methodSymbol: ru.MethodSymbol): Seq[ru.Type] =
methodSymbol.paramLists.headOption.getOrElse(Nil).map(_.typeSignature)
def main(a: Array[String]): Unit = {
// Grab the argument types of our methods.
val funcsType = ru.typeOf[MyFuncs].companion
val fn1ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn1")).asMethod)
val fn2ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn2")).asMethod)
val genericSeq = fn1ArgTypes.head // Seq[E]
val genericMap = fn2ArgTypes.head // Map[K, V]
// Create an extractor for the `E` in `Seq[E]`.
val seqElExtractor = extractTypeBinding(genericSeq, genericSeq.typeArgs.head) _
// Extractor for the `K` in `Map[K,V]`
val mapKeyExtractor = extractTypeBinding(genericMap, genericMap.typeArgs.head) _
// Extractor for the `V` in `Map[K,V]`
val mapValueExtractor = extractTypeBinding(genericMap, genericMap.typeArgs(1)) _
println(seqElExtractor(ru.typeOf[Seq[Int]])) // should be Int
println(seqElExtractor(ru.typeOf[Seq[Map[String, Double]]])) // should be Map[String, Double]
println(mapKeyExtractor(ru.typeOf[Map[String, Double]])) // should be String
println(mapKeyExtractor(ru.typeOf[Map[Int, Boolean]])) // should be Int
println(mapValueExtractor(ru.typeOf[Map[String, Double]])) // should be Double
println(mapValueExtractor(ru.typeOf[Map[Int, Boolean]])) // should be Boolean
}
}
Based on the docstrings, it seems like asSeenFrom should be the key to implementing extractTypeBinding. I tried the below implementation, but it returned the type parameter unchanged.
def extractTypeBinding(genType: ru.Type, typeParam: ru.Type)(concreteType: ru.Type): ru.Type =
typeParam.asSeenFrom(concreteType, genType.typeSymbol.asClass)
...
println(seqElExtractor(ru.typeOf[Seq[Int]])) // E
println(seqElExtractor(ru.typeOf[Seq[Map[String, Double]]])) // E
If asSeenFrom is the correct approach, what would the correct incantation be?
If not, then how should this be done?
The simplest solution came from the helpful prodding by Dmytro Mitin in the comments.
I had a couple misunderstandings about .typeArgs that were cleared up with some additional experimentation.
It returns all type arguments, not just the abstract ones.
It only returns the "top level" type arguments of the type you call it on. In other words, Map[A, Map[B, C]] only has 2 type args (A and Map[B, C])
Both of those seem very intuitive now, but I initially made some foolish assumptions. Below is a modified version of my test that more clearly achieves my original intent.
class MyFuncs
object MyFuncs {
def fn1[E](s: Seq[E]): E = ???
def fn2[K, V](m: Map[K, V]): Int = ???
}
object Scratch {
def typeArgBindings(genericType: ru.Type, concreteType: ru.Type): Map[ru.Type, ru.Type] =
// #todo consider validating both have the same base type.
genericType.typeArgs.zip(concreteType.typeArgs).toMap
def getArgTypes(methodSymbol: ru.MethodSymbol): Seq[ru.Type] =
methodSymbol.paramLists.headOption.getOrElse(Nil).map(_.typeSignature)
def main(a: Array[String]): Unit = {
// Grab the argument types of our methods.
val funcsType = ru.typeOf[MyFuncs].companion
val fn1ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn1")).asMethod)
val fn2ArgTypes = getArgTypes(funcsType.member(ru.TermName("fn2")).asMethod)
val genericSeq = fn1ArgTypes.head // Seq[E]
val genericMap = fn2ArgTypes.head // Map[K, V]
println(typeArgBindings(genericSeq, ru.typeOf[Seq[Int]])) // Map(E -> Int)
println(typeArgBindings(genericSeq, ru.typeOf[Seq[Map[String, Double]]])) // Map(E -> Map[String,Double])
println(typeArgBindings(genericMap, ru.typeOf[Map[String, Double]])) // Map(K -> String, V -> Double)
println(typeArgBindings(genericMap, ru.typeOf[Map[Int, Boolean]])) // Map(K -> Int, V -> Boolean)
}
}
I have simple class with N fields.
case class Book(a: UUID... z: String)
and function:
def sort(books:Seq[Book], fields:Seq[SortingFields]) = {...}
where
case class SortingField(field: String, asc: Boolean)
where field - a field of the Book class, asc - a sorting direction.
So, in advance I dont know which fields (from 0 to N) and sorting orders come into my function to sort a books collection. It may be just a single ID field or all exist fields of a class in a particular order.
How could it be implemented?
I would use the existing Ordering trait for this and use a function that maps from Book to a field, i.e. Ordering.by[Book, String](_.author). Then you can simply sort with books.sorted(myOrdering). If I define a helper method on Book's companion object, getting these orderings is very simple:
object Book {
def by[A: Ordering](fun: Book => A): Ordering[Book] = Ordering.by(fun)
}
case class Book(author: String, title: String, year: Int)
val xs = Seq(Book("Deleuze" /* and Guattari */, "A Thousand Plateaus", 1980),
Book("Deleuze", "Difference and Repetition", 1968),
Book("Derrida", "Of Grammatology", 1967))
xs.sorted(Book.by(_.title)) // A Thousand, Difference, Of Grammatology
xs.sorted(Book.by(_.year )) // Of Grammatology, Difference, A Thousand
Then to chain the ordering by multiple fields, you can create custom ordering that proceeds through the fields until one comparison is non-zero. For example, I can add an extension method andThen to Ordering like this:
implicit class OrderingAndThen[A](private val self: Ordering[A]) extends AnyVal {
def andThen(that: Ordering[A]): Ordering[A] = new Ordering[A] {
def compare(x: A, y: A): Int = {
val a = self.compare(x, y)
if (a != 0) a else that.compare(x, y)
}
}
}
So I can write:
val ayt = Book.by(_.author) andThen Book.by(_.year) andThen Book.by(_.title)
xs.sorted(ayt) // Difference, A Thousand, Of Grammatology
With the nice answer provided by #0__ I've come up to folowing:
def by[A: Ordering](e: Book => A): Ordering[Book] = Ordering.by(e)
with
implicit class OrderingAndThen[A](private val self: Ordering[A]) extends AnyVal {
def andThen(that: Ordering[A]): Ordering[A] = new Ordering[A] {
def compare(x: A, y: A): Int = {
val a = self.compare(x, y)
if (a != 0) a else that.compare(x, y)
}
}
}
next I map name of a class field with a direction to actual ordering
def toOrdering(name: String, r: Boolean): Ordering[Book] = {
(name match {
case "id" => Book.by(_.id)
case "name" => Book.by(_.name)
}) |> (o => if (r) o.reverse else o)
}
using a forward pipe operator:
implicit class PipedObject[A](value: A) {
def |>[B](f: A => B): B = f(value)
}
and finally I combine all the ordering with the reduce function:
val fields = Seq(SortedField("name", true), SortedField("id", false))
val order = fields.map(f => toOrdering(f.field, f.reverse)).reduce(combines(_,_))
coll.sorted(order)
where
val combine = (x: Ordering[Book], y: Ordering[Book]) => x andThen y
An aternate way is to use #tailrec:
def orderingSeq[T](os: Seq[Ordering[T]]): Ordering[T] = new Ordering[T] {
def compare(x: T, y: T): Int = {
#tailrec def compare0(rest: Seq[Ordering[T]], result: Int): Int = result match {
case 0 if rest.isEmpty => 0
case 0 => compare0(rest.tail, rest.head.compare(x, y))
case a => a
}
compare0(os, 0)
}
}
It is possible. But as far as I can see you will have to use reflection.
Additionally, you would have to change your SortingField class a bit as there is no way the scala compiler can figure out the right Ordering type class for each field.
Here is a simplified example.
import scala.reflect.ClassTag
/** You should be able to figure out the correct field ordering here. Use `reverse` to decide whether you want to sort ascending or descending. */
case class SortingField[T](field: String, ord: Ordering[T]) { type FieldType = T }
case class Book(a: Int, b: Long, c: String, z: String)
def sort[T](unsorted: Seq[T], fields: Seq[SortingField[_]])(implicit tag: ClassTag[T]): Seq[T] = {
val bookClazz = tag.runtimeClass
fields.foldLeft(unsorted) { case (sorted, currentField) =>
// keep in mind that scala generates a getter method for field 'a'
val field = bookClazz.getMethod(currentField.field)
sorted.sortBy[currentField.FieldType](
field.invoke(_).asInstanceOf[currentField.FieldType]
)(currentField.ord)
}
}
However, for sorting by multiple fields you would have to either sort the sequence multiple times or better yet compose the various orderings correctly.
So this is getting a bit more 'sophisticated' without any guarantees about correctness and completeness, but with a little test that it does not fail spectacularly:
def sort[T](unsorted: Seq[T], fields: Seq[SortingField[_]])(implicit tag: ClassTag[T]): Seq[T] = {
#inline def invokeGetter[A](field: Method, obj: T): A = field.invoke(obj).asInstanceOf[A]
#inline def orderingByField[A](field: Method)(implicit ord: Ordering[A]): Ordering[T] = {
Ordering.by[T, A](invokeGetter[A](field, _))
}
val bookClazz = tag.runtimeClass
if (fields.nonEmpty) {
val field = bookClazz.getMethod(fields.head.field)
implicit val composedOrdering: Ordering[T] = fields.tail.foldLeft {
orderingByField(field)(fields.head.ord)
} { case (ordering, currentField) =>
val field = bookClazz.getMethod(currentField.field)
val subOrdering: Ordering[T] = orderingByField(field)(currentField.ord)
new Ordering[T] {
def compare(x: T, y: T): Int = {
val upperLevelOrderingResult = ordering.compare(x, y)
if (upperLevelOrderingResult == 0) {
subOrdering.compare(x, y)
} else {
upperLevelOrderingResult
}
}
}
}
unsorted.sorted(composedOrdering)
} else {
unsorted
}
}
sort(
Seq[Book](
Book(1, 5L, "foo1", "bar1"),
Book(10, 50L, "foo10", "bar15"),
Book(2, 3L, "foo3", "bar3"),
Book(100, 52L, "foo4", "bar6"),
Book(100, 51L, "foo4", "bar6"),
Book(100, 51L, "foo3", "bar6"),
Book(11, 15L, "foo5", "bar7"),
Book(22, 45L, "foo6", "bar8")
),
Seq(
SortingField("a", implicitly[Ordering[Int]].reverse),
SortingField("b", implicitly[Ordering[Long]]),
SortingField("c", implicitly[Ordering[String]])
)
)
>> res0: Seq[Book] = List(Book(100,51,foo3,bar6), Book(100,51,foo4,bar6), Book(100,52,foo4,bar6), Book(22,45,foo6,bar8), Book(11,15,foo5,bar7), Book(10,50,foo10,bar15), Book(2,3,foo3,bar3), Book(1,5,foo1,bar1))
Case classes are Products, so you can iterate over all field values using instance.productIterator. This gives you the fields in order of declaration. You can also access them directly via their index. As far as I can see, there is however no way to get the field names. This would have to be done using reflection or macros. (Maybe some library as Shapeless can already do that).
An other way would be to not define fields to sort by with names but with functions:
case class SortingField[T](field: Book => T, asc: Boolean)(implicit ordering: Ordering[T])
new SortingField(_.fieldName, true)
And then declare sort as:
def sort(books: Seq[Book], fields: Seq[SortingField[_]]) = {...}
And use the following compare method to implement the combined ordering:
def compare[T](b1: Book, b2: Book, field: SortingField[T]) =
field.ordering.compare(field.field(b1), field.field(b2))
Starting my first project with Scala: a poker framework.
So I have the following class
class Card(rank1: CardRank, suit1: Suit){
val rank = rank1
val suit = suit1
}
And a Utils object which contains two methods that do almost the same thing: they count number of cards for each rank or suit
def getSuits(cards: List[Card]) = {
def getSuits(cards: List[Card], suits: Map[Suit, Int]): (Map[Suit, Int]) = {
if (cards.isEmpty)
return suits
val suit = cards.head.suit
val value = if (suits.contains(suit)) suits(suit) + 1 else 1
getSuits(cards.tail, suits + (suit -> value))
}
getSuits(cards, Map[Suit, Int]())
}
def getRanks(cards: List[Card]): Map[CardRank, Int] = {
def getRanks(cards: List[Card], ranks: Map[CardRank, Int]): Map[CardRank, Int] = {
if (cards isEmpty)
return ranks
val rank = cards.head.rank
val value = if (ranks.contains(rank)) ranks(rank) + 1 else 1
getRanks(cards.tail, ranks + (rank -> value))
}
getRanks(cards, Map[CardRank, Int]())
}
Is there any way I can "unify" these two methods in a single one with "field/method-as-parameter"?
Thanks
Yes, that would require high order function (that is, function that takes function as parameter) and type parameters/genericity
def groupAndCount[A,B](elements: List[A], toCount: A => B): Map[B, Int] = {
// could be your implementation, just note key instead of suit/rank
// and change val suit = ... or val rank = ...
// to val key = toCount(card.head)
}
then
def getSuits(cards: List[Card]) = groupAndCount(cards, {c : Card => c.suit})
def getRanks(cards: List[Card]) = groupAndCount(cards, {c: Card => c.rank})
You do not need type parameter A, you could force the method to work only on Card, but that would be a pity.
For extra credit, you can use two parameter lists, and have
def groupAndCount[A,B](elements: List[A])(toCount: A => B): Map[B, Int] = ...
that is a little peculiarity of scala with type inference, if you do with two parameters lists, you will not need to type the card argument when defining the function :
def getSuits(cards: List[Card]) = groupAndCount(cards)(c => c.suit)
or just
def getSuits(cards: List[Card] = groupAndCount(cards)(_.suit)
Of course, the library can help you with the implementation
def groupAndCount[A,B](l: List[A])(toCount: A => B) : Map[A,B] =
l.groupBy(toCount).map{case (k, elems) => (k, elems.length)}
although a hand made implementation might be marginally faster.
A minor note, Card should be declared a case class :
case class Card(rank: CardRank, suit: Suit)
// declaration done, nothing else needed
My game has
class Enemy
who's AI/functionality I can change with
trait Moving
trait VerticalMover extends Moving
trait RandomMover extends Moving
and so on. Now I need to fetch preloaded stuff based on trait. What I would like to do is have a Map that accepts all traits that extend Moving as keys and then some EnemyContainer as value that would have trait related content preloaded.
But how do I define such a Map and how do format my .get() to get the container by an instance of some Enemy. Something like:
val myEnemy = new Enemy with RandomMover
val myDetails:EnemyContainer = enemyDetailsStore.get(myEnemy.getClass)
Maybe you could wrap a Map[Manifest, Any] ensuring that the values corresponds to the manifest keys.
Possible sketch of that. First a little helper
class Typed[A](value: A)(implicit val key: Manifest[A]) {
def toPair: (Manifest[_], Any) = (key, value)
}
object Typed {
implicit def toTyped[A: Manifest](a: A) = new Typed(a)
implicit def toTypable[A](a: A) = new {
def typedAs[T >: A : Manifest] = new Typed[T](a)(manifest[T])
}
}
then the wrapper itself (which is not a map)
class TypedMap private(val inner: Map[Manifest[_], Any]) {
def +[A](t: Typed[A]) = new TypedMap(inner + t.toPair)
def +[A : Manifest](a: A) = new TypedMap(inner + (manifest[A] -> a))
def -[A : Manifest]() = new TypedMap(inner - manifest[A])
def apply[A : Manifest]: A = inner(manifest[A]).asInstanceOf[A]
def get[A : Manifest]: Option[A] = inner.get(manifest[A]).map(_.asInstanceOf[A])
override def toString = inner.toString
override def equals(other: Any) = other match {
case that: TypedMap => this.inner == that.inner
case _ => false
}
override def hashCode = inner.hashCode
}
object TypedMap {
val empty = new TypedMap(Map())
def apply(items: Typed[_]*) = new TypedMap(Map(items.map(_.toPair) : _*))
}
With that you can do
import Typed._
val repository = TypedMap("foo", 12, "bar".typedAs[Any])
repository: TypedMap = Map(java.lang.String -> foo, Int -> 12, Any ->
bar)
You retrieve elements with
repository[String] // returns "foo"
repository.get[Any] // returns Some("bar")
I think the private constructor should ensure that the _asInstanceOf is safe. inner may be left public, as it is immutable. This way, the rich interface of Map will be available, but unfortunately, not to create another TypedMap.
Well, I assume that your enemy details store is of type Map[Class[_ <: Moving], EnemyDetails]. I suspect that something like:
//gives a Map[Class[_ <: Moving], EnemyDetails] for all matching keys
enemyDetailsStore.filterKeys(_ isInstance myEnemy)
Or:
//Iterable[EnemyDetails]
enemyDetailsStore collect { case (c, d) if c isInstance myEnemy => d }
Or even just:
//Option[EnemyDetails]
enemyDetailsStore collectFirst { case (c, d) if c isInstance myEnemy => d }
Will do for you. The only "issue" with this code is that it's O(N), in that it requires a traversal of the map, rather than a simple lookup, which would be O(1), or O(log N)