2013-07-16 19 views
16

Ich brauche eine Karte, wo ich verschiedene Arten von Werten (Double, String, Int, ...) drin, Schlüssel kann String sein.Verschiedene Arten in Map Scala

Gibt es eine Möglichkeit, dies zu tun, so dass ich den richtigen Typen mit map.apply(k) wie

val map: Map[String, SomeType] = Map() 
val d: Double = map.apply("double") 
val str: String = map.apply("string") 

Ich versuchte es bereits mit einem generischen Typ

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) 

aber es ist nicht möglich, da Container Nimmt einen Parameter an. Gibt es dafür eine Lösung?

+1

Mögliches Duplikat von http://stackoverflow.com/questions/4309835/scala-reflection/4310959 –

Antwort

15

Das in shapeless jetzt sehr einfach ist,

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 

Thi s ist mit formlosem 2.0.0-SNAPSHOT ab dem Datum dieser Antwort.

+3

Das ist ziemlich ordentlich. Aber was sind die Zugriffseigenschaften, wenn ein HList als Karte verwendet wird? Da es sich im Grunde um eine Single-Linked-Liste handelt, müssen Sie jedes Element überprüfen, also wäre die Suche O (N), oder? Wird es nicht auch zu schrecklich komplexen Typen, wenn Sie mehr als ein paar Elemente haben? –

3

(a) Scala Container Typinformationen nicht für verfolgen, was in ihnen platziert wird, und

(b) die Rückkehr „Typ“ für eine Anwendung/get-Methode mit einem einfachen String Parameter/Taste wird zu für eine bestimmte Instanz des Objekts statisch sein, auf das die Methode angewendet werden soll.

Dies fühlt sich sehr wie eine Designentscheidung an, die überdacht werden muss.

1

Wenn Sie dies tun möchten, würden Sie die Art der Container angeben müssen Any sein, weil Any sowohl ein übergeordneter Typ von Double ist und 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) 

Oder die Dinge einfacher zu machen, können Sie die Definition von Container ändern, so dass es nicht mehr Typ unveränderlich ist:

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) 
+0

Ich habe das bereits versucht. Aber wenn ich den 'Cointainer' als' Any' definiere, bekomme ich einen 'Any'-Wert auf' map.apply (k) ', also z. 'val d: Double = map.apply (" double ")' ist nicht möglich. – 3x14159265

2

ich einen Weg gibt es nicht denken zu bekommen nackten map.apply() zu tun, was du würdest es wollen. Wie die anderen Antworten nahelegen, wird eine Art Container-Klasse notwendig sein. Hier ist ein Beispiel, dass die Werte werden nur bestimmte Typen (String, Double, Int, in diesem Fall) beschränkt:

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) 
} 

Der Hauptvorteil der sealted trait ist Compiler-Überprüfung für nicht erschöpfende Spiele in Mustervergleich .

Ich mag auch diesen Ansatz, weil es von Scala-Standards relativ einfach ist. Sie können in das Unkraut für etwas robuster gehen, aber meiner Meinung nach sind Sie in abnehmenden Renditen ziemlich schnell.

1

Es gibt einen Weg, aber es ist kompliziert. Siehe Unboxed union types in Scala. Im Wesentlichen müssen Sie die Map auf einen Typ Int |v| Double eingeben, um sowohl Int als auch Double zu halten. Sie werden auch einen hohen Preis in Kompilierzeiten zahlen.

21

Dies ist nicht einfach.

Die Art des Wertes hängt vom Schlüssel ab. Also muss der Schlüssel die Information darüber tragen, um welchen Typ es sich handelt. Dies ist ein allgemeines Muster. Es wird zum Beispiel in SBT (siehe zum Beispiel SettingsKey[T]) und Shapeless Records (Example) verwendet. In SBT sind die Schlüssel jedoch eine riesige, komplexe Klassenhierarchie, und der HList in formlos ist ziemlich komplex und macht auch mehr, als Sie wollen.

Hier ist ein kleines Beispiel, wie Sie dies implementieren könnten.Der Schlüssel kennt den Typ, und die einzige Möglichkeit, einen Datensatz zu erstellen oder einen Wert aus einem Datensatz zu erhalten, ist der Schlüssel. Wir verwenden eine Map [Key, Any] intern als Speicher, aber die Casts sind versteckt und garantiert erfolgreich. Es gibt einen Operator zum Erstellen von Datensätzen über Schlüssel und einen Operator zum Zusammenführen von Datensätzen. Ich habe die Operatoren ausgewählt, damit Sie Datensätze verketten können, ohne Klammern zu verwenden.

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] 
} 

Hier ist, wie Sie dies verwenden würden. Zunächst einige Schlüssel definieren:

val a = Key[Int] 
val b = Key[String] 
val c = Key[Float] 

Dann sie einen Datensatz

val record = a ~> 1 ++ b ~> "abc" ++ c ~> 1.0f 

Beim Zugriff auf den Datensatz mit den Tasten erstellen, erhalten Sie einen Wert des richtigen Typs zurück

scala> record(a) 
res0: Int = 1 

scala> record(b) 
res1: String = abc 

scala> record(c) 
res2: Float = 1.0 

Ich finde diese Art von Datenstruktur sehr nützlich. Manchmal benötigen Sie mehr Flexibilität, als eine Fallklasse bietet, aber Sie möchten nicht auf etwas zurückgreifen, das völlig typsicher ist, wie eine Map [String, Any]. Dies ist ein guter Mittelweg.

Bearbeiten: Eine andere Option wäre eine Karte zu haben, die intern ein (Name, Typ) -Paar als reellen Schlüssel verwendet. Sie müssen sowohl den Namen als auch den Typ angeben, wenn Sie einen Wert erhalten. Wenn Sie den falschen Typ wählen, gibt es keinen Eintrag. Dies hat jedoch ein großes Potenzial für Fehler, wie wenn Sie ein Byte einfügen und versuchen, einen int herauszuholen. Also ich denke, das ist keine gute Idee.

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) 
} 

Verbrauch:

scala> val x = TypedMap.empty[String].updated("a", 1).updated("b", "a string") 
x: TypedMap[String] = [email protected] 

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) 
+1

das ist ein schönes Muster. Leider müssen Sie die Methode 'apply' mit einem Kommentar versehen. Wie auch immer, ich denke, es ist die beste Lösung bisher. Danke für deine Antwort! – 3x14159265

+0

Du meinst die zweite? Bitte beachten Sie, dass bei Verwendung eines ClassTags Probleme auftreten, wenn Sie einen Scala-Typ eingeben, der kein JVM-Äquivalent aufweist. Sie können zum Beispiel eine Liste [Int] einfügen und nach einer Liste [String] fragen, und Sie werden etwas herausbekommen, weil List [Int] und List [String] dasselbe Löschen haben. Wenn Sie möchten, dass es auch in diesem Fall funktioniert, müssen Sie stattdessen einen TypeTag verwenden, der den gesamten Scala-Typ erfasst und nicht nur den JVM-Typ. Ich habe das Beispiel entsprechend aktualisiert. –

+0

Dein erstes Muster ist eine sehr nützliche Antwort, wenn man nicht die generelle Magie von Shapeless braucht, aber trotzdem einen Container mit mehreren Arten von Werten braucht, vielen Dank! – Egregore

3

ich endlich meine eigene Lösung gefunden, die am besten in meinem Fall gearbeitet:

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] 
Verwandte Themen