2010-07-06 13 views
8

Me encuentro utilizando una gran cantidad de mapas anidados, por ejemplo, un Mapa [Int, Mapa [Cadena, Establecer [Cadena]]], y me gustaría tener nuevos Mapas, Conjuntos, etc. creado automáticamente cuando accedo a una nueva clave. P.ej. algo así como lo siguiente:Diseñando un conveniente mapa valorado por defecto en Scala

val m = ... 
m(1992)("foo") += "bar" 

Tenga en cuenta que no quiero utilizar getOrElseUpdate aquí si no tengo que porque se pone bastante detallado cuando haya anidado mapas y oscurece lo que está sucediendo realmente en el código:

m.getOrElseUpdate(1992, Map[String, Set[String]]()).getOrElseUpdate("foo", Set[String]()) ++= "bar" 

Así que estoy anulando el método "predeterminado" de HashMap. He intentado dos formas de hacer esto, pero ninguna es completamente satisfactoria. Mi primera solución era escribir un método que ha creado el mapa, pero parece que todavía tengo que especificar el tipo completo Mapa anidada cuando declaro la variable o las cosas no funcionan:

scala> def defaultingMap[K, V](defaultValue: => V): Map[K, V] = new HashMap[K, V] {      | override def default(key: K) = { 
|  val result = defaultValue 
|  this(key) = result 
|  result 
| } 
| } 
defaultingMap: [K,V](defaultValue: => V)scala.collection.mutable.Map[K,V] 

scala> val m: Map[Int, Map[String, Set[String]]] = defaultingMap(defaultingMap(Set[String]())) 
m: scala.collection.mutable.Map[Int,scala.collection.mutable.Map[String,scala.collection.mutable.Set[String]]] = Map() 

scala> m(1992)("foo") += "bar"; println(m)              
Map(1992 -> Map(foo -> Set(bar))) 

scala> val m = defaultingMap(defaultingMap(Set[String]())) 
m: scala.collection.mutable.Map[Nothing,scala.collection.mutable.Map[Nothing,scala.collection.mutable.Set[String]]] = Map() 

scala> m(1992)("foo") += "bar"; println(m) 
<console>:11: error: type mismatch; 
found : Int(1992) 
required: Nothing 
     m(1992)("foo") += "bar"; println(m) 
     ^

Mi segunda solución era escribir una clase de fábrica con un método, y de esa manera solo tengo que declarar cada tipo una sola vez. Pero entonces cada vez que quiera un nuevo mapa valorada por defecto, tengo que ambas instancias de la clase de fábrica y luego llamar al método, que todavía parece un poco prolijo:

scala> class Factory[K] {          
| def create[V](defaultValue: => V) = new HashMap[K, V] { 
|  override def default(key: K) = {      
|  val result = defaultValue       
|  this(key) = result         
|  result            
|  }              
| }              
| }               
defined class Factory 

scala> val m = new Factory[Int].create(new Factory[String].create(Set[String]())) 
m: scala.collection.mutable.HashMap[Int,scala.collection.mutable.HashMap[String,scala.collection.mutable.Set[String]]] = Map() 

scala> m(1992)("foo") += "bar"; println(m) 
Map(1992 -> Map(foo -> Set(bar))) 

que realmente me gustaría tener algo tan simple como este:

val m = defaultingMap[Int](defaultingMap[String](Set[String]())) 

¿Alguien ve la manera de hacer eso?

Respuesta

5

Con Scala 2.8:

object DefaultingMap { 
    import collection.mutable 
    class defaultingMap[K] { 
    def apply[V](v: V): mutable.Map[K,V] = new mutable.HashMap[K,V] { 
     override def default(k: K): V = { 
     this(k) = v 
     v 
     } 
    } 
    } 
    object defaultingMap { 
    def apply[K] = new defaultingMap[K] 
    } 

    def main(args: Array[String]) { 
    val d4 = defaultingMap[Int](4) 
    assert(d4(3) == 4) 
    val m = defaultingMap[Int](defaultingMap[String](Set[String]())) 
    m(1992)("foo") += "bar" 
    println(m) 
    } 
} 

No puede ganarse parámetros de tipo en Scala, por lo tanto, el truco con la clase para capturar el tipo de clave es necesario.

Por cierto: no creo que la API resultante sea muy clara. En particular, no me gusta el acceso al mapa de efecto secundario.

+0

veo, la El truco es usar una capa de objeto acompañante encima de la clase de fábrica. Gracias, genial! Por curiosidad, ¿qué API usarías? Estoy tratando de evitar la atrocidad getOrElseUpdate anidada que mostré en el segundo bloque de código anterior. Estoy ciertamente abierto a otras formas de hacerlo. – Steve

3

Resulta que necesito extender MapLike también, o cuando llamo a filtro, mapa, etc. mi mapa validado por defecto se volverá a convertir en un mapa normal sin la semántica por defecto. Aquí está una variante de la solución de mkneissl que hace lo correcto para el filtro, mapa, etc.

import scala.collection.mutable.{MapLike,Map,HashMap} 

class DefaultingMap[K, V](defaultValue: => V) extends HashMap[K, V] 
with MapLike[K, V, DefaultingMap[K, V]] { 
    override def empty = new DefaultingMap[K, V](defaultValue) 
    override def default(key: K): V = { 
    val result = this.defaultValue 
    this(key) = result 
    result 
    } 
} 

object DefaultingMap { 
    def apply[K] = new Factory[K] 
    class Factory[K] { 
    def apply[V](defaultValue: => V) = new DefaultingMap[K, V](defaultValue) 
    } 
} 

Y aquí, es decir, en la acción, haciendo lo correcto con filtro:

scala> val m = DefaultingMap[String](0) 
m: DefaultingMap[String,Int] = Map() 

scala> for (s <- "the big black bug bit the big black bear".split(" ")) m(s) += 1 

scala> val m2 = m.filter{case (_, count) => count > 1} 
m2: DefaultingMap[String,Int] = Map((the,2), (big,2), (black,2))