рекурсивно преобразовать Map[Int, Map[Int, X]] в Array[Array[X]]

Я пытаюсь написать функцию, которая преобразует карты с целочисленными ключами в соответствующие массивы. У меня есть базовый вариант, но я пытаюсь написать рекурсивный случай (т.е. многомерные массивы: преобразование Map[Int, Map[Int, X]] в Array[Array[X]]).

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

У меня есть функция, которая это делает:

def toArrayHard[X:ClassManifest](x:scala.collection.Map[Int, X]):Array[X] =
{
    if (x.size == 0) new Array(0)
    else 
    {
        val max:Int = 1 + x.keys.max

        val a:Array[X] = new Array(max)

        var i = 0
        while (i < max)
        {
            a(i) = x(i)
            i += 1
        }
        a
    }
}

Обратите внимание: я знаю, что код не работает, если карта содержит ключ k, но не содержит ключ i, где 0 ‹= i ‹ k. Это нормально для моих целей.

Теперь я хочу сделать то же самое для произвольно глубоких многомерных массивов. Например, преобразование Map[Int, Map[Int, X]] в Array[Array[X]]. К сожалению, меня сбивают с толку типы. Используя приведенное выше в качестве базового случая, вот что у меня есть до сих пор:

def toArrayHardRec[X:ClassManifest](x:scala.collection.Map[Int, X]):Array[X] =
{
    import scala.collection.Map

    if (x.size == 0) new Array(0)
    else 
    {
        x match
        {
            case t:Map[Int, Map[Int, Y]] forSome { type Y } =>
            {
                val f0 = t.mapValues{m => toArrayHardRec[Map[Int, Y]](m)}
                toArrayHard(f0)
            }
            case _ => toArrayHard(x)
        }
    }
}

Это ошибка, которую я получаю:

Ожидается '=>', но найдено 'forSome'.

Поскольку это образовательное занятие, мы будем очень признательны за любые отзывы. В частности, я был бы признателен за любую критику моего ужасно похожего на Java кода, существующих функций scala, которые делают то же самое, или предложения по альтернативному способу создания этих массивов.


person dsg    schedule 04.12.2010    source источник
comment
Вам следует избегать размещения { на следующей строке. Это может вызвать проблемы с выводом точки с запятой.   -  person BenjaminJackman    schedule 05.12.2010
comment
@KingCub: Что ты имеешь в виду? Можете ли вы привести пример?   -  person dsg    schedule 06.12.2010


Ответы (1)


Это бессмысленно:

case t:Map[Int, Map[Int, Y]] forSome { type Y }

из-за стирания все, что вы можете проверить, это:

case t: Map[_, _]

что, по сути, является единственным способом избежать предупреждений о стирании. Кроме того, экзистенциальный тип почти всегда не нужен, и лично я всегда нахожу его синтаксис немного сложным для правильного понимания. Тем не менее, это единственный случай, когда использование _ для обозначения экзистенциального типа является адекватным. Обратите внимание, однако, что в моем собственном коде (первая версия) я не могу использовать его, потому что типы не будут совпадать, если я это сделаю.

Следующий,

произвольно глубокие многомерные массивы

Это не очень хорошая идея. Вы должны знать, какой тип вы вернете, чтобы объявить его — если глубина «произвольна», то и тип будет таким же. Вы можете обойтись без Array[AnyRef], но пользоваться им будет больно.

Тем не менее, вот один из способов. Я избавился от всех петель, заменив их картой. Обратите внимание, где я использую тот факт, что Map также является Function1, вызывая map m. Также обратите внимание, что я использую map только для массива (созданного с помощью toArray), чтобы избежать необходимости управлять созданием карты.

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[Int, AnyRef] => deMap(map)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  m.keys.toArray.sorted map m map translate
}

Есть две проблемы. Начнем с того, что если ключи не являются смежными (например, Map(0 -> "a", 2 -> "b")), элементы будут не на своем месте. Вот способ обойти это, заменив предпоследнюю строку кода на:

  Array.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate

Здесь я использовал пустую строку для обозначения любой дыры в массивах.

Другая проблема заключается в том, что я предполагаю, что все карты будут иметь тип Map[Int, AnyRef]. Чтобы исправить это, можно было бы переписать так:

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  Array.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate
}

Здесь я отбрасываю все пары, чьи ключи не Int, а значения не AnyRef. Можно дополнительно безопасно проверить код, чтобы проверить, не пропало ли что-то, и сообщить об ошибке в этом случае:

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      if (map.size > validMap.size) {
        val wrongPairs = map.toSeq diff validMap.toSeq
        throw new IllegalArgumentException("Invalid key/value pairs found: "+wrongPairs)
      }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  Array.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate
}

