Wednesday, February 12, 2014

How to use lists in Scala




Lists are....

  • persistent (immutable) - you cannot change it, but you can create copy
  • one-way - you can take next element, but not previous
  • ordered - each element has position
  • not unique - elements can be repeated in list
  • list take 1 parameter - type of element
  • recursive - they are either empty or a head followed by list


  • Table of contents...




    Empty list

    Nil //Nil is "The empty list." in Scala //List() (result)
    List() //Empty list //List() (result)
    List[AnyVal]() //List of numeric values //List() (result)
    List.empty[AnyVal]//same as above //List() (result)


    Type of an empty list

    typeOf(Nil) //scala.collection.immutable.Nil.type (result)
    typeOf(List()) //List[Nothing] (result)
    typeOf(List[Nothing]()) //List[Nothing] (result)
    typeOf(List[Any]()) //List of anything //List[Any] (result)
    typeOf(List[String]()) //List of strings //List[String] (result)
    typeOf(List.empty[String]) //List of strings //List[String] (result)


    Are empty lists equal?

    Yes.
    Nil == List() //true (result)
    List() == List[AnyVal]() //true (result)
    List[AnyVal]() == List.empty[AnyVal] //true (result)
    Lists of different types are equal if they're empty
    List[Int]() == List[String]() //true (result)


    Non-empty lists

    List of Int's
    val int_list = List(1, 2, 3)
    typeOf(int_list) //List[Int] (result)
    List of Strings
    val string_list = List("1", "2", "3")
    typeOf(string_list) //List[String] (result)


    Mixing element type

    Scala will try to keep most of information about type of elements
    List of Strings and Int's
    (common type is Any)
    val mixed_list_si = List("1", 2, "3")
    typeOf(mixed_list_si) //List[Any] (result)
    List of Int's and Double's
    (common type is AnyVal, there's conversion from int to double)
    val mixed_list_id = List(1, 2.0, 3)
    typeOf(mixed_list_id) //List[Double] (result)
    List of Int's and BigInt's
    (common type is Any)
    val mixed_list_ibi = List(1, BigInt(2), 3)
    typeOf(mixed_list_ibi) //List[Any] (result)


    Getting elements of list

    val items = List(1, 2, 3, 4.0)
    head of list (first element)
    items.head // fast, complexity O //1.0 (result)
    head cannot be used on empty list
    List().head
    Output
    Exception:
    java.util.NoSuchElementException: head of empty list

    head is a 0-th element of list
    items(0) == items.head //true (result)
    last element (we must iterate all elements to get there)
    items.last // slow, complexity N //4.0 (result)
    n-th element (we must iterate n elements to get there)
    items(3) // slow, complexity N //4.0 (result)


    Size of list

    size and length are eqivalent. Scala does not keep a counter of elements. We must iterate over all elements count them.
    items.size   // slow, complexity N //4 (result)
    items.length // slow, complexity N //4 (result)


    Splitting list into head and tail

    Split list to head and tail
    val items_head::items_tail = items
    //this will create 2 val's
    items_head //1.0 (result)
    items_tail //List(2.0, 3.0, 4.0) (result)
    Empty list cannot be split (no head)
    val empty_head::empty_tail = List()
    Output
    Exception:
    scala.MatchError: List() (of class scala.collection.immutable.Nil$)

    val empty_head::empty_tail = List[Int]()
    Output
    Exception:
    scala.MatchError: List() (of class scala.collection.immutable.Nil$)

    Single element list can be split info element and Empty list
    5 :: Nil == List[Int](5) //true (result)
    val single_head::single_tail = List[Int](5)
    single_head //5 (result)
    single_tail //List() (result)


    Lists are recursive


    List items
    items //List(1.0, 2.0, 3.0, 4.0) (result)
    is equivalent to
    1.0::(2.0::(3.0::(4.0::(Nil))))
    Output
    List(1.0, 2.0, 3.0, 4.0)




    Getting part of list

    items //List(1.0, 2.0, 3.0, 4.0) (result)
    Ignore 2 first elements, take the rest
    items.drop(2) //fast //List(3.0, 4.0) (result)
    Take 3 first elements, ignore the rest
    items.take(3) //fast //List(1.0, 2.0, 3.0) (result)
    typeOf(items.take(3)) //List[Double] (result)
    drop and take work with empty lists
    List().drop(2) //List() (result)
    List().take(2) //List() (result)
    and with list smaller than given number of elements
    items.drop(20) //empty //List() (result)
    items.take(20) //all list //List(1.0, 2.0, 3.0, 4.0) (result)
    take n last elements
    items.takeRight(4) //slow //List(1.0, 2.0, 3.0, 4.0) (result)
    items.reverse.take(4) //slow //List(4.0, 3.0, 2.0, 1.0) (result)
    drop n last elements
    items.dropRight(4) //slow //List() (result)
    items.reverse.drop(4) //slow //List() (result)


    Iteration

    def funny(x : Double) = x * 2
    for(x <- 0 until items.length) yield funny(x)
    Output
  • Vector(0.0, 2.0, 4.0, 6.0)

  • def foo(l : List[Double]) : List[Double]= l match {
    	case Nil => Nil
    	case h::t => funny(h)::foo(t)
    }
    
    foo(items)
    Output
    List(2.0, 4.0, 6.0, 8.0)

    Why not map?
    items.map(funny)
    Output
    List(2.0, 4.0, 6.0, 8.0)

    Or parallel map?
    items.par.map(funny)
    Output
    ParVector(2.0, 4.0, 6.0, 8.0)



    Get index of element

    items.zipWithIndex
    Output
    List((1.0,0), (2.0,1), (3.0,2), (4.0,3))

    Then we can use map
    items.zipWithIndex.map {ei => 
    	var (element,index) = ei
    	"items[" + index + "]=" + element}
    Output
    List(items[0]=1.0, items[1]=2.0, items[2]=3.0, items[3]=4.0)

    ...and partial function to extract item and index
    items.zipWithIndex.map{case (item,index) => s"items[$index]=$item"}
    Output
    List(items[0]=1.0, items[1]=2.0, items[2]=3.0, items[3]=4.0)



    Joining lists

    int_list //List(1, 2, 3) (result)
    typeOf(int_list) //List[Int] (result)
    string_list //List(1, 2, 3) (result)
    typeOf(string_list) //List[String] (result)
    Join of two lists (list style)
    int_list ::: string_list //List(1, 2, 3, 1, 2, 3) (result)
    typeOf(int_list ::: string_list) //List[Any] (result)
    Join of two lists (universal style)
    int_list ++ string_list //List(1, 2, 3, 1, 2, 3) (result)


    Adding element to list

    int_list //List(1, 2, 3) (result)
    typeOf(int_list) //List[Int] (result)
    list-style add as head
    100::items //List(100, 1.0, 2.0, 3.0, 4.0) (result)
    (100::items).head //100 (result)
    universal append as head
    100+:items //List(100, 1.0, 2.0, 3.0, 4.0) (result)
    (100+:items).head //100 (result)
    universal append as last
    items:+200 //List(1.0, 2.0, 3.0, 4.0, 200) (result)
    (items:+200).last //200 (result)


    Operators

  • colon operators :: and ::: are designed for Lists
  • colon operators +:,:+ and ++ are designed for all Traversables
  • +: and :+ are designed for use with all Traversables. since Scala 2.10 you can use them in pattern matching.
  • C.O.L.T. - COlons Like Traversables You should always put list or traversable near colon. (operators are bound to the side with colon)
    2+:List(3,7) //List(2, 3, 7) (result)
    is equivalent to
    List(3,7).+:(2) //List(2, 3, 7) (result)
    but
    List(3,7):+2 //List(3, 7, 2) (result)
    is equivalent to
    List(3,7).:+(2) //List(3, 7, 2) (result)


  • Common usecases

    Create list of length 4, filled with element 3
    List.make(4, 3) //List(3, 3, 3, 3) (result)
    Join list into String
    List(5,2,1).mkString(" and ") //5 and 2 and 1 (result)
    Join list into String with prefix and postfix
    List(5,2,1).mkString("list contains ", " and ", ".") //list contains 5 and 2 and 1. (result)
    Remove duplicates from list
    List(1,1,0,1,1,0,0,1,2).distinct //List(1, 0, 2) (result)

    No comments:

    Post a Comment