/*
Suppose we want to compute the sum of all integers from a range:
 */
 
def sumAll(start: Int, stop: Int): Int = {
  def loop (i: Int, crtSum: Int): Int =
    if (i > stop) crtSum
    else loop(i+1,crtSum+i)
  loop(start,0)
}
//... as well as the sum x*x of all elements
def sumSquares(start: Int, stop: Int): Int = {
  def loop (i: Int, crtSum: Int): Int =
    if (i > stop) crtSum
    else loop(i+1,crtSum+i*i)
  loop(start,0)
}
// we can see that the code is completely the same, with the
// exception of the accumulator. We can actually write a more general
// implementation that can be used for both sums:
 
def sumWithf(f: Int => Int, start: Int, stop: Int): Int = {
  def loop(i: Int, crtSum: Int): Int =
    if (i > stop) crtSum
    else loop(i+1,f(i)+crtSum)
  loop(start,0)
}
 
// How to use sumWithf?
def id(x: Int): Int = x
sumWithf(id,0,10)
 
// In Scala we can define functions without naming them (like anonymous classes)
// They are called anonymous functions, or shorter - "lambdas". We will see why later
 
sumWithf( (x:Int) => x, 0, 10)
// we can skip types in lambdas:
sumWithf( (x) => x, 0, 10)
// whenever our lambda has a single param, we can omit parentheses
sumWithf(x => x, 0, 10)
 
sumWithf(x => x*x, 0, 10)
 
// Now, imagine the current setting. We have a very large array
// of values, and we need to do some per element transformations
// like x => x * x, only more elaborate. The transformations are only
// a few. E.g. x => x, x => x * x, x => x * x * x. However we
// need to apply them over a lot of ranges. We might write the code
// like this:
 
def alg1(x: Int): Int = x
def alg2(x: Int): Int = x * x
def alg3(x: Int): Int = x * x * x
 
// now, we can use sumWithf(alg1,i,j) for all those i and j under scrutiny
// this does not scale nicely. The programmer should be able
// to write something like ApplyAlg1(i,j), without copy-pasting sumWithf,
// and preferabily by using sumWithf. We can achieve this as follows, by
// changing the design of f:
 
def currySumWithf(f : Int => Int): (Int, Int) => Int ={
  def sumWithf(start: Int, stop: Int): Int = {
    def loop (i: Int, acc: Int): Int =
      if (i > stop) acc
      else loop(i+1,f(i)+acc)
    loop(start,0)
  }
  sumWithf
}
 
val ApplyAlg1 = currySumWithf(x => x)
val ApplyAlg2 = currySumWithf(x => x * x)
ApplyAlg1(0,10)
ApplyAlg2(0,10)
 
// this is great, apart from the definition currySumWith f, which
// has a very long signature which is hard to read.
// This type of coding is so widespread that most languages have a special
// syntax to make it nicer to write.
 
def cleanSumWithf(f: Int => Int)(start: Int, stop: Int): Int = {
  def loop (i: Int, acc: Int): Int =
    if (i > stop) acc
    else loop(i+1,f(i)+acc)
  loop(start,0)
}
 
// we say that cleanSumWithf takes its parameters IN TURN. It first
// takes a function f, and returns another function taking two integers:
cleanSumWithf(x => x)(0,10)
 
def fcurry(x: Int)(y: Int)(z: Int): Int = x + y + z
// is fcurry(0,1,2) correct?
// if fcurry(0)(1,2) correct?
// we say that functions such as fcurry are in curry form. I.e. they take
// parameters IN TURN. On the other hand:
 
def funcurry(x: Int, y: Int, z: Int): Int = x + y + z
// is uncurried.
 
// We generally use curried functions whenever they help is build partial
// functionality that can be used in other places by the programmer.
// For instance sumWithf(x => x*x) designates Algorithm2 which can be
// applied on different ranges or passed as a parameter.
// A lot of library functions in Scala are curried, and we will see many examples
// as soon as we introduce lists.
// For now, we will stick to very basic examples which we use to practice.
 
// Suppose we want to combine two algorithms into one. For instance
// build x^5 by doing alg1(alg2(x)). There exists functional composition
// in Scala, but because everything is an object, including functions,
// we have to postpone it until we discuss objects in Scala.
 
//def compose(f: Int => Int, g: Int => Int): Int => Int
// sometimes, tracking types can be eased by introducing type aliases:
type Algorithm = Int => Int
 
/*
def compose(f: Algorithm, g: Algorithm): Algorithm = {
  def comp(x: Int): Int = f(g(x))
  comp
}*/
// we can improve this code to:
 
def compose(f: Algorithm, g: Algorithm): Algorithm =
  x => f(g(x))
 
type Reducer = (Int, Int) => Int
val Alg1and2: Reducer = cleanSumWithf(compose(x => x,x => x * x))
Alg1and2(0,10)
 
//Suppose we define:
// f(x) = y
type Fun2D = Int => Int
val h = (x:Int) => 2*x + 1
 
def gen2DFun(x: Int)(y: Int): Fun2D =
  z => x*z + y
// what kind of function is this?
 
val hp = gen2DFun(2)(1)
 
// Now, suppose we would like to write an alternative to gen2DFun
// which is not curry. We can obviously rewrite it:
 
def uncurryGen2D(x: Int, y: Int, z: Int): Int =
  x*z + y
 
// or we can write a piece of code that can be reused someplace-else:
 
/*
def uncurry(cf: Int => Int => Fun2D): (Int,Int,Int) => Int = {
  def aux(x: Int, y: Int, z:Int): Int = cf(x)(y)(z)
  aux
}*/
// or we can improve it to:
def uncurry(cf: Int => Int => Fun2D): (Int,Int,Int) => Int =
  (x,y,z) => cf(x)(y)(z)
 
uncurry(gen2DFun)(2,1,3)