Scala: реализация карты с конкретными типами - PullRequest
4 голосов
/ 10 августа 2011

Я сталкиваюсь с какой-то причудой в системе типов Scala, которая немного озадачила меня.Я пытаюсь создать класс, который расширяет Map [String, String], и я не могу понять, как реализовать метод + таким образом, чтобы компилятор его принял.

Вот код, который у меня естьсейчас:

class ParamMap(val pairs:List[(String,String)] = Nil) extends Map[String,String] {

  lazy val keyLookup = Map() ++ pairs

  override def get(key: String): Option[String] = keyLookup.get(key)
  override def iterator: Iterator[(String, String)] = pairs.reverseIterator

  /**
   * Add a key/value pair to the map
   */
  override def + [B1 >: String](kv: (String, B1)) = new ParamMap(kv :: pairs)

  /**
   * Remove all values for the given key from the map
   */
  override def -(key: String): ParamMap  = new ParamMap(pairs.filterNot(_._1 == key))

  /**
   * Remove a specific pair from the map
   */
  def -(kv: (String, String)) : ParamMap = new ParamMap(pairs - kv)
}

Scala говорит мне следующее:

type mismatch;  found: (String, B1)  required: (String, String)

Я считаю, что это потому, что B1 может быть подтипом String, но мой конструктор ожидает только String (?).Моя первоначальная попытка была:

override def +(kv: (String, String)) = new ParamMap(kv :: pairs)

Но эта жалоба была вызвана тем, что сигнатура типа не соответствовала признаку:

class ParamMap needs to be abstract, since method + in trait Map of type [B1 >: String](kv: (String, B1))scala.collection.immutable.Map[String,B1] is not defined
method + overrides nothing

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

Есть идеи?

Ответы [ 4 ]

7 голосов
/ 10 августа 2011

Некоторые сведения о системе типов Scala.

  • Синтаксис B1 >: String означает, что B1 является супертипом из String. Так что B1 менее конкретен и не может быть приведен к String. И наоборот, B1 <: String будет отношением подтип .

  • Определение признака Map: Map [A, +B], где A представляет тип ключа и B тип значения. Запись +B говорит, что Map является ковариантным в типе ключа, что означает, что T <: S подразумевает Map[A, T] <: Map[A, S].

  • Полный тип метода Map.+ - + [B1 >: B] (kv: (A, B1)): Map[A, B1]. Ковариация вида B заставляет использовать B1 >: B. Вот пример того, как это работает: учитывая карту m: Map[String, String] добавление пары ключ-значение с менее определенным типом kv : (String, Any) приведет к менее конкретной карте (m + kv): Map[String, Any].

Последний пункт иллюстрирует проблему с вашим ParamMap определением. Согласно интерфейсу Map, можно добавить ключ типа Any к карте типа ParamMap <: Map[String, String] и получить обратно Map[String, Any]. Но вы пытаетесь определить ParamMap.+, чтобы всегда возвращать ParamMap[String, String], что несовместимо с Map.+.

Одним из способов решения проблемы является предоставление ParamMap явного параметра типа, что-то вроде (предупреждение не проверено),

class ParamMap[B](val pairs:List[(String,String)] = Nil) extends Map[String, B] {
  ...
  override def + [B1 >: B](kv: (String, B1)) = new ParamMap[B1](kv :: pairs)
}

но это может быть не то, что вы хотите. Я не думаю, что есть способ исправить тип значения как String и реализовать интерфейс Map[String, String].


Учитывая все вышесказанное, почему код в вашем ответе компилируется? Вы фактически обнаружили ограничение (несостоятельность) соответствия шаблонов Scala, и это может привести к сбоям во время выполнения. Вот упрощенный пример:

def foo [B1 >: String](x: B1): Int = {
  val (s1: Int, s2: Int) = (x, x)
  s1
}

Хотя это компилируется, но ничего полезного не дает. На самом деле, он всегда будет зависать с MatchError:

scala> foo("hello")
scala.MatchError: (hello,hello) (of class scala.Tuple2)
    at .foo(<console>:9)
    at .<init>(<console>:10)
    at .<clinit>(<console>)
    ...

В своем ответе вы в основном сказали компилятору преобразовать экземпляр B1 в String, и если преобразование не сработает, вы получите сбой во время выполнения. Это эквивалентно небезопасному броску,

(value: B1).asInstanceOf[String]
4 голосов
/ 10 августа 2011

Вы правы, что ваш конструктор ожидает значение типа List[String, String], но проблема не в том, что B1 может быть подклассом String, а в том, что это может быть суперкласс - этоэто то, что указывает нотация B1 :> String.

На первый взгляд, вы можете задаться вопросом, почему родительский класс Map имеет метод, напечатанный таким образом.Фактически, тип возвращаемого значения метода +, который вы пытаетесь переопределить, - Map[String, B1].Однако в контексте общей карты это имеет смысл.Предположим, у вас был следующий код:

class Parent
class Child extends Parent

val childMap = Map[String, Child]("Key" -> new Child)

val result = childMap + ("ParentKey" -> new Parent)

Тип result должен был бы быть Map[String, Parent].В свете этого, ограничения типа для метода + в Map имеют смысл, но ваша карта фиксированного типа не способна выполнить то, для чего предназначен метод.Его подпись позволяет вам передавать значение, например, типа (String, AnyRef), но, используя определение метода, которое вы дали в своем ответе, вы получите MatchError, когда он попытается выполнить присваивание key и value.

Имеет ли это смысл?

2 голосов
/ 10 августа 2011

Я столкнулся с той же проблемой с коллегой, когда пытался построить Bag[T], то есть Map[T,Int].Мы нашли два разных решения:

  1. Реализация Traversable вместо Map с соответствующими Builder и CanBuildFrom и добавление полезных методов карты (get, +, -),Если вам нужно передать коллекцию в функцию, принимающую карты в качестве аргументов, вы можете использовать неявные преобразования.Вот наша полная реализация Bag: https://gist.github.com/1136259

  2. Будьте просты:

    object collection {
      type ParamMap = Map[String,String]
      object ParamMap {
        def apply( pairs: List[(String,String)] = Nil ) = Map( pairs:_* )
      }
    }
    
0 голосов
/ 10 августа 2011

Компилятор, похоже, принимает это:

  override def + [B1 >: String](kv: (String, B1)) = {
    val (key:String, value:String) = kv
    new ParamMap((key,value) :: pairs)
  }

Но я не знаю, почему это лучше, чем оригинал. Я полагаю, что это приемлемое решение, если ни у кого нет лучшего.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...