Различные типы в Map Scala - программирование
Подтвердить что ты не робот

Различные типы в Map Scala

Мне нужна карта, где я помещаю в нее разные типы значений (Double, String, Int,...), ключ может быть строкой.

Есть ли способ сделать это, так что я получаю правильный тип с map.apply(k) как

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

Я уже пробовал его с общим типом

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)

но это невозможно, так как Container принимает параметр. Есть ли какое-либо решение для этого?

4b9b3361

Ответ 1

Теперь это очень просто в 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

Это с бесформенным 2.0.0-SNAPSHOT на дату ответа.

Ответ 2

Это не просто.

Тип значения зависит от ключа. Таким образом, ключ должен нести информацию о том, какой тип имеет значение. Это общая картина. Он используется, например, в SBT (см., Например, SettingsKey [T]) и Shapeless Records (Example). Однако в SBT ключи представляют собой огромную сложную иерархию классов, а HList в бесформенном довольно сложна и также делает больше, чем вы хотите.

Итак, вот небольшой пример того, как вы могли это реализовать. Ключ знает тип, и единственный способ создать запись или получить значение из записи - это ключ. Мы используем Map [Key, Any] внутри хранилища, но приведения скрыты и гарантированно преуспеют. Существует оператор для создания записей из ключей и оператора для объединения записей. Я выбрал операторы, чтобы вы могли конкатенировать записи без использования скобок.

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

Вот как вы это используете. Сначала определите некоторые клавиши:

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

Затем используйте их для создания записи

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

При доступе к записи с помощью клавиш вы получите значение правильного типа назад

scala> record(a)
res0: Int = 1

scala> record(b)
res1: String = abc

scala> record(c)
res2: Float = 1.0

Я считаю, что такая структура данных очень полезна. Иногда вам требуется больше гибкости, чем класс case, но вы не хотите прибегать к чему-то совершенно небезопасному типа "Карта" [String, Any]. Это хорошая средняя почва.


Изменить: другой вариант должен состоять в том, чтобы иметь карту, которая использует внутреннюю пару (имя, тип) в качестве реального ключа. При получении значения вы должны указать имя и тип. Если вы выберете неправильный тип, нет записи. Однако это имеет большой потенциал для ошибок, например, когда вы помещаете байты и пытаетесь выйти из int. Поэтому я думаю, что это не очень хорошая идея.

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

Использование:

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)

Ответ 3

Контейнеры

(a) Scala не отслеживают информацию о типе для того, что находится внутри них, и

(b) возвращаемый тип для метода apply/get с простым параметром /t String будет статическим для данного экземпляра объекта, к которому должен применяться метод.

Это очень похоже на дизайнерское решение, которое нужно переосмыслить.

Ответ 4

Наконец-то я нашел свое собственное решение, которое лучше всего работало в моем случае:

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]

Ответ 5

Я не думаю, что есть способ, чтобы обнажить map.apply(), чтобы делать то, что вы хотите. Как показывают другие ответы, потребуется какой-то контейнерный класс. Здесь пример, который ограничивает значения только определенными типами (String, Double, Int, в этом случае):

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

Основным преимуществом sealted trait является проверка времени компиляции для не исчерпывающих совпадений в сопоставлении с образцом.

Мне также нравится этот подход, потому что он относительно прост по стандартам Scala. Вы можете уйти в сорняки для чего-то более крепкого, но, на мой взгляд, вы очень быстро возвращаетесь.

Ответ 6

Если вы хотите сделать это, вам нужно указать тип Container как Any, потому что Any является супертипом как Double, так и 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)

Или для упрощения, вы можете изменить определение Container, чтобы он больше не вводил инвариант:

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)

Ответ 7

Есть способ, но это сложно. См. Unboxed union types в Scala. По существу вам придется ввести Map в некоторый тип Int |v| Double, чтобы иметь возможность удерживать как Int, так и Double. Вы также заплатите высокую цену во время компиляции.