Что такое идиоматический способ Scala «удалить» один элемент из неизменяемого списка?

86

У меня есть список, который может содержать элементы, которые будут сравниваться как равные. Мне нужен аналогичный список, но с удалением одного элемента. Итак, из (A, B, C, B, D) я хотел бы иметь возможность «удалить» только один B, чтобы получить, например, (A, C, B, D). Порядок элементов в результате не имеет значения.

У меня есть рабочий код, написанный на Scala в стиле Lisp. Есть ли более идиоматический способ сделать это?

Контекст - это карточная игра, в которой участвуют две колоды стандартных карт, поэтому могут быть дубликаты карт, но все равно играть по одной.

def removeOne(c: Card, left: List[Card], right: List[Card]): List[Card] = {
  if (Nil == right) {
    return left
  }
  if (c == right.head) {
    return left ::: right.tail
  }
  return removeOne(c, right.head :: left, right.tail)
}

def removeCard(c: Card, cards: List[Card]): List[Card] = {
  return removeOne(c, Nil, cards)
}
Гавилан Комун
источник
Добавлено примечание, что порядок списка результатов не имеет значения в данном конкретном случае.
Gavilan Comun
Итак, List[Card]в этом вопросе рука игрока?
Кен Блум
@ Кен Блум, да, это рука игрока.
Gavilan Comun
Вы знаете, я какое-то время искал подобный вопрос, затем опубликовал тот же вопрос, а затем нашел этот, пока просматривал и ждал, пока люди ответят на мой. Думаю, я должен проголосовать, чтобы закрыть свой вопрос как дубликат. ;-)
Джо Карнахан
Этот вопрос для Clojure: stackoverflow.com/questions/7662447/…
Gavilan Comun

Ответы:

147

Я не видел такой возможности в ответах выше, поэтому:

scala> def remove(num: Int, list: List[Int]) = list diff List(num)
remove: (num: Int,list: List[Int])List[Int]

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

Редактировать:

scala> remove(2,List(2,2,2))
res0: List[Int] = List(2, 2)

Как колдовство :-).

Антонин Бреттснайдр
источник
18
Ницца! Я бы добавил в список еще 2, чтобы было ясно, что удаляется только один элемент.
Фрэнк С. Томас
40

Вы можете использовать этот filterNotметод.

val data = "test"
list = List("this", "is", "a", "test")
list.filterNot(elm => elm == data)
Сорен Матиасен
источник
22
Это удалит все элементы, которые равны "test" - а не то, о чем просят;)
yǝsʞǝla
1
На самом деле он сделает именно то, что вам нужно. Он вернет все элементы из списка, кроме тех, которые не равны "test". Обратите внимание, что он использует filterNot
btbvoy
14
Первоначальный вопрос заключался в том, как удалить ОДИН экземпляр. Не все экземпляры.
ty1824 02
@ Søren Mathiasen, если я хочу отфильтровать несколько элементов, например последовательность, например val data = Seq ("test", "a"), как это сделать?
BdEngineer
18

Вы можете попробовать это:

scala> val (left,right) = List(1,2,3,2,4).span(_ != 2)
left: List[Int] = List(1)
right: List[Int] = List(2, 3, 2, 4)

scala> left ::: right.tail                            
res7: List[Int] = List(1, 3, 2, 4)

И как метод:

def removeInt(i: Int, li: List[Int]) = {
   val (left, right) = li.span(_ != i)
   left ::: right.drop(1)
}
Фрэнк С. Томас
источник
3
Стоит отметить, что left ::: right.drop(1)он короче, чем оператор if с isEmpty.
Рекс Керр,
2
Спасибо, есть ли какие-либо обстоятельства, по которым можно предпочесть .drop (1) над .tail или наоборот?
Gavilan Comun
8
@James Petry - Если вы звоните tailна пустой список вы получите исключение: scala> List().tail java.lang.UnsupportedOperationException: tail of empty list. drop(1)однако в пустом списке возвращает пустой список.
Фрэнк С. Томас,
3
tailвыдает исключение, если список пуст (т.е. его нет head). drop(1)в пустом списке просто дает еще один пустой список.
Рекс Керр
8

К сожалению, иерархия коллекций немного запуталась из-за -on List. Потому что ArrayBufferэто работает так, как вы могли бы надеяться:

scala> collection.mutable.ArrayBuffer(1,2,3,2,4) - 2
res0: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 3, 2, 4)

но, к сожалению, Listзакончился filterNotреализацией -стилем и, таким образом, делает "неправильную вещь" и выдает вам предупреждение об устаревании (достаточно разумно, поскольку это действительно так filterNot):

scala> List(1,2,3,2,4) - 2                          
warning: there were deprecation warnings; re-run with -deprecation for details
res1: List[Int] = List(1, 3, 4)

Так что, возможно, проще всего преобразовать Listв коллекцию, которая делает это правильно, а затем снова преобразовать:

import collection.mutable.ArrayBuffer._
scala> ((ArrayBuffer() ++ List(1,2,3,2,4)) - 2).toList
res2: List[Int] = List(1, 3, 2, 4)

