Зв'язування списку Scala, ::: vs ++


362

Чи є якась різниця між спірними списками :::та ++для їх об'єднання?

scala> List(1,2,3) ++ List(4,5)
res0: List[Int] = List(1, 2, 3, 4, 5)

scala> List(1,2,3) ::: List(4,5)
res1: List[Int] = List(1, 2, 3, 4, 5)

scala> res0 == res1
res2: Boolean = true

З документації це схоже на ++більш загальне, тоді як :::є Listспецифічним. Чи надається остання, оскільки вона використовується в інших функціональних мовах?


4
Також :::є оператор префікса, як і всі методи, починаючи з:
Бен Джексон

3
Відповіді в значній мірі окреслюють те, як масштаб еволюціонував навколо списків та однаковості операторів у Scala (або відсутність останнього). Трохи прикро, що щось таке просте має такий довгий хвіст дрібниць, щоб плутати і витрачати час будь-якого учня Scala. Я б хотів, щоб він нівелювався в 2.12.
matanster

Відповіді:


321

Спадщина. Список спочатку був визначений як функціональний на вигляд мов:

1 :: 2 :: Nil // a list
list1 ::: list2  // concatenation of two lists

list match {
  case head :: tail => "non-empty"
  case Nil          => "empty"
}

Звичайно, Scala розробляла інші колекції, спеціально. Коли вийшло 2.8, колекції були перероблені для максимального повторного використання коду та послідовного API, так що ви можете використовувати ++для об'єднання будь-яких двох колекцій - і навіть ітераторів. Список, однак, повинен утримувати своїх оригінальних операторів, окрім одного або двох, які застаріли.


19
Тож чи найкраще практику уникати :::на користь ++зараз? Також використовувати +:замість ::?
Луїджі Плінге

37
::корисна через відповідність шаблону (див. другий приклад Даниїла). Ви не можете цього зробити+:
парадигматичний

1
@Luigi Якщо ви використовуєте Listзамість Seq, ви можете також використовувати ідіоматичні Listметоди. З іншого боку, це буде важче перейти на інший тип, якщо ви коли-небудь захочете це зробити.
Даніель К. Собрал

2
Мені здається, що в одному є ідіоматичні операції «Список» (як ::і :::), і більш загальна операція, спільна для інших колекцій. Я б не скинув жодної операції з мови.
Джорджо

21
@paradigmatic Scala 2.10 має :+та +:витяжки об'єктів.
0__

97

Завжди використовуйте :::. Є дві причини: ефективність та безпека типу.

Ефективність

x ::: y ::: zшвидше, ніж x ++ y ++ z, тому що :::є правильним асоціативним. x ::: y ::: zаналізується як x ::: (y ::: z), що алгоритмічно швидше, ніж (x ::: y) ::: z(для останнього потрібно O (| x |) більше кроків).

Тип безпеки

З :::вами можна об'єднати лише два Listс. З ++ви можете додати будь-яку колекцію до List, яка страшно:

scala> List(1, 2, 3) ++ "ab"
res0: List[AnyVal] = List(1, 2, 3, a, b)

++також легко змішується з +:

scala> List(1, 2, 3) + "ab"
res1: String = List(1, 2, 3)ab

9
При об'єднанні всього двох списків різниці немає, але у випадку 3 і більше у вас є хороший момент, і я підтвердив це швидким орієнтиром. Однак, якщо ви переживаєте за ефективність, її x ::: y ::: zслід замінити на List(x, y, z).flatten. pastebin.com/gkx7Hpad
Луїджі Плінг

3
Поясніть, будь ласка, чому для лівого асоціативного конкатенації потрібно більше кроків O (x). Я подумав, що вони обидва працюють на O (1).
pacman

6
Списки @pacman пов’язані поодиноко, щоб додати один список до іншого, вам потрібно зробити копію першого списку, у якому в кінці додається другий. Отже, конкатенація є O (n) щодо кількості елементів у першому списку. Довжина другого списку не впливає на час виконання, тому краще додати довгий список до короткого, а не додавати короткий список до довгого.
puhlen

1
Списки @pacman Scala незмінні . Ось чому ми не можемо просто замінити останню посилання під час конкатенації. Ми повинні створити новий список з нуля.
ЖекаКозлов

4
@pacman Складність завжди є лінійною wrt довжиною xі y( zніколи не повторюється, тому ні в якому разі не впливає на час виконання, тому краще додати довгий список до короткого, ніж навпаки), але асимптотична складність не розповідає всієї історії. x ::: (y ::: z)ітераціює yі додає z, потім повторює xі додає результат y ::: z. xі yобидва повторені один раз. (x ::: y) ::: zітераціює xі додає y, потім ітератує результат x ::: yі додає z. yвсе ще повторюється один раз, але xв цьому випадку повторюється двічі.
puhlen

84

:::працює лише зі списками, хоча ++може використовуватися з будь-яким прохідним. У поточній реалізації (2.9.0) ++відновлюється, :::якщо аргументом є також a List.


4
Тому дуже просто використовувати і :::, і ++, працюючи зі списком. Це потенційно може ввести хаос у код / ​​стиль.
ses

24

Інший момент полягає в тому, що перше речення аналізується як:

scala> List(1,2,3).++(List(4,5))
res0: List[Int] = List(1, 2, 3, 4, 5)

Тоді як другий приклад аналізується як:

scala> List(4,5).:::(List(1,2,3))
res1: List[Int] = List(1, 2, 3, 4, 5)

Тож якщо ви використовуєте макроси, вам слід подбати.

Крім того, ++для двох списків дзвінки, :::але з більшою накладною витратою, тому що він просить неявного значення мати будівельника зі списку в список. Але мікроблоки не виявили нічого корисного в цьому сенсі, я думаю, що компілятор оптимізує такі дзвінки.

Мікро-орієнтири після прогрівання.

scala>def time(a: => Unit): Long = { val t = System.currentTimeMillis; a; System.currentTimeMillis - t}
scala>def average(a: () => Long) = (for(i<-1 to 100) yield a()).sum/100

scala>average (() => time { (List[Int]() /: (1 to 1000)) { case (l, e) => l ++ List(e) } })
res1: Long = 46
scala>average (() => time { (List[Int]() /: (1 to 1000)) { case (l, e) => l ::: List(e ) } })
res2: Long = 46

Як сказав Даніель С. Собрай, ви можете додати вміст будь-якої колекції до списку, використовуючи ++, тоді як з ними :::ви можете лише об'єднати списки.


20
Будь ласка, опублікуйте свої не надто спрощені мікро-показники, і я проголосую за них.
Mikaël Mayer
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.