Scala-函数式编程

Scala-函数式编程

1.函数式编程

1.1 面向对象和面向过程

面向对象

  • 按照功能划分问题,将功能分解成不同的的对象,定义对象中的行为和属性,最终通过对象的行为调用来解决问题。
  • 优点:
    • 耦合度低。
  • 缺点:
    • 执行效率低。

面向过程

  • 按照步骤解决问题。
  • 优点:
    • 执行效率高。
  • 缺点:
    • 耦合度高。

1.2 函数式编程

函数式编程和命令式编程的区别:

  • 其实面向对象和面向过程都是命令式编程(计算机的命令),其实语言其实都是面向计算机硬件的抽象。
    • int a=1;这里a只是内存地址的引用,还可以让a=其他数,是个变量。
    • 命令式编程的编码都可以翻译成计算机底层的指令,对计算机最友好的语言,是计算机的子程序。
  • 而函数式编程则重点关注的是数据的映射关系(自变量因变量的映射关系), 不关心计算机底层如何实现,这里的函数指的是数学中的函数。
    • val a =1 ,函数式编程中其实没有变量,即一个常量,a=1不能再让a=其他数,这更符合数学中函数的定义。
    • sacla中因为其有函数式编程的特点,所以推荐能用常量的不要用变量。
    • 定义一个函数,求未知数的值,函数式编程中每一段程序都有一个返回值。
    • 函数式编程本质就是数据的映射关系,定义一个表达式,通过该表达式不同的求值,做函数的映射关系。
    • 函数至简原则:能省则省 ,只专注于对应的业务代码。
      • java1.8中的函数式编程就是参照于此,简化和业务无关的逻辑。

两种编程范式的优缺点:

  • 命令式编程:
    • 是对于计算机有好的编程,因为都是对应计算机命令的编程。
    • 因为最终都是翻译成计算机命令,所以其受计算机环境的影响很大。
  • 函数式编程:
    • 是对于人更好理解的,定义函数的理念和数学上一致,不用关心计算机底层。
    • 定义一个函数,那么其功能就确定了,该公式是不可变的,入参或者说未知数可以来回变,但是同一个值对应的结果一定是唯一的。
    • 天然适用于分布式计算:
      • 函数式编程中没有变量,都是常量,处理逻辑的过程中具有不可变性,不受外界影响,天然适合在不同的机器上运行,最终只需要将各个机器上的值汇总即可,特别的适合大数据处理的分布式计算。

scala兼容了两种编程范式的特点,scala是一个面向对象的语言,同时也是一个面向编程的语言。

2. 函数基础

2.1 函数基本语法

image.png

2.1.1 例子

object TestFunction {
    def main(args: Array[String]): Unit = {
        // (1)函数定义
        def f(arg: String): Unit = {
            println(arg)
        }
        // (2)函数调用
        // 函数名(参数)
        f("hello world")
    }
}

2.2 函数和方法的区别

java中的方法强调的是:

  • 类中的函数,类中定义的位置是有限制的。
    • 比如就不能在方法中定义方法。
  • 方法可以进行重载和重写。

scala中的函数

  • 定义在类中的任意代码块中。
  • Scala 语言可以在任何的语法结构中声明任何的语法。
  • 函数没有重载和重写的概念。
  • Scala 中函数可以嵌套定义。

2.2.1 例子

object FunctionAndMethod {
  def main(args: Array[String]): Unit = {
    // 定义函数
    def sayHi(name: String): Unit = {
      println("hi, " + name)
    }

    // 调用函数,根据函数的作用域,按照就近原则,先调用main中定义的sayHi函数
    sayHi("alice")

    // 调用对象方法,为了调用方法可以如下调用
    FunctionAndMethod.sayHi("bob")

    // 获取方法返回值
    val result = FunctionAndMethod.sayHello("cary")
    println(result)
  }

  // 定义对象的方法
  def sayHi(name: String): Unit = {
    println("Hi, " + name)
  }

  def sayHello(name: String): String = {
    println("Hello, " + name)
    return "Hello"
  }
}

