/* how to construct lists */
val l0 = List(1,2,3) // the list [1,2,3]   has type List[Int]
List("apples","banannas") // List[String]
List('c','h','a','r') // List[Char]
 
List(Nil, List(3,4), List(5,6)) // List[List[Int]]
 
/*
*   Lists are: homogeneous - THEY CONTAIN ELEMENTS OF THE
*   same type
*
 */
 
/* how to construct lists (using cons, and Nil)*/
 
(1 :: (2 :: (3 :: Nil))).head //just the same as List(1,2,3)
 
/*
  e :: l
  ^ current element
       ^ the rest of the list
 */
/* The datatype pair
*   two values ("first") and ("second"),
*   not necessarily of the same type
* */
val pair = (1, true)
pair._1
pair._2
 
/* Observers for lists */
l0.head
// a "function" with zero parameters
l0.tail
 
(1,2) :: (3,4) :: Nil //List[(Int,Int)]
 
val l3 = (1,true) :: (2,false) :: Nil // List[(Int,Boolean)]
l3.head._2
 
/* implement stuff with lists */
 
def contains0 (e: Int, l: List[Int]): Boolean =
  if (l.isEmpty) false
  else if (e == l.head) true
  else contains0(e, l.tail)
 
/* there is a better way to decompose lists:
*  Pattern matching */
 
def contains1 (e: Int, l: List[Int]): Boolean =
  l match {
    case Nil => false
    case x :: xs => if (e == x) true else contains1(e,xs)
  }
 
/*
   <expression> match {
      case <pattern1> =>
      case <pattern2> =>
      ...
      case <pattern_n> =>
   }
 
   What patterns can be:
    - "ways" to construct objects using THEIR
      CONSTRUCTORS
 
   Examples of patterns:
 
   x :: Nil  - the list with only one element
   List(x)   - the very same thing
 
   x :: 1 :: xs - a list of at least two elements
 
   Nil :: xs - a list of lists, where the first
               element is Nil
 
   (1 :: Nil) :: xs - a list of lists where the fst
                      element is [1]
 
   (a,b) :: xs - a list of pairs, where the first pair
                 is (a,b)
 
   (Nil, 1 :: xs) - a pair where the first elem is
                    the empty list, and the snd is a non-empty list where the fst elem is 1
 
 */
// add 1 to each element of the list
def addOne(l: List[Int]): List[Int] =
  l match {
    case Nil => Nil
    case x :: xs => (x + 1) :: addOne(xs)
  }
 
def fmap (f: Int => Int)(l: List[Int]): List[Int] =
  l match {
    case Nil => Nil
    case x :: xs => f(x) :: fmap(f)(xs)
  }
 
def multiplyBy(v: Int, l: List[Int]): List[Int] =
  fmap(_ * v)(l)
 
val l3 = List(List(1,2), List(3,4), List(5,6))
val l4 = List(1,2,4)
 
//map from Scala
l3.map(_.isEmpty)
l3.map(_.head)
 
 
/* What is missing from our fmap? */
 
 
 
/*
   Discussion: a short-hand for writing lambda functions (anonymous functions)
   x => x * 2
 
   _ * 2
 
   (x, y) => x + y  equiv   _ + _
 
   (x, y) => x + y * x cannot be written shorter
 */
 
def sum(l: List[Int]): Int =
  l match {
    case Nil => 0
    case x :: xs => x + sum(xs)
  }
 
def product(l: List[Int]): Int =
  l match {
    case Nil => 1
    case x :: xs => x * product(xs)
  }
/*
Such code is repetitive. What is different:
   the initial value
   the operation
 */
 
def fold(acc: Int)(op: (Int, Int) => Int)(l: List[Int]) : Int =
  l match {
    case Nil => acc
    case x :: xs => op(x,fold(acc)(op)(xs))
  }
 
val l5 = List(1,2,3,4)
fold(0)(_ + _)(l5)
fold(1)(_ * _)(l5)
 
/* The fold from scala:
   This fold is called - foldRight
 */
 
l5.foldRight(0)(_ + _)
 
/* Why are folds important */
 
def max(l: List[Int]): Int = {
  l.tail.foldRight(l.head)((x,crt_max) => if (x > crt_max) x else crt_max)
}
max(List(1,2,7,3,5,2,9))
 
def contains(e: Int, l: List[Int]): Boolean =
  l.foldRight(false)(_ == e || _)
 
contains(12,List(3,4,6,5,6,1,2))
 
def avg(l: List[Int]): Int = {
  val p = l.foldRight((0, 0))((x, pair) => (pair._1 + x, pair._2 + 1))
  p._1 / p._2
}
//
def example(f: List[Int] => List[Int], l: List[Int]): Boolean = {
  ???
}
 
example((x)=>x.map(_ * 2), List(1,2,3))
 
example(fmap(_ * 2), List(1,2,3))