Scala函数式编程

一.Scala语言特性

1)面向对象编程

  • 解决问题时,分解对象、行为、属性,然后通过对象的关系以及行为的调用来解决问题。

例如:

对象:用户
行为:登录、连接JDBC、读取数据库
属性:用户名、密码

Scala语言是一个完全面向对象编程语言:万物皆对象

  • 对象的本质:对数据和行为的一个封装

2)函数式编程

  • 解决问题时,将问题分解成一个一个的步骤,将每个步骤进行封装(函数),通过调用这些封装好的步骤,解决问题。

例如:

步骤:请求->用户名、密码->连接JDBC->读取数据库

Scala语言是一个完全函数式编程语言:万物皆函数

  • 函数的本质:函数可以当做一个值进行传递

3)在Scala中函数式编程和面向对象编程完美融合在一起了


二.Scala函数基础

1.函数和方法的区别

两者区别:

  • 1)函数:完成某个特殊功能的代码块
  • 2)函数和方法定义的位置不一
    • 函数:定义在方法或者函数内部
    • 方法:定义在类下的函数,叫方法
  • 3)函数没有重载和重写的概念、而方法可以进行重载和重写
  • 4)Scala中函数可以嵌套定义,且可以在任何的语法结构中声明任何的语法

案例:

object TestFunction {
    // (2)方法可以进行重载和重写,程序可以执行
    def main(): Unit = {

    }

    def main(args: Array[String]): Unit = {
        // (1)Scala语言可以在任何的语法结构中声明任何的语法
        import java.util.Date
        new Date()

        // (2)函数没有重载和重写的概念,程序报错
        def test(): Unit ={
            println("无参,无返回值")
        }
        test()

        def test(name:String):Unit={
            println()
        }

        //(3)Scala中函数可以嵌套定义
        def test2(): Unit ={
            def test3(name:String):Unit={
                println("函数可以嵌套定义")
            }
        }
    }
}

2.函数基本语法

def 函数名(参数列表):返回值类型={
	函数体
}

在这里插入图片描述

函数类型参数*

其中函数的参数类型也可以是函数类型:

  • 函数类型:(参数类型) => 返回值
  • 当参数只有一个时,小括号可以省略
def f9 (函数类型参数): Unit = {}

def f9 (f:Function): Unit = {
	1)f:Function:如果使用类Function来限定函数类型参数,并不能很好的表示该参数(如参数列表与返回值的信息)
	2)应该使用的函数类型参数 f: (参数列表声明) => 返回值
}
//f9有一个函数类型参数 f :其无返回值、参数类型为String
def f9(f: String => Unit) = {
  f("zxy")
}

3.函数参数

1)可变参数:加星号 (如 s : String*

  • 在一个参数列表中,至多只能有一个可变长参数
  • 如果一个参数列表中,有多个参数,应该将可变长参数放到最后
  • 有输入参数时:输出 WrappedArray(Hello, Scala)
  • 无输入参数时:输出 List()

2)参数默认值

  • 在声明函数的时候,函数的形参可以赋默认值
    • age : Int = 30
  • 如果参数有默认值,那么在调用函数的时候,有默认值的参数可以不用传参,使用默认值作为参数
  • 如果参数有默认值,那么在调用函数的时候,也可以传递参数,会用传递的参数替换默认值作为参数

3)带名参数:消除默认值参数位置引起的歧义

  • 调用时指定参数名:test(age = 18)
object TestFunction {
    def main(args: Array[String]): Unit = {
        // (1)可变参数
        def test( s : String* ): Unit = {
            println(s)
        }
        // 有输入参数:输出 WrappedArray(Hello, Scala)
        test("Hello", "Scala")
        // 无输入参数:输出List()
        test()
		/*输出结果:
		WrappedArray(Hello, Scala)
		List()
		*/
		
        // (2)如果参数列表中存在多个参数,那么可变参数一般放置在最后
        def test2( name : String, s: String* ): Unit = {
            println(name + "," + s)
        }
        test2("jinlian", "dalang")

        // (3)参数默认值
        def test3( name : String, age : Int = 30 ): Unit = {
            println(s"$name, $age")
        }
        // 如果参数传递了值,那么会覆盖默认值
        test3("jinlian", 20)
        // 如果参数有默认值,在调用的时候,可以省略这个参数
        test3("dalang")
        // 一般情况下,将有默认值的参数放置在参数列表的后面
        def test4( sex : String = "男", name : String ): Unit =      {
            println(s"$name, $sex")
        }
		// Scala函数中参数传递是,从左到右
        test4("wusong") //是把wusong赋给了sex

        //(4)带名参数
        test4(name="ximenqing")
    }
}

4.函数至简原则*

至简原则:能省则省

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

