Qual é a maneira idiomática do Scala de “remover” um elemento de uma lista imutável?

86

Eu tenho uma lista, que pode conter elementos que serão comparados como iguais. Eu gostaria de uma lista semelhante, mas com um elemento removido. Portanto, de (A, B, C, B, D), gostaria de poder "remover" apenas um B para obter, por exemplo, (A, C, B, D). A ordem dos elementos no resultado não importa.

Eu tenho código de trabalho, escrito de forma inspirada em Lisp em Scala. Existe uma maneira mais idiomática de fazer isso?

O contexto é um jogo de cartas em que dois baralhos de cartas padrão estão em jogo, então pode haver cartas duplicadas, mas ainda jogadas uma de cada vez.

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
fonte
Adicionada uma observação de que a ordem da Lista de resultados não importa neste caso específico.
Gavilan Comun
então o List[Card]nesta questão é a mão de um jogador?
Ken Bloom,
@Ken Bloom, sim, é a mão de um jogador.
Gavilan Comun
Sabe, eu procurei por uma pergunta como esta por um tempo, depois postei a mesma pergunta e encontrei esta enquanto eu estava navegando e esperando que as pessoas respondessem a minha. Acho que devo votar para fechar minha própria pergunta agora como uma duplicata. ;-)
Joe Carnahan,
Esta pergunta para Clojure: stackoverflow.com/questions/7662447/…
Gavilan Comun

Respostas:

147

Não vi essa possibilidade nas respostas acima, então:

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)

Editar:

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

Como um encanto :-).

Antonin Brettsnajdr
fonte
18
Agradável! Eu adicionaria mais 2 à lista para deixar claro que apenas um elemento é removido.
Frank S. Thomas,
40

Você pode usar o filterNotmétodo.

val data = "test"
list = List("this", "is", "a", "test")
list.filterNot(elm => elm == data)
Søren Mathiasen
fonte
22
Isso removerá todos os elementos que são iguais a "teste" - não o que é solicitado;)
yǝsʞǝla
1
Na verdade, ele fará exatamente o que você precisa. Ele retornará todos os elementos da lista, exceto aqueles que não são iguais a "teste". Preste atenção que ele usa filterNot
btbvoy
14
A questão original era como remover uma única instância. Nem todas as instâncias.
ty1824
@ Søren Mathiasen se eu quiser filtrar vários elementos como sequência, como dados val = Seq ("teste", "a"), como fazer isso?
BdEngineer
18

Você pode tentar isto:

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)

E como método:

def removeInt(i: Int, li: List[Int]) = {
   val (left, right) = li.span(_ != i)
   left ::: right.drop(1)
}
Frank S. Thomas
fonte
3
É importante notar que left ::: right.drop(1)é mais curto do que a instrução if com isEmpty.
Rex Kerr
2
Obrigado, existe alguma circunstância para preferir .drop (1) em vez de .tail, ou vice-versa?
Gavilan Comun
8
@ James Petry - Se você chamar tailem uma lista vazia você recebe uma exceção: scala> List().tail java.lang.UnsupportedOperationException: tail of empty list. drop(1)em uma lista vazia, entretanto, retorna uma lista vazia.
Frank S. Thomas,
3
taillança uma exceção se a lista estiver vazia (ou seja, não houver head). drop(1)em uma lista vazia apenas produz outra lista vazia.
Rex Kerr
8

Infelizmente, a hierarquia de coleções se meteu em uma confusão com o -on List. Pois ArrayBufferfunciona exatamente como você pode esperar:

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

mas, infelizmente, Listacabou com uma filterNotimplementação de estilo e, portanto, faz a "coisa errada" e lança um aviso de depreciação para você (bastante sensato, uma vez que está realmente funcionando 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)

Portanto, sem dúvida, a coisa mais fácil de fazer é converter Listem uma coleção que faça isso direito e, em seguida, converter novamente:

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

Como alternativa, você pode manter a lógica do código que possui, mas tornar o estilo mais idiomático:

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)
Rex Kerr
fonte
removeInt(5,List(1,2,6,4,5,3,6,4,6,5,1))rendimentos List(4, 6, 2, 1, 3, 6, 4, 6, 5, 1). Acho que não era isso que você queria.
Ken Bloom,
@Ken Bloom - Certamente. É um erro no algoritmo original, que copiei sem pensar o suficiente. Corrigido agora.
Rex Kerr
Mais uma omissão na especificação da questão, já que a ordem não importa no meu caso específico. É bom ver a versão que preserva a ordem, obrigado.
Gavilan Comun
@Rex: o que você quer dizer com 'filtroNão faz a' coisa errada ''? Que está removendo todas as ocorrências? E por que ele lança um aviso de depreciação? Obrigado
teo
1
@teo - Ele remove todas as ocorrências (o que não é o desejado aqui), e está obsoleto porque está indiscutivelmente quebrado (ou talvez o comportamento desejado não esteja claro - de qualquer forma, ele está obsoleto no 2.9 e desapareceu no 2.10).
Rex Kerr
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
 }
Suat KARAKUSOGLU
fonte
2

E se

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

Se você vê return, há algo errado.

Eugene Yokota
fonte
1
Isso não faz o que ele quer, que é remover apenas a primeira instância dec
Ken Bloom
1
Isso removerá todos os cartões c, mas apenas primeiro deve ser removido.
tenshi
Devo ler as perguntas com mais atenção! corrigiu minha resposta.
Eugene Yokota,
+1 para "Se você ver o retorno, há algo errado." Essa é uma lição muito importante de "Scala idiomática" por si só.
Joe Carnahan,
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
}
Ken Bloom
fonte
1

Como uma das soluções possíveis, você pode encontrar o índice do primeiro elemento adequado e, em seguida, remover o elemento neste índice:

def removeOne(l: List[Card], c: Card) = l indexOf c match {
    case -1 => l
    case n => (l take n) ++ (l drop (n + 1))
}
tenshi
fonte
Veja minha resposta usando spanpara fazer a mesma coisa.
Ken Bloom,
0

Apenas outra ideia sobre como fazer isso usando uma dobra:

def remove[A](item : A, lst : List[A]) : List[A] = {
    lst.:\[List[A]](Nil)((lst, lstItem) => 
       if (lstItem == item) lst else lstItem::lst )
}
gdiz
fonte
0

Solução genérica de recursão de cauda:

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)
  }
Shankar Shastri
fonte
-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)
Huckleberry Finn
fonte
-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)

    }

}
Anbinson
fonte
Você poderia adicionar alguma explicação (comentários, descrição) sobre como isso responde à pergunta?
rjp
4
1. Esta pergunta foi feita e respondida 5 anos atrás. 2. O OP pediu um Scala "idiomático". Usar 2 se varum whileloop não é um Scala idiomático.
jwvh