2.3 函数的定义

  • 函数 1:无参,无返回值
  • 函数 2:无参,有返回值
  • 函数 3:有参,无返回值
  • 函数 4:有参,有返回值
  • 函数 5:多参,无返回值
  • 函数 6:多参,有返回值

2.3.1 例子

object FunctionDefine {
  def main(args: Array[String]): Unit = {
    //    (1)函数1:无参,无返回值
    def f1(): Unit = {
      println("1. 无参,无返回值")
    }
    f1()
    println(f1())

    println("=========================")

    //    (2)函数2:无参,有返回值
    def f2(): Int = {
      println("2. 无参,有返回值")
      return 12
    }
    println(f2())

    println("=========================")

    //    (3)函数3:有参,无返回值
    def f3(name: String): Unit = {
      println("3:有参,无返回值 " + name)
    }

    println(f3("alice"))

    println("=========================")

    //    (4)函数4:有参,有返回值
    def f4(name: String): String = {
      println("4:有参,有返回值 " + name)
      return "hi, " + name
    }

    println(f4("alice"))

    println("=========================")

    //    (5)函数5:多参,无返回值
    def f5(name1: String, name2: String): Unit = {
      println("5:多参,无返回值")
      println(s"${name1}和${name2}都是我的好朋友")
    }

    f5("alice","bob")

    println("=========================")

    //    (6)函数6:多参,有返回值
    def f6(a: Int, b: Int): Int = {
      println("6:多参,有返回值")
      return a + b
    }

    println(f6(12, 37))
  }
}

