How can I idiomatically "remove" a single element from a list in Scala and close the gap?

14,821

Solution 1

This is the use case of filter:

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

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

You want to use map when you are transforming all the elements of a list.

Solution 2

To answer your question directly, I think you're looking for patch, for instance to remove element with index 2 ("c"):

List("a","b","c","d").patch(2, Nil, 1)      // List(a, b, d)

where Nil is what we're replacing it with, and 1 is the number of characters to replace.

But, if you do this:

I have four lists that are associated with each other by index; one list stores the course names, one stores the time the class begins in a simple int format (ie 130), one stores either "am" or "pm", and one stores the days of the classes by int

you're going to have a bad time. I suggest you use a case class:

case class Course(name: String, time: Int, ampm: String, day: Int)

and then store them in a Set[Course]. (Storing time and days as Ints isn't a great idea either - have a look at java.util.Calendar instead.)

Solution 3

First a few sidenotes:

  1. List is not an index-based structure. All index-oriented operations on it take linear time. For index-oriented algorithms Vector is a much better candidate. In fact if your algorithm requires indexes it's a sure sign that you're really not exposing Scala's functional capabilities.

  2. map serves for transforming a collection of items "A" to the same collection of items "B" using a passed in transformer function from a single "A" to single "B". It cannot change the number of resulting elements. Probably you've confused map with fold or reduce.

To answer on your updated question

Okay, here's a functional solution, which works effectively on lists:

val (resultCourses, resultTimeList, resultAmOrPmList, resultDateList)
  = (courses, timeList, amOrPmList, dateList)
      .zipped
      .filterNot(_._1 == input)
      .unzip4

But there's a catch. I actually came to be quite astonished to find out that functions used in this solution, which are so basic for functional languages, were not present in the standard Scala library. Scala has them for 2 and 3-ary tuples, but not the others.

To solve that you'll need to have the following implicit extensions imported.

implicit class Tuple4Zipped 
  [ A, B, C, D ] 
  ( val t : (Iterable[A], Iterable[B], Iterable[C], Iterable[D]) ) 
  extends AnyVal 
  {
    def zipped 
      = t._1.toStream
          .zip(t._2).zip(t._3).zip(t._4)
          .map{ case (((a, b), c), d) => (a, b, c, d) }
  }

implicit class IterableUnzip4
  [ A, B, C, D ]
  ( val ts : Iterable[(A, B, C, D)] )
  extends AnyVal
  {
    def unzip4
      = ts.foldRight((List[A](), List[B](), List[C](), List[D]()))(
          (a, z) => (a._1 +: z._1, a._2 +: z._2, a._3 +: z._3, a._4 +: z._4)
        )
  }

This implementation requires Scala 2.10 as it utilizes the new effective Value Classes feature for pimping the existing types.

I have actually included these in a small extensions library called SExt, after depending your project on which you'll be able to have them by simply adding an import sext._ statement.

Of course, if you want you can just compose these functions directly into the solution:

val (resultCourses, resultTimeList, resultAmOrPmList, resultDateList)
  = courses.toStream
      .zip(timeList).zip(amOrPmList).zip(dateList)
      .map{ case (((a, b), c), d) => (a, b, c, d) }
      .filterNot(_._1 == input)
      .foldRight((List[A](), List[B](), List[C](), List[D]()))(
        (a, z) => (a._1 +: z._1, a._2 +: z._2, a._3 +: z._3, a._4 +: z._4)
      )

Solution 4

Removing and filtering List elements

In Scala you can filter the list to remove elements.

scala> val courses = List("Artificial Intelligence", "Programming Languages", "Compilers", "Networks", "Databases")
courses: List[java.lang.String] = List(Artificial Intelligence, Programming Languages, Compilers, Networks, Databases)

Let's remove a couple of classes:

courses.filterNot(p => p == "Compilers" || p == "Databases")

You can also use remove but it's deprecated in favor of filter or filterNot.

If you want to remove by an index you can associate each element in the list with an ordered index using zipWithIndex. So, courses.zipWithIndex becomes:

List[(java.lang.String, Int)] = List((Artificial Intelligence,0), (Programming Languages,1), (Compilers,2), (Networks,3), (Databases,4))

To remove the second element from this you can refer to index in the Tuple with courses.filterNot(_._2 == 1) which gives the list:

res8: List[(java.lang.String, Int)] = List((Artificial Intelligence,0), (Compilers,2), (Networks,3), (Databases,4))

Lastly, another tool is to use indexWhere to find the index of an arbitrary element.

courses.indexWhere(_ contains "Languages") res9: Int = 1

Re your update

I'm writing a function to remove the corresponding element from each lists, and all I know is that 1) the indices correspond and 2) the user inputs the course name. How can I remove the corresponding element from each list using filterNot?

Similar to Nikita's update you have to "merge" the elements of each list. So courses, meridiems, days, and times need to be put into a Tuple or class to hold the related elements. Then you can filter on an element of the Tuple or a field of the class.

Combining corresponding elements into a Tuple looks as follows with this sample data:

val courses = List(Artificial Intelligence, Programming Languages, Compilers, Networks, Databases)
val meridiems = List(am, pm, am, pm, am)
val times = List(100, 1200, 0100, 0900, 0800)
val days = List(MWF, TTH, MW, MWF, MTWTHF)

Combine them with zip:

courses zip days zip times zip meridiems

val zipped = List[(((java.lang.String, java.lang.String), java.lang.String), java.lang.String)] = List((((Artificial Intelligence,MWF),100),am), (((Programming Languages,TTH),1200),pm), (((Compilers,MW),0100),am), (((Networks,MWF),0900),pm), (((Databases,MTWTHF),0800),am))

This abomination flattens the nested Tuples to a Tuple. There are better ways.

zipped.map(x => (x._1._1._1, x._1._1._2, x._1._2, x._2)).toList

A nice list of tuples to work with.

List[(java.lang.String, java.lang.String, java.lang.String, java.lang.String)] = List((Artificial Intelligence,MWF,100,am), (Programming Languages,TTH,1200,pm), (Compilers,MW,0100,am), (Networks,MWF,0900,pm), (Databases,MTWTHF,0800,am))

Finally we can filter based on course name using filterNot. e.g. filterNot(_._1 == "Networks")

List[(java.lang.String, java.lang.String, java.lang.String, java.lang.String)] = List((Artificial Intelligence,MWF,100,am), (Programming Languages,TTH,1200,pm), (Compilers,MW,0100,am), (Databases,MTWTHF,0800,am))

Share:
14,821
jkeys
Author by

jkeys

Updated on June 04, 2022

Comments

  • jkeys
    jkeys almost 2 years

    Lists are immutable in Scala, so I'm trying to figure out how I can "remove" - really, create a new collection - that element and then close the gap created in the list. This sounds to me like it would be a great place to use map, but I don't know how to get started in this instance.

    Courses is a list of strings. I need this loop because I actually have several lists that I will need to remove the element at that index from (I'm using multiple lists to store data associated across lists, and I'm doing this by simply ensuring that the indices will always correspond across lists).

      for (i <- 0 until courses.length){
        if (input == courses(i) {
        //I need a map call on each list here to remove that element
        //this element is not guaranteed to be at the front or the end of the list
        }
      }
    }
    

    Let me add some detail to the problem. I have four lists that are associated with each other by index; one list stores the course names, one stores the time the class begins in a simple int format (ie 130), one stores either "am" or "pm", and one stores the days of the classes by int (so "MWF" evals to 1, "TR" evals to 2, etc). I don't know if having multiple this is the best or the "right" way to solve this problem, but these are all the tools I have (first-year comp sci student that hasn't programmed seriously since I was 16). I'm writing a function to remove the corresponding element from each lists, and all I know is that 1) the indices correspond and 2) the user inputs the course name. How can I remove the corresponding element from each list using filterNot? I don't think I know enough about each list to use higher order functions on them.

  • jkeys
    jkeys over 11 years
    I added some details to the OP. I understand you're not my prof, but could you show me how to what I want to do?
  • jkeys
    jkeys over 11 years
    Update: the next chapter in our book was on case classes, and one of the projects for that chapter was refactoring this code with the use of case classes in mind. Now I have a list of single objects as opposed to four lists - success!
  • Zook
    Zook almost 10 years
    Docs: Right under patch is updated, which is exactly what I was looking for. I bet that it works for your case too, you can just omit the last parameter.
  • Zook
    Zook almost 10 years
    Turns out updated won't actually remove an element with Nil, it will replace it with "Nil". patch removes the element(s).
  • shinzou
    shinzou almost 3 years
    He asked for a single element not all elements. This is wrong.
  • pedrofurla
    pedrofurla almost 3 years
    Tell me how is List(1, 2, 3, 4, 5) is different from List(1, 3, 4, 5) if not by a single element? It is only going to remove "all elements" if the all elements are 2. To be more precise, the question ask to remove a value. That's exactly what the answer provides.
  • shinzou
    shinzou almost 3 years
    It literally say in the title "remove single element" which is not "remove a value" or "remove all values" which is what filter does.
  • pedrofurla
    pedrofurla almost 3 years
    SInce repl evidence is not enough: a counter example: List(1,2,3,4).filter(_ !=10) == List(1,2,3,4).
  • pedrofurla
    pedrofurla almost 3 years
    Another one List(1,2,3,4).filter(_:Int => true) ,