superponer - Diferentes tipos en el mapa Scala
superponer graficas en r ggplot (7)
(a) Los contenedores de Scala no rastrean la información de tipo de lo que se coloca dentro de ellos, y
(b) el "tipo" de devolución para un método de aplicación / obtención con un parámetro / clave String
simple será estático para una instancia determinada del objeto al que se aplicará el método.
Esto se parece mucho a una decisión de diseño que necesita ser repensada.
Necesito un Mapa donde coloque diferentes tipos de valores (Doble, Cadena, Int, ...), la clave puede ser Cadena.
¿Hay una manera de hacer esto, para que obtenga el tipo correcto con map.apply(k)
como
val map: Map[String, SomeType] = Map()
val d: Double = map.apply("double")
val str: String = map.apply("string")
Ya lo probé con un tipo genérico.
class Container[T](element: T) {
def get: T = element
}
val d: Container[Double] = new Container(4.0)
val str: Container[String] = new Container("string")
val m: Map[String, Container] = Map("double" -> d, "string" -> str)
pero no es posible ya que Container
toma un parámetro. Hay alguna solución para esto?
Esto es ahora muy sencillo en shapeless ,
scala> import shapeless._ ; import syntax.singleton._ ; import record._
import shapeless._
import syntax.singleton._
import record._
scala> val map = ("double" ->> 4.0) :: ("string" ->> "foo") :: HNil
map: ... <complex type elided> ... = 4.0 :: foo :: HNil
scala> map("double")
res0: Double with shapeless.record.KeyTag[String("double")] = 4.0
scala> map("string")
res1: String with shapeless.record.KeyTag[String("string")] = foo
scala> map("double")+1.0
res2: Double = 5.0
scala> val map2 = map.updateWith("double")(_+1.0)
map2: ... <complex type elided> ... = 5.0 :: foo :: HNil
scala> map2("double")
res3: Double = 5.0
Esto es con 2.0.0-SNAPSHOT sin forma a partir de la fecha de esta respuesta.
Esto no es sencillo.
El tipo de valor depende de la clave. Así que la clave tiene que llevar la información sobre qué tipo es su valor. Este es un patrón común. Se usa, por ejemplo, en SBT (ver, por ejemplo, SettingsKey[T] ) y Shapeless Records ( Example ). Sin embargo, en SBT las claves son una enorme jerarquía de clases, compleja, y la HList en forma sin forma es bastante compleja y también hace más de lo que desea.
Así que aquí hay un pequeño ejemplo de cómo podría implementar esto. La clave conoce el tipo y la única forma de crear un Registro o de obtener un valor de un Registro es la clave. Utilizamos un Mapa [Clave, Cualquiera] internamente como almacenamiento, pero las conversiones están ocultas y se garantiza que tendrán éxito. Hay un operador para crear registros a partir de claves y un operador para fusionar registros. Elegí los operadores para que pueda concatenar Registros sin tener que usar corchetes.
sealed trait Record {
def apply[T](key:Key[T]) : T
def get[T](key:Key[T]) : Option[T]
def ++ (that:Record) : Record
}
private class RecordImpl(private val inner:Map[Key[_], Any]) extends Record {
def apply[T](key:Key[T]) : T = inner.apply(key).asInstanceOf[T]
def get[T](key:Key[T]) : Option[T] = inner.get(key).asInstanceOf[Option[T]]
def ++ (that:Record) = that match {
case that:RecordImpl => new RecordImpl(this.inner ++ that.inner)
}
}
final class Key[T] {
def ~>(value:T) : Record = new RecordImpl(Map(this -> value))
}
object Key {
def apply[T] = new Key[T]
}
Aquí es cómo usarías esto. Primero defina algunas teclas:
val a = Key[Int]
val b = Key[String]
val c = Key[Float]
Luego úsalos para crear un registro
val record = a ~> 1 ++ b ~> "abc" ++ c ~> 1.0f
Cuando acceda al registro utilizando las teclas, obtendrá un valor del tipo correcto.
scala> record(a)
res0: Int = 1
scala> record(b)
res1: String = abc
scala> record(c)
res2: Float = 1.0
Encuentro este tipo de estructura de datos muy útil. A veces necesita más flexibilidad que la que proporciona una clase de caso, pero no desea recurrir a algo completamente inseguro como un Mapa [Cadena, Cualquiera]. Este es un buen término medio.
Edición: otra opción sería tener un mapa que use un par (nombre, tipo) como la clave real internamente. Debe proporcionar tanto el nombre como el tipo al obtener un valor. Si elige el tipo incorrecto no hay entrada. Sin embargo, esto tiene un gran potencial de errores, como cuando se ingresa un byte y se intenta obtener un int. Así que creo que esta no es una buena idea.
import reflect.runtime.universe.TypeTag
class TypedMap[K](val inner:Map[(K, TypeTag[_]), Any]) extends AnyVal {
def updated[V](key:K, value:V)(implicit tag:TypeTag[V]) = new TypedMap[K](inner + ((key, tag) -> value))
def apply[V](key:K)(implicit tag:TypeTag[V]) = inner.apply((key, tag)).asInstanceOf[V]
def get[V](key:K)(implicit tag:TypeTag[V]) = inner.get((key, tag)).asInstanceOf[Option[V]]
}
object TypedMap {
def empty[K] = new TypedMap[K](Map.empty)
}
Uso:
scala> val x = TypedMap.empty[String].updated("a", 1).updated("b", "a string")
x: TypedMap[String] = TypedMap@30e1a76d
scala> x.apply[Int]("a")
res0: Int = 1
scala> x.apply[String]("b")
res1: String = a string
// this is what happens when you try to get something out with the wrong type.
scala> x.apply[Int]("b")
java.util.NoSuchElementException: key not found: (b,Int)
Finalmente encontré mi propia solución, que funcionó mejor en mi caso:
case class Container[+T](element: T) {
def get[T]: T = {
element.asInstanceOf[T]
}
}
val map: Map[String, Container[Any]] = Map("a" -> Container[Double](4.0), "b" -> Container[String]("test"))
val double: Double = map.apply("a").get[Double]
val string: String = map.apply("b").get[String]
Hay una manera pero es complicada. Ver tipos de unión sin caja en Scala . Esencialmente tendrás que escribir el Map
para algún tipo Int |v| Double
Int |v| Double
para poder sostener tanto Int
como Double
. También pagará un alto precio en tiempos de compilación.
No creo que haya una manera de obtener map.apply()
desnudo para hacer lo que quieres. Como sugieren las otras respuestas, será necesario algún tipo de clase contenedora. Aquí hay un ejemplo que restringe los valores para que sean solo ciertos tipos (String, Double, Int, en este caso):
sealed trait MapVal
case class StringMapVal(value: String) extends MapVal
case class DoubleMapVal(value: Double) extends MapVal
case class IntMapVal(value: Int) extends MapVal
val myMap: Map[String, MapVal] =
Map("key1" -> StringMapVal("value1"),
"key2" -> DoubleMapVal(3.14),
"key3" -> IntMapVal(42))
myMap.keys.foreach { k =>
val message =
myMap(k) match { // map.apply() in your example code
case StringMapVal(x) => "string: %s".format(x)
case DoubleMapVal(x) => "double: %.2f".format(x)
case IntMapVal(x) => "int: %d".format(x)
}
println(message)
}
El principal beneficio del sealted trait
es la verificación en tiempo de compilación para coincidencias no exhaustivas en la coincidencia de patrones.
También me gusta este enfoque porque es relativamente simple para los estándares de Scala. Puede ir a la maleza por algo más robusto, pero en mi opinión, está disminuyendo los rendimientos con bastante rapidez.
Si quieres hacer esto, deberías especificar el tipo de Container
para que sea Any
, ya que Any
es un supertipo de Double
y String
.
val d: Container[Any] = new Container(4.0)
val str: Container[Any] = new Container("string")
val m: Map[String, Container[Any]] = Map("double" -> d, "string" -> str)
O para facilitar las cosas, puede cambiar la definición de Container
para que ya no sea de tipo invariante:
class Container[+T](element: T) {
def get: T = element
override def toString = s"Container($element)"
}
val d: Container[Double] = new Container(4.0)
val str: Container[String] = new Container("string")
val m: Map[String, Container[Any]] = Map("double" -> d, "string" -> str)