2.4 函数参数

  • (1)可变参数 (不确定个参数传入)
  • (2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
  • (3)参数默认值,一般将有默认值的参数放置在参数列表的后面
  • (4)带名参数(入参可以选择性的指定某个参数进行传值)

2.4.1 例子

object Test03_FunctionParameter {
  def main(args: Array[String]): Unit = {
    //    (1)可变参数,其实此时入参已经是集合类型了,这里是数组
    def f1(str: String*): Unit = {
      println(str)
    }

    f1("alice")
    f1("aaa", "bbb", "ccc")

    //    (2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
    def f2(str1: String, str2: String*): Unit = {
      println("str1: " + str1 + " str2: " + str2)
    }
    f2("alice")
    f2("aaa", "bbb", "ccc")

    //    (3)参数默认值,一般将有默认值的参数放置在参数列表的后面
    def f3(name: String = "xxx"): Unit = {
      println("My school is " + name)
    }

    f3("school")
    f3()

    //    (4)带名参数
    def f4(name: String = "xx", age: Int): Unit = {
      println(s"${age}岁的${name}在学习")
    }

    f4("alice", 20)
    f4(age = 23, name = "bob")
    f4(age = 21)
  }
}

输出

WrappedArray(alice)
WrappedArray(aaa, bbb, ccc)

str1: alice str2: WrappedArray()
str1: aaa str2: WrappedArray(bbb, ccc)

My school is school
My school is xxx

20岁的alice在学习
23岁的bob在学习
21岁的xx在学习

2.5 函数至简原则

函数至简原则:能省则省 ,只专注于对应的业务代码。

2.5.1 至简原则细节

  • (1)return 可以省略,Scala 会使用函数体的最后一行代码作为返回值
  • (2)如果函数体只有一行代码,可以省略花括号
  • (3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
  • (4)如果有 return,则不能省略返回值类型,必须指定
  • (5)如果函数明确声明 unit,那么即使函数体中使用 return 关键字也不起作用
  • (6)Scala 如果期望是无返回值类型,可以省略等号
  • (7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
  • (8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
  • (9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略

2.5.2 例子

object Simplify {
  def main(args: Array[String]): Unit = {

    def f0(name: String): String = {
      return name
    }

    println(f0("xxx"))

    println("==========================")

    //    (1)return可以省略,Scala会使用函数体的最后一行代码作为返回值
    def f1(name: String): String = {
      name
    }
    println(f1("xxx"))

    println("==========================")

    //    (2)如果函数体只有一行代码,可以省略花括号
    def f2(name: String): String = name
    println(f2("xxx"))

    println("==========================")

    //    (3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
    def f3(name: String) = name
    println(f3("xxx"))

    println("==========================")

    //    (4)如果有return,则不能省略返回值类型,必须指定
//    def f4(name: String) = {
//      return name
//    }
//
//    println(f4("xxx"))

    println("==========================")

    //    (5)如果函数明确声明unit,那么即使函数体中使用return关键字也不起作用
    def f5(name: String): Unit = {
      return name
    }

    println(f5("xxx"))

    println("==========================")

    //    (6)Scala如果期望是无返回值类型,可以省略等号
    def f6(name: String) {
      println(name)
    }

    println(f6("xxx"))

    println("==========================")

    //    (7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
    def f7(): Unit = {
      println("xxx")
    }

    f7()
    f7

    println("==========================")

    //    (8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
    def f8: Unit = {
      println("xxx")
    }

//    f8()
    f8

    println("==========================")

    //    (9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
    def f9(name: String): Unit = {
      println(name)
    }

    // 匿名函数,lambda表达式
    (name: String) => { println(name) }

    println("==========================")
    
    def f10 = (x:String)=>{println("wusong")}
    def f11(f:String=>Unit) = {
    f("")
    }
    f10(f0)
    println(f10((x:String)=>{println("wusong")}))


   // 匿名函数的简化原则
   f((name: String) => {
      println(name)
    })
   
    //    (1)参数的类型可以省略,会根据形参进行自动的推导
    f((name) => {
      println(name)
    })

    //    (2)类型省略之后,发现只有一个参数,则圆括号可以省略;其他情况:没有参数和参数超过1的永远不能省略圆括号。
    f( name => {
      println(name)
    })

    //    (3)匿名函数如果只有一行,则大括号也可以省略
    f( name => println(name) )

    //    (4)如果参数只出现一次,则参数省略且后面参数可以用_代替,参数名只在函数中出现一次
    f( println(_) )

    //     (5) 如果可以推断出,当前传入的println是一个函数体,而不是调用语句,可以直接省略下划线
    f( println )

    println("=========================")
  }
}

3. 函数高级

3.1 高阶函数

1)函数作为值传递

object HighOrderFunction {
  def main(args: Array[String]): Unit = {
    def f(n: Int): Int = {
      println("f调用")
      n + 1
    }
   // 1.普通的函数调用
    val result: Int = f(123)
    println(result)

    // 2. 函数作为值进行传递,有点像重命名
    // f1,f2要的是函数体,而不是触发函数
    val f1: Int=>Int = f
    val f2 = f _
    //f1,f2输出的是函数对象,对象引用不同
    println(f1)
    println(f1(12))
    println(f2)
    println(f2(35))
    // 3 无参函数
    def fun(): Int = {
      println("fun调用")
      1
    }
    //调用函数体
    val f3: ()=>Int = fun
    val f4 = fun _
    println(f3)
    println(f4)
   //注意:如果写val f4 = fun 那这就是一次函数
    
  }
}

输出

f调用
124

com.pl.HighOrderFunction$$$Lambda$5/1510067370@19bb089b
f调用
13
com.pl.HighOrderFunction$$$Lambda$6/1908923184@4563e9ab

com.pl.HighOrderFunction$$$Lambda$7/1289479439@7cf10a6f
com.pl.HighOrderFunction$$$Lambda$8/6738746@7e0babb1

2)函数作为参数传递

object Test1 {
  def main(args: Array[String]): Unit = {
 // (1)定义一个函数,函数参数还是一个函数签名;f 表示函数名称;(Int,Int) 表示输入两个 Int 参数;Int 表示函数返回值
    def dualEval(op:(Int,Int)=>Int,a:Int,b:Int):Int={
      op(a,b)
    }
 // (1)定义一个函数,函数参数还是一个函数签名;f 表示函数名称;(Int,Int) 表示输入两个 Int 参数;Int 表示函数返回值
    def add(a:Int,b:Int):Int={
      a+b;
    }
   // (3)将 add 函数作为参数传递给 f1 函数,如果能够推断出来不是调用
    println(dualEval(add,1,2))
    println(dualEval((a,b)=>a+b,1,2))

  }
}

3)函数作为函数返回值返回

package chapter05.test

object Test1 {
  def main(args: Array[String]): Unit = {
  
    // 3. 函数作为函数的返回值返回
    // Int=>Unit 返回函数的入参和返回值
    def f5(): Int=>Unit = {
      def f6(a: Int): Unit = {
        println("f6调用 " + a)
      }
      f6    // 将函数直接返回
    }

        val f6 = f5()
        println(f6)
        println(f6(25))

    println(f5()(25))
  }
}

输出

chapter05.test.Test1$$$Lambda$1/1989780873@47f37ef1    // 返回函数对象
f6调用 25
()
f6调用 25
()

4) 引用案例

  其实高阶函数的一个应用比较多的场合是:定义集合中数据的操作,将操作抽象出来。

object Test07_Practice_CollectionOperation {
  def main(args: Array[String]): Unit = {
    val arr: Array[Int] = Array(12, 45, 75, 98)

    // 对数组进行处理,将操作抽象出来,处理完毕之后的结果返回一个新的数组
    def arrayOperation(array: Array[Int], op: Int=>Int): Array[Int] = {
      for (elem <- array) yield op(elem)
    }

    // 定义一个加一操作
    def addOne(elem: Int): Int = {
      elem + 1
    }

    // 调用函数,传递函数  这里可以看出arrayOperation只是定义函数处理的大致流程,具体的逻辑推迟到调用方,和map的逻辑很符合
    val newArray: Array[Int] = arrayOperation(arr, addOne)

    println(newArray.mkString(","))

    // 传入匿名函数,实现元素翻倍
    val newArray2 = arrayOperation(arr, _ * 2)
    println(newArray2.mkString(", "))
  }
}

5)扩展练习

