Jaki jest idiomatyczny sposób Scali na „usunięcie” jednego elementu z niezmiennej listy?

84

Mam listę, która może zawierać elementy, które będą porównywane jako równe. Chciałbym mieć podobną listę, ale z usuniętym jednym elementem. Tak więc z (A, B, C, B, D) chciałbym móc „usunąć” tylko jedno B, aby uzyskać np. (A, C, B, D). Kolejność elementów w wyniku nie ma znaczenia.

Mam działający kod, napisany w Scali w sposób inspirowany Lispem. Czy jest na to bardziej idiomatyczny sposób?

Kontekstem jest gra karciana, w której w grze znajdują się dwie talie standardowych kart, więc mogą istnieć zduplikowane karty, ale nadal gra się je pojedynczo.

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
źródło
Dodano uwagę, że kolejność listy wyników nie ma znaczenia w tym konkretnym przypadku.
Gavilan Comun
więc List[Card]w tym pytaniu jest ręka gracza?
Ken Bloom,
@Ken Bloom, tak, to dobra ręka gracza.
Gavilan Comun
Wiesz, szukałem przez chwilę takiego pytania, potem opublikowałem to samo pytanie, a potem znalazłem to, gdy przeglądałem i czekałem, aż ludzie odpowiedzą na moje. Chyba powinienem teraz zagłosować za zamknięciem mojego pytania jako duplikatu. ;-)
Joe Carnahan
To pytanie do Clojure: stackoverflow.com/questions/7662447/…
Gavilan Comun

Odpowiedzi:

144

W powyższych odpowiedziach nie widziałem takiej możliwości, więc:

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)

Edytować:

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

Jak urok :-).

Antonin Brettsnajdr
źródło
18
Ładny! Dodałbym kolejne 2 do listy, aby wyjaśnić, że tylko jeden element jest usuwany.
Frank S. Thomas,
39

Możesz użyć tej filterNotmetody.

val data = "test"
list = List("this", "is", "a", "test")
list.filterNot(elm => elm == data)
Søren Mathiasen
źródło
21
Spowoduje to usunięcie wszystkich elementów, które są równe "test" - a nie to, o co jest proszone;)
yǝsʞǝla
1
Właściwie zrobi dokładnie to, czego potrzebujesz. Zwróci wszystkie elementy z listy oprócz tych, które nie są równe „test”. Zwróć uwagę, że używa filterNot
btbvoy
14
Pierwotne pytanie brzmiało, jak usunąć POJEDYNCZĄ instancję. Nie wszystkie instancje.
ty1824
@ Søren Mathiasen, jeśli chcę odfiltrować wiele elementów, takich jak sekwencja, np. Val data = Seq („test”, „a”), jak to zrobić?
BdEngineer
18

Możesz spróbować tego:

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)

I jako metoda:

def removeInt(i: Int, li: List[Int]) = {
   val (left, right) = li.span(_ != i)
   left ::: right.drop(1)
}
Frank S. Thomas
źródło
3
Warto zauważyć, że left ::: right.drop(1)jest krótszy niż instrukcja if z isEmpty.
Rex Kerr,
2
Dzięki, czy jest jakaś okoliczność, aby preferować .drop (1) zamiast .tail, czy odwrotnie?
Gavilan Comun
8
@James Petry - Jeśli zadzwonisz tailna pustej liście pojawi się wyjątek: scala> List().tail java.lang.UnsupportedOperationException: tail of empty list. drop(1)na pustej liście zwraca jednak pustą listę.
Frank S. Thomas,
3
tailzgłasza wyjątek, jeśli lista jest pusta (tj. nie ma head). drop(1)na pustej liście daje po prostu kolejną pustą listę.
Rex Kerr,
8

Niestety hierarchia kolekcji wpadła w trochę bałagan z -włączeniem List. Ponieważ ArrayBufferdziała tak, jak możesz mieć nadzieję:

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

ale niestety Listskończyło się na filterNotimplementacji w stylu a, przez co robi „złą rzecz” i rzuca ostrzeżenie o dezaprobacie (dość rozsądne, ponieważ tak naprawdę jest 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)

Prawdopodobnie najłatwiejszą rzeczą do zrobienia jest przekonwertowanie Listna zbiór, który robi to dobrze, a następnie ponowna konwersja:

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

Alternatywnie możesz zachować logikę kodu, który masz, ale uczynić styl bardziej idiomatycznym:

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
źródło
removeInt(5,List(1,2,6,4,5,3,6,4,6,5,1))plony List(4, 6, 2, 1, 3, 6, 4, 6, 5, 1). Myślę, że nie tego chciałeś.
Ken Bloom,
@Ken Bloom - rzeczywiście. To błąd w oryginalnym algorytmie, który skopiowałem bez zastanowienia. Naprawiono teraz.
Rex Kerr,
Bardziej pominięcie w specyfikacji pytania, ponieważ kolejność nie ma znaczenia w moim przypadku. Ale dobrze widzieć wersję zachowującą porządek, dzięki.
Gavilan Comun
@Rex: co masz na myśli, mówiąc „filterNie działa to źle”? Że usuwa wszystkie wystąpienia? I dlaczego generuje ostrzeżenie o wycofaniu? Dzięki
teo
1
@teo - usuwa wszystkie wystąpienia (co nie jest tym, co jest tutaj pożądane) i jest przestarzałe, ponieważ prawdopodobnie jest zepsute (lub być może pożądane zachowanie jest niejasne - tak czy inaczej, jest przestarzałe w 2.9 i zniknęło w 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
źródło
2

Co powiesz na

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

Jeśli widzisz return, coś jest nie tak.

Eugene Yokota
źródło
1
To nie robi tego, co chce, czyli usuwa tylko pierwszą instancjęc
Ken Bloom,
1
Spowoduje to usunięcie wszystkich kart c, ale tylko najpierw należy usunąć.
tenshi
Powinienem uważniej przeczytać pytania! poprawił moją odpowiedź.
Eugene Yokota,
+1 dla „Jeśli widzisz powrót, coś jest nie tak”. To bardzo ważna lekcja „idiomatyczna Scala” sama w sobie.
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
źródło
1

Jako jedno z możliwych rozwiązań możesz znaleźć indeks pierwszego odpowiedniego elementu, a następnie usunąć element z tego indeksu:

def removeOne(l: List[Card], c: Card) = l indexOf c match {
    case -1 => l
    case n => (l take n) ++ (l drop (n + 1))
}
tenshi
źródło
Zobacz moją odpowiedź używając spando zrobienia tego samego.
Ken Bloom,
0

Jeszcze jedna myśl, jak to zrobić za pomocą fałdy:

def remove[A](item : A, lst : List[A]) : List[A] = {
    lst.:\[List[A]](Nil)((lst, lstItem) => 
       if (lstItem == item) lst else lstItem::lst )
}
gdiz
źródło
0

Ogólne rozwiązanie rekurencji ogona:

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
źródło
-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
źródło
-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
źródło
Czy mógłbyś dodać jakieś wyjaśnienie (komentarze, opis), jak to odpowiada na pytanie?
rjp
4
1. To pytanie zostało zadane i udzielono na nie odpowiedzi 5 lat temu. 2. PO poprosił o „idiomatyczną” Scalę. Używanie 2 vars i whilepętli nie jest idiomatycznym Scala.
jwvh