Как создать подкласс Scala immutable.Map с параметрами фиксированного типа?

Я не могу понять, как справиться с переопределением «+» в неизменяемой карте, если карта может хранить только инвариантный тип для своих значений.

Что-то типа:

class FixedMap(val impl : Map[String, Int])
    extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {
    // This should return FixedMap if B1 is Int, and Map[String,B1]
    // if B1 is a superclass of Int; but there's no way to do that.
    // It is possible to return FixedMap here but then you have to
    // throw at runtime if B1 is not Int
    override def +[B1 >: Int](kv : (String, B1)) : Map[String, B1] = {
        kv match {
            case (k, v : Int) =>
                new FixedMap(impl + Pair(k, v))
            case _ =>
                impl + kv
        }
    }
    // ...
}

Я бы хотел, чтобы это работало как методы, использующие CanBuildFrom, и всегда сохраняло исходный тип, если это возможно. Есть ли способ? Или подклассы Map всегда должны оставлять тип значения в качестве параметра типа?

Вот полный компилируемый пример:

import scala.collection.immutable

// pointless class that wraps another map and adds one method
class FixedMap(val impl : Map[String, Int])
    extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {

    override val empty : FixedMap = FixedMap.empty

    // This should return FixedMap if B1 is Int, and Map[String,B1]
    // if B1 is a superclass of Int; but there's no way to do that.
    // It is possible to return FixedMap here but then you have to
    // throw at runtime if B1 is not Int
    override def +[B1 >: Int](kv : (String, B1)) : Map[String, B1] = {
        kv match {
            case (k, v : Int) =>
                new FixedMap(impl + Pair(k, v))
            case _ =>
                impl + kv
        }
    }

    override def -(key : String) : FixedMap = {
        new FixedMap(impl - key)
    }

    override def get(key : String) : Option[Int] = {
        impl.get(key)
    }

    override def iterator : Iterator[(String, Int)] = {
        impl.iterator
    }

    def somethingOnlyPossibleOnFixedMap() = {
        println("FixedMap says hi")
    }
}

object FixedMap {
    val empty : FixedMap = new FixedMap(Map.empty)
}

object TestIt {
    val empty = FixedMap.empty
    empty.somethingOnlyPossibleOnFixedMap()
    val one = empty + Pair("a", 1)
    // Can't do the below because one is a Map[String,Int] not a FixedMap
    // one.somethingOnlyPossibleOnFixedMap()
}

person Havoc P    schedule 05.05.2011    source источник
comment
Можете ли вы либо принять ответ, либо рассказать нам больше о том, что вы хотите знать?   -  person Jean-Philippe Pellet    schedule 16.05.2011
comment
принял ваш ответ, хотя я проверил только свой собственный ответ, в котором вместо неявного фиктивного неявного используется canbuildfrom. та же основная идея.   -  person Havoc P    schedule 16.05.2011


Ответы (3)


Вот что я бы попробовал:

class FixedMap(val impl: immutable.Map[String, Int])
    extends immutable.Map[String, Int] with immutable.MapLike[String, Int, FixedMap] {

  override def +[B1 >: Int](kv: (String, B1)): immutable.Map[String, B1] = impl + kv

  def +(kv: (String, Int))(implicit d: DummyImplicit): FixedMap = new FixedMap(impl + kv)

  // ...

}

Вы правы: вы не можете переопределить уже существующий +, поэтому вам придется оставить его там, иначе ваш подкласс не сможет делать то, что могут делать суперклассы, что нарушает принцип замещения Лисков. Но вы можете добавить дополнительный метод с точными аргументами, которые вам нужны (и вам не нужен CanBuildFrom в этом конкретном случае).

Единственная проблема заключается в том, что новый метод имеет тот же самый тип стирания, что и тот, который вы пытались переопределить, но который имеет несовместимую сигнатуру. Чтобы решить эту проблему, вы можете добавить DummyImplicit — определенный в Predef класс, для которого всегда доступно неявное значение. Его основное использование — обойти стирание типа в случае такой перегрузки.

Обратите внимание, что статический тип карты, на которой вы хотите вызвать перегруженный метод, должен быть FixedMap, чтобы это работало. Если объект имеет тип времени выполнения FixedType, но статически типизирован как обычный Map[String, Int], компилятор не будет вызывать ваш новый перегруженный метод.

person Jean-Philippe Pellet    schedule 06.05.2011