(1) 定义一个匿名函数,并将它作为值赋给变量 fun。函数有三个参数,类型分别为 Int,String,Char,返回值类型为 Boolean。 要求调用函数 fun(0, “”, ‘0’)得到返回值为 false,其它情况均返回 true。

object Test08_Practice {
  def main(args: Array[String]): Unit = {
    // 1. 练习1
    val fun = (i: Int, s: String, c: Char) => {
      if (i == 0 && s == "" && c == '0') false else true
    }

    println(fun(0, "", '0'))
    println(fun(0, "", '1'))
    println(fun(23, "", '0'))
    println(fun(0, "hello", '0'))

    println("===========================")

   
  }
}

(2) 定义一个函数 func,它接收一个 Int 类型的参数,返回一个函数(记作 f1)。 它返回的函数 f1,接收一个 String 类型的参数,同样返回一个函数(记作 f2)。函数 f2 接 收一个 Char 类型的参数,返回一个 Boolean 的值。 要求调用函数 func(0) (“”) (‘0’)得到返回值为 false,其它情况均返回 true。

object Test08_Practice {
  def main(args: Array[String]): Unit = {
    // 2. 练习2
    def func(i: Int): String=>(Char=>Boolean) = {
      def f1(s: String): Char=>Boolean = {
        def f2(c: Char): Boolean = {
          if (i == 0 && s == "" && c == '0') false else true
        }
        f2
      }
      f1
    }

    println(func(0)("")('0'))
    println(func(0)("")('1'))
    println(func(23)("")('0'))
    println(func(0)("hello")('0'))

    // 匿名函数简写
    def funcc(i: Int): String=>(Char=>Boolean) = {
      //匿名函数首先不需要知道名字,且返回值不用写即所有参数类型的定义省略
     // def f1(s: String): Char=>Boolean = {
      (s: String) =>{
       (c: Char)=> {
          if (i == 0 && s == "" && c == '0') false else true
        }
      }
    }
    //当然还可以进一步缩写
    //如果在外侧已经将形参类型定义好,那么内层的匿名函数形参也都是可以确定的
    def func1(i: Int): String=>(Char=>Boolean) = {
      s => c => if (i == 0 && s == "" && c == '0') false else true
    }

    println(func1(0)("")('0'))
    println(func1(0)("")('1'))
    println(func1(23)("")('0'))
    println(func1(0)("hello")('0'))

    // 上面的简写还可以进一步省略,将 String=>(Char=>Boolean)省略
    // 函数的柯里化
    def func2(i: Int)(s: String)(c: Char): Boolean = {
      if (i == 0 && s == "" && c == '0') false else true
    }
    println(func2(0)("")('0'))
    println(func2(0)("")('1'))
    println(func2(23)("")('0'))
    println(func2(0)("hello")('0'))
  }
}