Как вариант, вы можете сохранить логику кода, который у вас есть, но сделать стиль более идиоматическим:

def removeInt(i: Int, li: List[Int]) = {
  def removeOne(i: Int, left: List[Int], right: List[Int]): List[Int] = right match {
    case r :: rest =>
      if (r == i) left.reverse ::: rest
      else removeOne(i, r :: left, rest)
    case Nil => left.reverse
  }
  removeOne(i, Nil, li)
}

scala> removeInt(2, List(1,2,3,2,4))
res3: List[Int] = List(1, 3, 2, 4)
Рекс Керр
источник
removeInt(5,List(1,2,6,4,5,3,6,4,6,5,1))урожайность List(4, 6, 2, 1, 3, 6, 4, 6, 5, 1). Я думаю, это не то, что вы хотели.
Кен Блум,
@ Кен Блум - Верно. Это ошибка исходного алгоритма, который я скопировал, не задумываясь. Исправлено сейчас.
Рекс Керр,
Скорее упущение в спецификации вопроса, так как порядок не имеет значения в моем конкретном случае. Приятно видеть версию с сохранением порядка, спасибо.
Gavilan Comun
@Rex: что ты имеешь в виду под "filterNot"? Что удаляет все вхождения? И почему он выдает предупреждение об устаревании? Спасибо
teo
1
@teo - удаляет все вхождения (что здесь нежелательно), и он устарел, потому что он, возможно, не работает (или, возможно, желаемое поведение неясно - в любом случае, оно устарело в 2.9 и исчезло в 2.10).
Рекс Керр
5
 def removeAtIdx[T](idx: Int, listToRemoveFrom: List[T]): List[T] = {
    assert(listToRemoveFrom.length > idx && idx >= 0)
    val (left, _ :: right) = listToRemoveFrom.splitAt(idx)
    left ++ right
 }
Суат КАРАКУСОГЛУ
источник
2

Как насчет

def removeCard(c: Card, cards: List[Card]) = {
  val (head, tail) = cards span {c!=}   
  head ::: 
  (tail match {
    case x :: xs => xs
    case Nil => Nil
  })
}

Если видите return, что-то не так.

Юджин Йокота
источник
1
Это не делает то , что он хочет, что удалить только первый экземплярc
Кен Блум
1
Это удалит все карты c, но только сначала следует удалить.
tenshi
Я должен прочитать вопросы внимательнее! поправил свой ответ.
Юджин Йокота,
+1 за «Если вы видите возврат, значит что-то не так». Это сам по себе очень важный урок "идиоматической Scala".
Джо Карнахан
2
// throws a MatchError exception if i isn't found in li
def remove[A](i:A, li:List[A]) = {
   val (head,_::tail) = li.span(i != _)
   head ::: tail
}
Кен Блум
источник
1

В качестве одного из возможных решений вы можете найти индекс первого подходящего элемента, а затем удалить элемент по этому индексу:

def removeOne(l: List[Card], c: Card) = l indexOf c match {
    case -1 => l
    case n => (l take n) ++ (l drop (n + 1))
}
Tenhi
источник
См. Мой ответ, использующий spanто же самое.
Кен Блум,
0

Еще одна мысль о том, как это сделать с помощью складки:

def remove[A](item : A, lst : List[A]) : List[A] = {
    lst.:\[List[A]](Nil)((lst, lstItem) => 
       if (lstItem == item) lst else lstItem::lst )
}
гдиз
источник
0

Общее решение для рекурсии хвоста:

def removeElement[T](list: List[T], ele: T): List[T] = {
    @tailrec
    def removeElementHelper(list: List[T],
                            accumList: List[T] = List[T]()): List[T] = {
      if (list.length == 1) {
        if (list.head == ele) accumList.reverse
        else accumList.reverse ::: list
      } else {
        list match {
          case head :: tail if (head != ele) =>
            removeElementHelper(tail, head :: accumList)
          case head :: tail if (head == ele) => (accumList.reverse ::: tail)
          case _                             => accumList
        }
      }
    }
    removeElementHelper(list)
  }
Шанкар Шастри
источник
-3
val list : Array[Int] = Array(6, 5, 3, 1, 8, 7, 2)
val test2 = list.splitAt(list.length / 2)._2
val res = test2.patch(1, Nil, 1)
Гекльберри Финн
источник
-4
object HelloWorld {

    def main(args: Array[String]) {

        var months: List[String] = List("December","November","October","September","August", "July","June","May","April","March","February","January")

        println("Deleting the reverse list one by one")

        var i = 0

        while (i < (months.length)){

            println("Deleting "+months.apply(i))

            months = (months.drop(1))

        }

        println(months)

    }

}
Анбинсон
источник
Не могли бы вы добавить некоторые пояснения (комментарии, описание), как это отвечает на вопрос?
rjp
4
1. На этот вопрос был задан и дан ответ 5 лет назад. 2. OP запросил "идиоматическую" Scala. Использование 2 varс и whileцикла - это не идиоматика Scala.
jwvh