1)前八类案例

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

    // 函数标准写法:
    def f( s : String ): String = {
        return s + " world!"
    }
    println(f("Hello"))

    //应用至简原则:能省则省

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

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


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

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

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

    //(6)Scala如果期望是无返回值类型,可以省略等号
    // 将无返回值的函数称之为过程
    def f6() {
        "Hello6"
    }
    println(f6())

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

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

2)匿名函数*

第九类:如果不关心函数名称,只关心函数的处理逻辑,那么函数名(def)可以省略

  • 一般用匿名函数来简化操作:(x:Int) => {函数体}

匿名函数的作用:

  • 扩展被传入函数的功能:如案例中f10()扩展了f9()的功能
  • 具体扩展的功能是通过该匿名函数传递,灵活性高

匿名函数的简化:

  • 1)参数的类型可以省略,会根据形参进行自动的推导
  • 2)类型省略之后,发现只有一个参数,则圆括号可以省略;
    • 其他情况:没有参数和参数超过1的永远不能省略圆括号
  • 3)匿名函数如果只有一行,则大括号也可以省略
  • 4)如果参数只出现一次,则参数省略且后面参数可以用_代替
    • 当有多个参数被_替换时,处理逻辑中参数的顺序不能改变

注意:如果匿名函数输入和输出相同,那么不能简化

  • 例如:spark算子中的flatmap

    val rdd = sc.makeRDD(List(List(1, 2), List(3, 4), List(5, 6), List(7)), 2)
    
    //以下两个变量方式都是通过.var自动生成的,可以看出第二种并没有很好的识别出传入参数的类型,也就不能生成正确的RDD
    val newRdd: RDD[Int] = rdd.flatMap(list => list)
    val newRdd: (List[Int] => TraversableOnce[U_]) => RDD[Nothing] = rdd.flatMap(_)
    
    // 3.3 打印修改后的RDD中数据
    newRdd.collect().foreach(println)
    

案例1:传递的函数有1个参数

object Test_Function {

  def main(args: Array[String]): Unit = {
  	//函数标准写法:
    //f9有一个函数式参数 f :其无返回值、参数类型为String
    def f9(f:(String)=>Unit) = {
      f("zxy")
    }
    def f10(s:String) : Unit = {
      println(s)
    }
    //f9中:调用f10
    f9(f10)   

	//开始简化:

    //f9(f10)其中函数式参数f10,可以使用匿名函数表示:
    //(0)使用匿名函数替代f10 :(x:Int)=>{函数体}
    f9((s:String) => {println(s)})
    //(1)参数的类型可以省略,会根据f9的形参定义进行自动的推导
    f9((s) => {println(s)})
    //(2)类型省略之后,发现只有一个参数,则圆括号可以省略;
    // 其他情况:没有参数和参数超过1的永远不能省略圆括号。
    f9(s => {println(s)})
    //(3)匿名函数如果只有一行,则大括号也可以省略
    f9(s => println(s))
    //(4)如果参数只出现一次,则参数省略且后面参数可以用_代替
    f9(println(_))

	//以上函数调用都输出:
	//zxy
  }
}

案例2:传递的函数有两个参数

object Test_Function {
  def main(args: Array[String]): Unit = {
	// 当有多个参数被_替换时,处理逻辑中参数的顺序不能被识别
	def f11(fun : (Int, Int) => Int) : Int = {
		fun(1,2)
	}

	println(f11((x:Int,y:Int) => {x + y}))
	//参数类型可以根据实参推到出来
	println(f11((x,y) => {x + y}))
	//只有一行代码:
	println(f11((x,y) => x + y))
	//参数只出现一次:但x\y的顺序不能被改变
	println(f11(_ + _))
  }
}

三.Scala函数高级

1.函数的高阶用法

在Scala语言中,函数是一等公民,其有很多高级的用法:

1)函数可以作为值进行传递

  • 语法:在被调用函数后面加上空格 + _,相当于把函数当成一个整体,传递给变量fun
  • 如果明确了变量的数据类型,那么下划线可以省略
var fun = 函数名 _
var fun : (参数列表) => 返回值类型 = 函数名

案例:

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

    //(1)调用foo函数,把返回值给变量f
    val f = foo     //调用 foo(),并把Int返回值赋给变量 f
    println(f)

    //(2)在被调用函数foo后面加上 _,相当于把函数foo当成一个整体,传递给变量f1
    val f1 = foo _

    foo     //调用函数foo
//  f1      此时f1代表的是一个函数类型变量,并不是调用该函数。可以输出一下:
    println(f1)  //输出结果: <function0>
    f1()    //调用该函数则必须加()

    //(3)如果明确变量类型,那么不使用下划线也可以将函数作为整体传递给变量
    var f2:()=>Int = foo
    //var f2:()=>Int = foo()  报错:是将foo()的执行结果Int数据赋值给f2

    //f2不加括号是函数类型变量,其可以直接赋值给另外一个变量(f1也是)
    var f3 = f2
    f3()
  }

  def foo():Int = {
    println("foo...")
    1
  }
}