3.2 闭包&柯里化

闭包

  • 如果一个函数,访问到了它的外部(局部)变量的值,那么这个函数和他所处的 环境,称为闭包。
  • 即内部函数将依赖的外部变量保存在本函数中,延长了外部函数局部变量的生命周期。
  • scala中调用函数相当于创建了一个对象实例,对象实例在heap中,改对象实例打包保存了该对象的环境(外部环境和局部变量),所以不会因为方法弹栈而丢失方法局部变量。
    • 比如上面例子中的func2(0)("")('0'),实际上的调用顺序是:func2>f1>f2,f2并不会因为前两者的弹栈而丢失其依赖的局部变量。

函数柯里化

  • 把一个参数列表的多个参数变成多个参数列表。
  • 一般纯函数式编程就是定义自变量和因变量之间的关系,只有一个入参,得到一个因变量,不存在输入多个自变量得出一个因变量的用法。
  • 但是scala中因为需要兼容java和函数式编程,所以没有只有一个入参的限制,可以允许多个入参。
  • 函数柯里化可以实现这么一个效果,每一层调用只有一个入参,所以一个参数列表多个参数实际可以变成多个参数列表。
object Test09_ClosureAndCurrying {
  def main(args: Array[String]): Unit = {
    def add(a: Int, b: Int): Int = {
      a + b
    }

    // 1. 考虑固定一个加数的场景
    def addByFour(b: Int): Int = {
      4 + b
    }

    // 2. 扩展固定加数改变的情况
    def addByFive(b: Int): Int = {
      5 + b
    }

    // 3. 将固定加数作为另一个参数传入,但是是作为”第一层参数“传入
    def addByFour1(): Int=>Int = {
      val a = 4
      def addB(b: Int): Int = {
        a + b
      }
      addB
    }

    def addByA(a: Int): Int=>Int = {
      def addB(b: Int): Int = {
        a + b
      }
      addB
    }

    println(addByA(35)(24))
    println(addByA(35))

    val addByFour2 = addByA(4)
    val addByFive2 = addByA(5)

    println(addByFour2(13))
    println(addByFive2(25))

    // 4. lambda表达式简写
    def addByA1(a: Int): Int=>Int = {
      //def 函数名 返回值均省略
      (b: Int) => {
        a + b
      }
    }
    //进一步简写  省略形参
    def addByA2(a: Int): Int=>Int = {
      //def 函数名 形参 返回值均省略
      b => a + b
    }
    // 进一步简写  参数只出现一次,函数只有一行
    def addByA3(a: Int): Int=>Int = a + _
    val addByFour3 = addByA3(4)
    val addByFive3 = addByA3(5)

    println(addByFour3(13))
    println(addByFive3(25))

    // 5. 柯里化  该函数分为两层,几个参数列表几层  一旦用到柯里化,那么其底层必然是闭包
    def addCurrying(a: Int)(b: Int): Int = {
      a + b
    }

    println(addCurrying(35)(24))
  }
}

输出

59
chapter05.Test09_ClosureAndCurrying$$$Lambda$5/1510067370@2ff4f00f   //println(addByA(35))实际输出的是函数对象实例
17
30
17
30
59

3.3 递归

object Test10_Recursion {
  def main(args: Array[String]): Unit = {
    println(fact(5))
    println(tailFact(5))
  }

  // 阶乘
 // 递归算法
 // 1) 方法调用自身
 // 2) 方法必须要有跳出的逻辑
 // 3) 方法调用自身时,传递的参数应该有规律
 // 4) scala 中的递归必须声明函数返回值类型
  // 递归实现计算阶乘
  def fact(n: Int): Int = {
    if (n == 0) return 1
    //最后一层代码可以省略 return
    fact(n - 1) * n
  }
  //上面这种递归方式有一个很大的确定就是:每层递归都需要产生新的栈帧,如果层数很多,会导致当前的栈中需要保存的栈帧非常多,甚至会出现stack over flow
  //递归是以耗费栈空间资源为代价的
  //函数式编程语言中提供了一种优化方式:每层栈帧覆盖之前的栈帧,只消耗一个栈帧
  