Можно реализовать то, что вы хотите, используя CanBuildFrom. Вопрос в том, действительно ли вы хотите/должны это сделать? В SO есть несколько похожих вопросов, вот один из них (надеюсь, вы найдете там ответ):

Расширение коллекций Scala

Как правило, Scala предоставляет достаточно инструментов, чтобы избежать этого (расширение коллекций). И вам действительно нужна веская причина, чтобы начать с этого.

person tenshi    schedule 05.05.2011
comment
Я читал об этом главу в книге "Программирование на Scala" и думаю, что CanBuildFrom обычно является ответом на подобные вопросы, но насколько я могу судить, это не ответ на этот вопрос. Я как бы думаю, что проблема может заключаться в том, что + в базовом классе не имеет (неявного bf : CanBuildFrom) на нем. Вы не можете добавить его в подкласс, потому что тогда вы больше не будете переопределять +. Я уверен, что что-то упускаю, но что? Я запутался с такими вещами, как добавление дополнительных перегрузок +, но это просто создает неоднозначные проблемы с перегрузкой. - person Havoc P; 06.05.2011
comment
Я могу себе представить, что если бы + в базовом классе имел неявный построитель, вам вообще не нужно было бы переопределять +, поскольку его можно было бы реализовать для вас с помощью построителя. Вместо этого вам придется реализовать конструктор, чтобы переопределить, как работает добавление элементов. Прямо сейчас построитель по умолчанию реализован с +, поэтому вам нужно переопределить +, чтобы заставить построитель работать, но, к сожалению, переопределение + не дает вам возможности изменить результирующий тип коллекции, в то время как переопределение построителя даст вам это. - person Havoc P; 06.05.2011
comment
@Havoc P: Вы совершенно правы. Кажется, мне нужно более внимательно читать вопросы, извините за это. Я проверил черты, связанные с Map, и обнаружил, что CanBuildFrom нигде не используется. Это выглядит странно, потому что все эти реализации Map на самом деле определяют CanBuildFrom в своих сопутствующих объектах. Не уверен, почему CanBuildFrom не используется в свойствах Map (может быть, сложность реализации) - person tenshi; 06.05.2011
comment
Он работает за исключением для + кажется, что я получаю правильные типы, когда я использую другие операции, если я делаю + возвращаю FixedMap вместо Map. Что имеет смысл, так как предварительно созданный строитель возвращает правильную вещь. Но тогда вы должны выдать ошибку времени выполнения, если RHS + имеет супертип вашего инвариантного типа значения, поскольку вы больше не можете возвращать карту из +, так что это хакерское компромиссное решение. В основном это похоже на проблему дизайна в базовом классе, что строитель не используется (и не может использоваться) для +. Но я все еще надеюсь ошибиться в этом. - person Havoc P; 06.05.2011
comment
Теперь я думаю, что вы все-таки были правы, я опубликовал ответ со своим выводом (в основном потому, что я не могу вставлять код в комментарии). Решение этой проблемы с другой перегрузкой +, похоже, работает нормально, раньше я получал кучу ошибок из-за того, что неявные преобразования перестали работать, но как только я перестал думать о том, почему это так, это имеет смысл. - person Havoc P; 06.05.2011

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

def +(kv : (String, Int))(implicit bf : CanBuildFrom[FixedMap, (String, Int), FixedMap]) : FixedMap = {
    val b = bf(empty)
    b ++= this
    b += kv
    b.result
}

Проблема, с которой я столкнулся при этом раньше, была вызвана возвратом FixedMap из переопределенного +, что предотвратило любое обновление до общей карты. Я предполагаю, что это позволило неявное преобразование пары +'d работать. Но если вы исправите этот переопределенный метод +, чтобы он снова возвращал Map[String,B1], вы больше не сможете использовать неявное преобразование для значения пары. У компилятора нет возможности узнать, следует ли перейти к карте суперкласса, т. Е. Map [String, Any], или неявно преобразовать в Int, чтобы придерживаться FixedMap. Интересно, что возвращаемый тип метода меняется независимо от того, используются ли неявные преобразования; Я думаю, учитывая возвращаемый тип FixedMap, компилятор может сделать вывод, что B1 всегда просто B (или что-то в этом роде!).

Во всяком случае, это, кажется, моя ошибка; вы просто не можете использовать неявное преобразование для пары._2, переданной +, будучи совместимым с интерфейсом карты, даже концептуально. Теперь, когда я отказался от этого, я думаю, что перегруженный + будет работать нормально.

person Havoc P    schedule 06.05.2011