2)函数可以作为参数进行传递

函数可以作为参数进行传递,通过匿名函数

  • 扩展函数的功能
  • 提高函数的灵活度

案例:
//函数可以作为参数,进行传递(大多数情况都是通过匿名函数的形式)

//定义一个函数calculator,该函数接收一个函数类型的参数op
//  op定义了对两个整数要进行什么样的操作
def calculator(a:Int,b:Int,op:(Int,Int)=>Int): Int ={
  op(a,b)
}

//函数类型参数传递方式1:定义一个函数f1,完成两个整数的加法运算
def op(a:Int,b:Int):Int={
  a + b
}
println(calculator(10,20,op))

//函数类型参数传递方式2:使用匿名函数作为参数直接调用
println(calculator(50, 20, (a: Int, b: Int) => {
  a - b
}))

//函数类型参数传递方式3:对匿名函数进行简化
println(calculator(50, 20, _ - _))
println(calculator(50, 20, _ + _))

3)函数可以作为返回值进行返回

  • 场景:函数的嵌套使用
  • 函数链式调用,通过参数传递数据,在执行的过程中,函数始终占据栈内存,容易导致内存溢出
  • 这种情形Scala通过闭包解决,相应的还有柯里化

案例:

object ClosePackage_Function {

  def main(args: Array[String]): Unit = {
    def f1(): (Int) => Int = {
      var a = 10

      def f2(b: Int): Int = {
        a + b
      }
      // 返回一个函数类型变量
      f2 _
    }

    //执行f1函数,返回f2,将返回的f2赋佰给ff变量
    val fun : (Int) => Int = f1()

    //调用ff函数,其实就是调用f2
    println(fun(20))

    //也可以直接通过如下方式调用
    println(f1()(30))
  }
}

扩展:java中函数链式调用,缺点:

  • m1 -> m2 -> m3 -> m4:容易断链
  • 方法执行时会放在栈中,如果在大数据环境下,每个程序都会执行多个嵌套调用的函数,此时栈就会被海量的栈帧占用,耗费内存资源,极容易导-致栈溢出

2.闭包&柯里化

闭包:函数式编程的标配

什么是闭包:

上一节的案例中:

  • 内层函数 f2 要访向外层函数 f1 局部变量 a,当外层函数 f1 执行结束之后,f1 会释放栈内存(如果在java中f1的局部变量a也会被释放,f2就不能再调用a);
  • 但是scala会自动的延长 f1 函数的局部变量的生命周期,和内层函数 f2 形成一个闭合的效果,我们将这种闭合的效果称之为闭包

1)闭包:如果一个内层函数访问它的外部(局部)变量的值,会自动延长外层函数局部变量的生命周期,外层函数的局部变量与内层函数形成一个闭合的效果,这个函数和他所处的环境,称为闭包

  • 闭包 = 外层函数的局部变量 + 内层函数
  • 如果存在闭包,那么编译器会生成包含$anonfun$的字节码文件

首先以一个简单的例子开始:

class ClosureDemo {
  def func() = {
    var i = 2
    val inc: () => Unit = () => i = i + 1
    val add: Int => Int = (ii: Int) => ii + i
    (inc, add)
  }
}

在这个代码中,inc和add引用了func函数中的i变量,由于Scala中函数是头等值,因此inc和add将形成闭包来引用外部的i变量。

编译上述代码我们将得到三个class文件:

ClosureDemo.class 
ClosureDemo$$anonfun$1.class
ClosureDemo$$anonfun$2.class

这三个文件分别是ClosureDemo类自身和两个闭包,Scala会为每个闭包生成一个Class文件,如果嵌套过深,可能会出现特别长的类名,从而在Windows上引起一些路径过长的错误。

在Spark源码中的ClosureCleaner类中,我们可以看到这样的代码,用来判断这个类是不是闭包:

// Check whether a class represents a Scala closure
private def isClosure(cls: Class[_]): Boolean = {
    cls.getName.contains("$anonfun$")

扩展:深入理解 Scala 中的闭包(Closures)

2)函数柯里化:

  • 函数柯里化是将一个函数的一个参数列表中的多个参数,拆分为多个参数列表,简化闭包代码的编写
  • 函数柯里化,其实就是将复杂的参数逻辑变得简单化
  • 函数柯里化一定存在闭包
//原函数
def f1(): (Int)=>Int ={
   var a:Int = 10
   
   def f2(b:Int): Int ={
      a + b
   }   
   f2 _
}