  // 尾递归实现
  def tailFact(n: Int): Int = {
    //尾递归 保存每次该层的结果currRes,将每层的结果值不停的往下传,这样每次调用就不需要保存上一层的调用信息了
    // @tailrec 可以确保写出的是一个尾递归,如果不是会报错
    @tailrec
    def loop(n: Int, currRes: Int): Int = {
      if (n == 0) return currRes
      loop(n - 1, currRes * n)
    }
    //从1开始算阶乘
    loop(n, 1)
  }
}

3.4 控制抽象

控制抽象

  • 强调的是参数的调用方式
    • 传值调用:将一个确定的值当做参数传递。
    • 传名调用:将代码块当做参数传递。
object Test11_ControlAbstraction {
  def main(args: Array[String]): Unit = {
    // 1. 传值参数
    def f0(a: Int): Unit = {
      println("a: " + a)
      println("a: " + a)
    }
    f0(23)

    def f1(): Int = {
      println("f1调用")
      12
    }
    f0(f1())

    println("========================")

    // 2. 传名参数,传递的不再是具体的值,而是代码块
    // a: =>Int 该入参可以是代码块,返回值是int 
    def f2(a: =>Int): Unit = {
      //如果a是代码块,那么a每出现一次就执行一遍对应的代码块
      println("a: " + a)
      println("a: " + a)
    }

    f2(23)
    f2(f1())

    f2({
      println("这是一个代码块")
      29
    })

  }
}

控制抽象传名参数的特性丰富了scala的功能,比如利用该特性实现自定义关键字。

object Test12_MyWhile {
  def main(args: Array[String]): Unit = {
    var n = 10

    // 1. 常规的while循环
    while (n >= 1){
      println(n)
      n -= 1
    }

    // 2. 用闭包实现一个函数,将代码块作为参数传入,递归调用  入参 返回值 都是代码块
    def myWhile(condition: =>Boolean): (=>Unit)=>Unit = {
      // 内层函数需要递归调用,参数就是循环体
      def doLoop(op: =>Unit): Unit = {
        if (condition){
          op
          myWhile(condition)(op)
        }
      }
      doLoop _
    }

    println("=================")
    n = 10
    myWhile(n >= 1){
      println(n)
      n -= 1
    }

    // 3. 用匿名函数实现
    def myWhile2(condition: =>Boolean): (=>Unit)=>Unit = {
      // 内层函数需要递归调用,参数就是循环体
      op => {
        if (condition){
          op
          myWhile2(condition)(op)
        }
      }
    }
    println("=================")
    n = 10
    myWhile2(n >= 1){
      println(n)
      n -= 1
    }

    // 3. 用柯里化实现
    def myWhile3(condition: =>Boolean)(op: =>Unit): Unit = {
      if (condition){
        op
        myWhile3(condition)(op)
      }
    }

    println("=================")
    n = 10
    myWhile3(n >= 1){
      println(n)
      n -= 1
    }
  }
}

3.5惰性加载

惰性加载

  • 当函数返回值被声明为 lazy 时,函数的执行将被推迟,直到我们首次对此取值,该函 数才会执行。
  • 即推迟函数的执行时期,只有第一次需要调用的时候才触发其逻辑。
object Test13_Lazy {
  def main(args: Array[String]): Unit = {
    //惰性加载和控制抽象差不多,只是控制抽象将代码块给入参,而惰性加载将代码块给val
    lazy val result: Int = sum(13, 47)

    println("1. 函数调用")
    println("2. result = " + result)
    println("4. result = " + result)
  }

  def sum(a: Int, b: Int): Int = {
    println("3. sum调用")
    a + b
  }
}

输出

1. 函数调用
3. sum调用  //sum函数第一次调用
2. result = 60
4. result = 60   //sum函数只调用一次将值传给了常量result,后续不会再调用该函数

你可能感兴趣的:(Scala-函数式编程)