Почему Set # map возвращает массив, а не другой набор? - PullRequest
0 голосов
/ 24 марта 2020

Просто столкнулся с TypeError: no implicit conversion of Set into Array, выполняя что-то в форме

thingGetterSet.map{|m| m.getThing }.select{|t| t.appropriate? } + appropriateThingsSet

Оказывается, Set#map возвращает массив, где я ожидал, что набор будет отображаться в другой набор.

Даже если он вернул набор, Set#select также возвращает массив, где я также ожидал набор.

Что здесь происходит? Почему Ruby налагает случайный порядок и разрешает дублирование на мои вещи, которые канонически неупорядочены, и где я хочу запретить дублирование?

Ответы [ 2 ]

4 голосов
/ 24 марта 2020

В общем, есть два разных стиля библиотек операций коллекции:

  • сохранение типов : это то, что вы ожидаете в своем вопросе
  • generi c (не в «параметрическом c смысле полиморфизма», а в стандартном английском языке), или, возможно, «однородный»

Тип- сохраняя операции сбора, попробуйте сохранить тип точно для таких операций, как select, take, drop, et c. которые только принимают существующие элементы без изменений. Для таких операций, как map, он пытается найти ближайший супертип, который все еще может содержать результат. Например, отображение IntSet в String может явно не привести к IntSet, а только к Set. Отображение IntSet в Boolean может быть представлено в BitSet, но я не знаю ни одной структуры коллекций, которая была бы настолько умна, чтобы на самом деле сделать это.

Generi c / операции однородного сбора всегда возвращают того же типа. Обычно этот тип выбирается как очень общий, чтобы охватить самый широкий диапазон вариантов использования. Например, In. NET, операции сбора возвращают IEnumerable, в Java они возвращают Stream с, в C ++ они возвращают итераторы.

До недавнего времени реализовывать операции по сохранению типов можно было только путем дублирования всех операций для всех типов. Например, инфраструктура коллекций Smalltalk сохраняет тип и делает это за счет того, что каждый отдельный класс коллекций повторно реализует каждую отдельную операцию коллекций. Это приводит к большому количеству дублированного кода и является кошмаром обслуживания. (Не случайно, что многие новые объектно-ориентированные абстракции, которые были изобретены, написали свою первую статью о том, как ее можно применить к структуре коллекций Smalltalk. См. Черты: составные единицы поведения для примера.)

Насколько мне известно, Scala 2.8 перепроектирование структуры коллекций ( см. также этот ответ по SO ) было В первый раз кому-то удалось создать операции с сохранением типов и минимизировать (хотя и не исключить) дублирование. Однако структура коллекций Scala 2.8 была подвергнута широкой критике как чрезмерно сложная, и в течение последнего десятилетия она требовала постоянной работы. Фактически, это фактически привело к полной перестройке системы документации Scala, чтобы иметь возможность скрыть очень сложные сигнатуры типов, которые требуются для операций сохранения типов . Но, этого все еще не было достаточно , поэтому структура коллекций была полностью отброшена и перепроектирована еще раз в Scala 2.13 . (И эта переработка заняла несколько лет.)

Таким образом, ответ на вопрос «почему не сохраняется сохранение структуры типа коллекции Ruby» на самом деле довольно прост: потому что Ruby был создан в 1993, и мы (под этим я подразумеваю сообщество программистов в целом) не понимали, как правильно сделать это, до 2019 года, 26 лет спустя.

Также отметим, что реализация Scala сильно зависит от stati c печатать Не только при использовании stati c, но и при программировании на уровне типов во время компиляции, самоанализе на уровне типов во время компиляции и метапрограммировании на уровне типов во время компиляции. Они не существенные , но они do означают, что вы не можете просто скопировать их решение в Ruby. Например, Scala будет использовать классы типов и неявный поиск , чтобы выяснить, что наилучшее из возможных совпадений для

IntSet(1, 2, 3).map(_.toString)
//=> val res: Set[String] = Set("1", "2", "3")

- это Set[String] во время компиляции . В Ruby вы, очевидно, могли бы по-прежнему запускать тот же алгоритм поиска, хотя он будет намного медленнее , потому что вам нужно запускать его во время выполнения, снова и снова и снова при каждом запуске map. Это будет медленнее, но это будет возможно: это всего лишь алгоритм, если вы можете запустить его во время компиляции, то вы можете запустить его и во время выполнения. НО! Алгоритм нуждается в возвращаемом типе блока в качестве одного из аргументов! В Scala это выводится во время компиляции. Откуда вы знаете, что в Ruby?

Но даже в Scala иногда невозможно найти хорошее совпадение, например, здесь:

val m = Map(1 → "one", 2 → "two", 3 → "three")

m.map { case (k, v) ⇒ s"$k $v" }
//=> val res: Iterable[String] = List("1 one", "2 two", "3 three")

