В общем, есть два разных стиля библиотек операций коллекции:
- сохранение типов : это то, что вы ожидаете в своем вопросе
- 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
.