Коллекции Scala - умные вещи ...
Внутренняя часть библиотеки коллекции - одна из наиболее продвинутых тем в земле Скала. Он включает в себя типы с более высоким родом, логический вывод, дисперсию, импликации и механизм CanBuildFrom
- все это делает его невероятно универсальным, простым в использовании и мощным с точки зрения пользователя. Понимание этого с точки зрения дизайнера API - не легкая задача для начинающего.
С другой стороны, невероятно редко когда-либо вам действительно нужно работать с коллекциями на такой глубине.
Итак, давайте начнем ...
С выпуском Scala 2.8 библиотека коллекций была полностью переписана для устранения дублирования, огромное количество методов было перенесено в одно место, так что текущее обслуживание и добавление новых методов сбора было бы намного проще, но это также делает сложнее понять иерархию.
Например, List
, это наследуется от (в свою очередь)
LinearSeqOptimised
GenericTraversableTemplate
LinearSeq
Seq
SeqLike
Iterable
IterableLike
Traversable
TraversableLike
TraversableOnce
Это довольно горстка! Так почему эта глубокая иерархия? Вкратце игнорируя черты XxxLike
, каждый уровень в этой иерархии добавляет немного функциональности или предоставляет более оптимизированную версию унаследованной функциональности (например, для извлечения элемента по индексу в Traversable
требуется комбинация drop
и head
операций, крайне неэффективных в индексированной последовательности). Там, где это возможно, вся функциональность продвигается как можно дальше вверх по иерархии, максимизируя количество подклассов, которые могут ее использовать, и удаляя дублирование.
map
- только один такой пример. Этот метод реализован в TraversableLike
(хотя черты XxxLike
действительно существуют только для разработчиков библиотек, поэтому обычно он считается методом Traversable
для большинства намерений и целей - я скоро вернусь к этой части), и широко наследуется. Можно определить оптимизированную версию в некотором подклассе, но она все равно должна соответствовать той же сигнатуре. Рассмотрим следующие варианты использования map
(как также упоминалось в вопросе):
"abcde" map {_.toUpperCase} //returns a String
"abcde" map {_.toInt} // returns an IndexedSeq[Int]
BitSet(1,2,3,4) map {2*} // returns a BitSet
BitSet(1,2,3,4) map {_.toString} // returns a Set[String]
В каждом случае выход имеет тот же тип, что и вход, где это возможно. Когда это невозможно, суперклассы типа ввода проверяются до тех пор, пока не будет обнаружено, что действительно предлагает допустимый тип возвращаемого значения. Чтобы сделать это правильно, потребовалось много работы, особенно если учесть, что String
- это даже не коллекция, а просто неявно конвертируемая в единицу.
Так как это сделать?
Половина головоломки - это черты XxxLike
(я сделал сказал, что доберусь до них ...), чья основная функция - взять параметр типа Repr
(сокращение от «Представление»), чтобы они знали истинный подкласс, на котором фактически ведутся операции. Так, например TraversableLike
совпадает с Traversable
, но абстрагируется от параметра типа Repr
. Этот параметр затем используется второй половиной головоломки; класс типа CanBuildFrom
, который фиксирует тип коллекции источника, тип целевого элемента и тип коллекции назначения, которые будут использоваться операциями преобразования коллекции.
Это проще объяснить на примере!
BitSet определяет неявный экземпляр CanBuildFrom
следующим образом:
implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = bitsetCanBuildFrom
При компиляции BitSet(1,2,3,4) map {2*}
компилятор попытается выполнить неявный поиск CanBuildFrom[BitSet, Int, T]
Это умная часть ... Есть только один неявный в области видимости, который соответствует первым двум параметрам типа. Первый параметр - Repr
, зафиксированный признаком XxxLike
, а второй - тип элемента, зафиксированный текущей характеристикой сбора (например, Traversable
). Операция map
затем также параметризируется с типом, этот тип T
выводится на основе параметра третьего типа для экземпляра CanBuildFrom
, который был неявно расположен. BitSet
в этом случае.
Таким образом, первые два параметра типа CanBuildFrom
являются входными данными, которые будут использоваться для неявного поиска, а третий параметр является выходными данными, которые будут использоваться для логического вывода.
CanBuildFrom
в BitSet
поэтому совпадает с двумя типами BitSet
и Int
, поэтому поиск будет успешным, и предполагаемый тип возврата также будет BitSet
.
При компиляции BitSet(1,2,3,4) map {_.toString}
компилятор попытается выполнить неявный поискCanBuildFrom[BitSet, String, T]
.Это не удастся для неявного в BitSet, поэтому компилятор в следующий раз попробует свой суперкласс - Set
- Это содержит неявное:
implicit def canBuildFrom[A]: CanBuildFrom[Coll, A, Set[A]] = setCanBuildFrom[A]
Что соответствует, потому что Coll является псевдонимом типа, который инициализирован как BitSet
когда BitSet
происходит от Set
.A
будет соответствовать чему угодно, так как canBuildFrom
параметризован с типом A
, в этом случае он выводится как String
... Таким образом, получается тип возврата Set[String]
.
Таким образом, для правильной реализации типа коллекции вам нужно не только предоставить корректный неявный тип CanBuildFrom
, но и убедиться, что конкретный тип этого набора предоставлен в качестве параметра Repr
для правильного родителя.traits (например, это было бы MapLike
в случае подкласса Map
).
String
немного сложнее, поскольку обеспечивает map
неявным преобразованием.Неявное преобразование - в StringOps
, то есть подклассы StringLike[String]
, который в конечном итоге выводит TraversableLike[Char,String]
- String
, являющийся параметром типа Repr
.
Существует также область действия CanBuildFrom[String,Char,String]
, так чтоКомпилятор знает, что при отображении элементов из String
в Char
s тип возвращаемого значения также должен быть строкой.С этого момента используется тот же механизм.