Итак, лучший можно найти * stati c type Scala, равный Iterable, который на самом деле является самой верхней частью иерархии коллекций Scala, и наилучший возможный тип времени выполнения, который может найти Scala, это List, что на самом деле Тип коллекции "go -to" в Scala, аналогично Array в Ruby. Другими словами, это на самом деле Scala говорит: «Я сдаюсь, чувак».

Есть и другая складка, в которой операции по сохранению типов ставят под сомнение то, что мы считаем частью контракта определенного операции. Например, большинство людей утверждают, что количество элементов коллекции должно быть инвариантным относительно map, другими словами, map должно отображать каждый элемент ровно на один новый элемент, и, таким образом, map никогда не должен изменять размер коллекции , Но как насчет этого гипотетического кода с каркасом с сохранением типов Ruby:

Set[1, 2, 3].map(&:odd?)
#=> Set[true, false]

Есть и другие интересные случаи, когда я даже не знаю, какой тип возвращаемого значения должен быть в фреймворк коллекций с сохранением типов, например, что касается потоков Range s или IO:

(1..1000).map(&:odd?)
(1..1000).select(&:odd?)

File.open('bla').map(&:upcase)

Из-за всего этого разработчики Ruby выбрали однородный операций сбора в Ruby: каждые операции сбора всегда возвращает Array.

Хорошо. Ладно. За исключением иногда они делают решили переопределить их. Например, Hash, операции фильтрации select, reject, et c. на самом деле сделать вернуть Hash. Но обратите внимание, что это недавнее изменение, и на самом деле имеет интересную историю:

  • В Ruby 1,8 (и, возможно, раньше), Hash#select вернул Array, но Hash#reject вернул Hash!
  • В Ruby 1,9 это было изменено, так что оба возвращают Hash.
  • Однако, find_all, который определен в Enumerable как псевдоним select, по-прежнему и по сей день не переопределяется в Hash, и, таким образом, возвращает Array!
  • С другой стороны, недавно представленный filter, который также определен как псевдоним select в Enumerable, равен переопределено в Hash для возврата Hash.

Итак, конструкторы Ruby выбрали простоту (без дублирования кода, без вычисления сложного типа во время выполнения, все операции всегда возвращают массивы, поэтому есть никаких сюрпризов, как в примере выше, где map меняет размер набора и т. д. c.) на корректность и делает операции сбора Ruby однородными вместо o Сохранение типа f. Но затем они также предпочли прагматизм чистоте и посыпали здесь и там небольшое количество переопределений с сохранением типов.

Таким образом, тот факт, что Set#map возвращает Array, не должен вызывать удивления, потому что это любой другой класс в структуре коллекций также делает это. Изменение этого только для Set#map не очень хорошая идея, ИМО. Если мы сделаем это, это должно быть сделано для всех разработчиков map. Но это серьезное изменение, и поэтому придется подождать до Ruby 3 в ближайшее время. (На самом деле, Матц сказал, что он хочет избежать критических изменений в Ruby 3.) Но даже изменение only map для всех разработчиков странно, , если мы делаем это, это должно быть сделано для всех операций. Это важная исследовательская задача, и поэтому слишком поздно для Ruby 3, поэтому придется подождать, по крайней мере, Ruby 4.

О чем мы можем спорить однако, является ли Array правильным выбором для универсального типа коллекции. Вы можете заметить, что другие подобные структуры выбирают очень общий тип:. NET имеет IEnumerable, Java имеет Stream, C ++ имеет итераторы. Эквивалент в Ruby будет Enumerator. Возможно, Enumerator должен быть типом, который возвращается всеми операциями коллекций. Например, если вы map на бесконечном множестве, результат снова будет бесконечным, но это будет Array, что означает, что ему нужно бесконечное количество памяти!

Это возвращает нас к Прагматизм, однако: в большинстве случаев использование Array более полезно, чем Enumerator.

1 голос
/ 24 марта 2020

Как сказано в одном из комментариев, проблема в том, что Set включает Enumerable в качестве поставщика множества методов, включая map. Это решение по языку, поэтому лучший ответ, который я могу дать, - указать вам на список рассылки разработчика. Это интересная тема нескольких лет: go: " Hash # выберите тип возвращаемого значения не соответствует Hash # find_all ", в частности " [Ruby trunk Bug # 13795] Hash # select тип возвращаемого значения не совпадает с Hash # find_all".

Я видел несколько причин в потоке не повторного внедрения map и других в Set:

  1. Это противоречит преимуществам использования Enumerable общего назначения, что приводит к дополнительной работе по реализации для классов, которые включают этот модуль, поскольку им придется повторно реализовывать группу методов (хотя это не было главной причиной).
  2. Существуют варианты использования map, когда неясно, является ли хорошей идеей сохранение исходной структуры данных. Например, с :odd? в комментариях, также есть пример в ветке списка рассылки

    {}.map {|k, v| "#{k}-#{v}"}
    

    , который не будет работать, возвращая Hash.

  3. Там другие классы стандартной библиотеки, такие как Range и IO, где сохранение их типа post-map не имеет смысла, поэтому для поведения по умолчанию более логично возвращать массив.
...