//f2()函数可以转换为以下函数:返回值可以通过最后一行值推导出来
def f3() ={
   var a:Int = 10
   //需要指定参数类型,如果使用 _ + b 匿名函数的推导的参数类型为Any
   (b:Int) => a + b
}

println(f3()(30))

//函数柯里化,其实就是将复杂的参数逻辑变得简单化,函数柯里化一定存在闭包
//将一个函数的一个参数列表中的多个参数,拆分为多个参数列表,简化闭包代码的编写
def f4()(b:Int):Int = {
  var a:Int = 10
  a + b
}
println(f4()(20))

//f4在执行的时候,其实会转换为以下结构
def f4() ={
  var a:Int = 10
  (b:Int) => a + b
}

柯里化,是对函数嵌套的简化,但是这里只有一层函数体, 如果每层函数完成的业务逻辑不一样,一个函数体如何处理?

3.递归

一个函数/方法在函数/方法体内又调用了本身,我们称之为递归调用。

递归算法:

  1. 方法调用自身
  2. 方法必须要有跳出的逻辑
  3. 方法调用自身时,传递的参数应该有规律
  4. scala中的递归必须声明函数返回值类型

案例:计算阶乘

object jiecheng {
  def main(args: Array[String]): Unit = {
    println(test(5))
  }

  def test(i : Int) : Int = {
    if (i == 1) {
      1
    } else {
      i * test(i - 1)
    }
  }
}

4.抽象控制

1)值调用

  • 把计算后的值传递过去
object TestControl {
    def main(args: Array[String]): Unit = {
        def f() : Int = {
            println("f...")
            10
        }
        //此时调用f(),并将返回值10传递给foo()
        foo(f())
    }
    def foo(a: Int):Unit = {
        println(a)
        println(a)
    }
}
输出结果:
f...
10
10 

2)名调用:代码块

  • 把代码块传递过去
  • 代码块形参表示类型:=> 返回类型
object TestControl {
    def main(args: Array[String]): Unit = {
        def f(): Int = {
            println("f...")
            10
        }
        //此时传递f()的代码块部分给foo,其在foo中调用了两次
        foo(f())
    }

    def foo(a: =>Int):Unit = {//注意这里变量a没有小括号了
        println(a)
        println(a)
    }
}
输出结果:
f...
10
f...
10 

案例:自定义实现while循环

使用柯里化+名调用+递归实现:

  • mywhile (循环条件判断代码)(循环体代码) {递归调用方法体}
object Scala11_TestMyWhile {
  def main(args: Array[String]): Unit = {
    //while循环语句
    var n0 = 10
    while (n0 >= 1){
      println(n0)
      n0 -= 1
    }

    //1.使用柯里化+名调用+递归实现 定义: mywhile
    def mywhile(condition: =>Boolean)(op: =>Unit):Unit = {
      if(condition){
        op
        mywhile(condition)(op)
      }
    }
    // 调用 mywhile
    var n1 = 10
    mywhile(n>=1){
      println("mywhile-->" + n1)
      n1 -= 1
    }

    //2.直接使用普通的函数,实现while循环
    //柯里化好处1:将一个参数列表的多个参数,拆分为多个参数列表,这样参数所表示的含义,清晰、明确
    def mywhile(condition: =>Boolean,op: =>Unit): Unit ={
      if(condition){
        op
        mywhile(condition,op)
      }
    }

    var n2 = 10
    mywhile(n >=1,{
      println("mywhile-->" + n2)
      n2 -= 1
    })

    //3.使用闭包的形式,实现mywhile循环
    //外层函数的参数表示循环条件
    //柯里化好处2 :简化闭包的编写
    def mywhile(condition: =>Boolean): (=>Unit)=>Unit ={
      //内层函数参数表示循环体
      def ff(op: =>Unit): Unit ={
        if(condition){
          op
          mywhile(condition)(op)
        }
      }
      ff _
    }

    var n3 = 10
    mywhile(n3 >= 1){
      println("mywhile-->" + n3)
      n3 -= 1
    }
  }
}

柯里化的好处:

  • 1)将一个参数列表的多个参数,拆分为多个参数列表,这样参数所表示的含义,清晰、明确
  • 2)简化闭包的编写

5.惰性函数

当函数返回值被声明为lazy时,函数的执行将被推迟,直到我们首次用到,该函数才会执行。这种函数我们称之为惰性函数。

案例:

def main(args: Array[String]): Unit = {
    lazy val res = sum(10, 30)
    println("----------------")
    println("res=" + res)
}

def sum(n1: Int, n2: Int): Int = {
    println("sum被执行...")
    return n1 + n2
}
输出结果:
----------------
sum被执行...
res=40

注意:lazy不能修饰var类型的变量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值