Наконец, о производительности. Использование map так, как это сделал я, означает, что для каждого map будет выделен новый массив. Некоторые люди считают, что это означает, что мне нужно выполнить три итерации вместо одной, но поскольку каждый раз я выполняю другую задачу, на самом деле это не намного больше работы. Однако тот факт, что я каждый раз выделяю новый массив, определенно влияет на производительность — как из-за штрафа за выделение (Java предварительно инициализирует все массивы), так и из-за проблем с локальностью кэша.

Один из способов избежать этого — использовать view. Когда вы выполняете map, flatMap и filter поверх view, вы получаете новое представление, а эта операция сохраняется для будущего использования (другие методы тоже могут работать таким же образом — проверьте документацию или проверьте, когда в сомнениях). Когда вы, наконец, сделаете apply для объекта view, он применит все операции, необходимые для получения определенного элемента, о котором вы просили. Это будет происходить каждый раз, когда вы apply будете использовать этот элемент, так что производительность может быть как лучше, так и хуже.

Здесь я начну с представления Range, чтобы избежать выделения массива, а затем преобразую представление в Array в конце. Тем не менее, keys создаст набор, что приведет к некоторым накладным расходам. После этого я объясню, как этого избежать.

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      if (map.size > validMap.size) {
        val wrongPairs = map.toSeq diff validMap.toSeq
        throw new IllegalArgumentException("Invalid key/value pairs found: "+wrongPairs)
      }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  (0 to m.keys.max view) map (m getOrElse (_, "")) map translate toArray
}

Однако вы должны оставить view для необходимых оптимизаций, вместо того, чтобы активно использовать их. Это не обязательно быстрее, чем обычные коллекции, и его нестрогость может быть сложной задачей. Другой альтернативой использованию view является использование вместо него Stream. Stream очень похож на List, за исключением того, что он вычисляет свои элементы только по запросу. Это означает, что ни одна из map операций не будет применяться до тех пор, пока в этом нет необходимости. Чтобы использовать его, просто замените предпоследнюю строку на эту:

  Stream.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate toArray

Основное преимущество Stream перед view заключается в том, что после того, как значение в Stream было вычислено, оно остается вычисленным. Это также его главный недостаток по сравнению с view, как это ни парадоксально. В данном конкретном случае я думаю, что view быстрее.

Наконец, о выполнении max без предварительного вычисления Set (результата keys). Map также является Iterable, и все Iterable имеют метод max, так что вы можете просто выполнить m.max. Это, однако, вычислит максимум Tuple2[Int, AnyRef], типа, для которого нет Ordering. Однако max получает неявный параметр, указывающий, какой Ordering использовать, что может быть предоставлено таким образом:

m.max(Ordering by ((_: (Int, AnyRef))._1))

Это немного многословно, так что мы можем определить нужный нам имплицит и использовать его, эээ, неявно. :-)

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  implicit val myOrdering = Ordering by ((_: (Int, AnyRef))._1)
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      if (map.size > validMap.size) {
        val wrongPairs = map.toSeq diff validMap.toSeq
        throw new IllegalArgumentException("Invalid key/value pairs found: "+wrongPairs)
      }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  (0 to m.max._1 view) map (m getOrElse (_, "")) map translate toArray
}

Обратите внимание, что max возвращает кортеж, key и value, поэтому нам нужно _1, чтобы получить key. Также обратите внимание, что мы создаем объект Ordering при каждом вложении deMap. Неплохо, но его можно было бы улучшить, определив его в другом месте.

Как только вы все это сделаете, цикл while все равно будет работать быстрее, хотя я понятия не имею, насколько. Учитывая достаточное количество оптимизаций JVM, они могут подойти довольно близко. С другой стороны, если вам действительно нужна скорость, вы можете полностью перейти на ассемблерный код. Все сводится к балансу между тем, насколько легко и быстро писать код, насколько легко его поддерживать и насколько быстро он работает.

person Daniel C. Sobral    schedule 05.12.2010
comment
Спасибо! Интересный момент насчет стираний, и ваш код намного элегантнее моего. Как карта toArray + соотносится по скорости с циклами while? Кроме того, вы сказали: вы должны знать, какой тип вы вернете, чтобы объявить его. Я надеялся, что, используя типы более высокого порядка (которые мне сейчас кажутся магией), я смогу избежать создания новой функции deMap для каждой глубины; но, увидев методы ofDim в объекте-компаньоне для Array в scala API, я убедился в обратном. - person dsg; 05.12.2010
comment
Вау, это фантастический ответ! Я многому научился, большое спасибо. - person dsg; 06.12.2010