Scala函数式编程【从基础到高级】

简介: Scala函数式编程【从基础到高级】

函数基础

       Scala 的函数式编程以及面向对象的特点,使它能够很好的应用到大数据场景下,比如 Spark、Kafka 的底层都是 Scala 编写的。

基本语法

       在Java中,方法只能够在类下面声明,不可以在main方法以及其它方法内部声明,但是Scala可以,因为Scala是一个函数式编程,函数是一等公民。

       所以,我们通常把main方法内部定义的方法叫做函数,main外部声明的方法才叫做对象的方法。

object Test{
    def main(args: Array[String]): Unit = {
    //定义函数
    def sayHello(name: String): Unit = {
      println("hello " + name)
    }
    //调用函数
    sayHello("GGBond")
    //调用对象的方法
    Test01_Function.sayHello("Tom")
  }
  //定义对象的方法
  def sayHello(name: String): Unit = {
    println("hello " + name)
  }
}

       可以看到,我们可以直接通过函数名调用函数,但是在伴生类中,一切类变量和方法都是静态的,所以我们需要通过类名才可以调用方法。

函数参数

  1. 可变参数
  2. 如果参数列表中存在多个参数,那么可变参数一般放置在最后
  3. 参数默认值,一般将有默认值的参数放置在参数列表的后面
  4. 带名参数
def main(args: Array[String]): Unit = {
    //1.可变参数
    def f1(names: String*): Unit = {
      println(names)
    }
    f1("alice","tom","GG bond")
    //2.如果参数列表中存在多个参数,那么可变参数一般放置在最后
    def f2(grade: String, names: String*): Unit = {
      println(grade)
      println(names)
    }
    f2("大二","alice","tom","GG bond")
    //3.参数默认值,一般将有默认值的参数放置在参数列表的后面
    def f3(name: String = "GG bond"): Unit = {
      println(name)
    }
    f3()
    f3("燕双鹰")
    //4.带名参数
    def f4(name: String, age: Int): Unit = {
      println(s"${name}的年龄=${age}")
    }
    f4("GG bond",14)
    f4(age = 13,name = "熊大")
  }

函数至简原则

代码能省则省

至简原则细节

  • return 可以省略,Scala 会使用函数体的最后一行代码作为返回值
  • 如果函数体只有一行代码,可以省略花括号
  • 返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
  • 如果有 return,则不能省略返回值类型,必须指定
  • 如果函数明确声明 unit,那么即使函数体中使用 return 关键字也不起作用
  • Scala 如果期望是无返回值类型,可以省略等
  • 如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
  • 如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
  • 如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
object Test{
  def main(args: Array[String]): Unit = {
    //(1)return 可以省略,Scala 会使用函数体的最后一行代码作为返回值
    def f0(name: String): String = {
      return name
    }
    println(f0("GG bond"))
    def f1(name: String): String = {
      name
    }
    println(f1("GG bond"))
    //(2)如果函数体只有一行代码,可以省略花括号
    def sum(a: Int,b: Int): Int = a+b
    //(3)返回值类型如果能够推断出来,那么可以省略(:和返回值类型一起省略)
    def add(a: Int,b: Int) = a+b  //因为参数都是Int类型,所以可以自己推断返回值类型
    //(4)如果有 return,则不能省略返回值类型,必须指定
    def f3(name: String): String = {
      return name
    }
    //(6)Scala 如果期望是无返回值类型,可以省略等号
    def f4(name: String){
      println("hello")
    }
    //(7)如果函数无参,但是声明了参数列表,那么调用时,小括号,可加可不加
    def f5() = "name"
    println(f5)
    //(8)如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
    def f6 = "hi"
    //(9)如果不关心名称,只关心逻辑处理,那么函数名(def)可以省略
    def say_Hi(name: String): Unit = {
      println("hi" + name)
    }
    //匿名函数 匿名函数的返回值是一个函数
    val say_Hello = (name: String) => {"hello " + name}
    //调用匿名函数
    println(say_Hello("GG bond"))
  }
}

函数高级

高阶函数

     高阶函数指的是接受一个或多个函数作为参数,并返回一个函数作为结果的函数。函数是Scala中的一等公民,地位很高,这体现在它可以在main方法中直接定义并通过函数名来直接使用,而不用向Java一样,需要再对象下面声明方法,来通过对象或者类名来调用。

高阶函数可以减少代码的重复,并增加代码的灵活性和可重用性。

       通常我们的业务数据在高阶函数中写死,等待传进来的函数参数来作处理。

函数的3种高阶用法:

  1. 函数作为值传递
  2. 函数作为参数传递
  3. 函数作为返回值
1、函数作为值传递

       函数作为返回值赋值给一个变量,这个变量类型是一个函数对象。如果直接输出的话,如果这个函数没有参数,将直接输出这个函数的返回值,否则直接输出这个对象的地址(对象引用)。我们也可以通过返回值来直接使用,因为返回值本身就是一个函数对象,我们可以直接+(参数)来调用函数。

def main(args: Array[String]): Unit = {
    //定义一个普通函数 输入num返回num+1
    def f(num: Int): Int = {
      num + 1
    }
    //1.函数作为值传递 -两种方式返回函数对象
    val f1: Int => Int = f
    val f2 = f _
    //我们可以直接输出函数对象,也可以直接利用函数对象进行使用传递参数
    println(f1)   //输出这个对象的地址(也叫对象引用)
    println(f1(9))
    println(f2)   //输出这个对象的地址(也叫对象引用)
    println(f2(10))
    //定义一个函数无参,直接返回1
    def fun(): Int = {
      1
    }
    val f3 = fun    //因为fun是无参的,所以这里返回的是返回值 1
    val f4 = fun _  //返回函数体
    val f5: () => Int = fun
    println(f3)   //输出 1
    println(f4)   //输出这个对象的地址(也叫对象引用)
    println(f5)   //输出这个对象的地址(也叫对象引用)
  }
2、函数作为参数传递

 我们可以定义两个函数,分别实现两个数的加和减,然后我们可以将这两个函数作为参数传递给另一个函数,来实现对两个数的处理(数据是写死的)。

    //定义一个函数 sum 求和
    def sum(a: Int,b: Int): Unit = {
      println(s"${a} + ${b} = ${a+b}")
    }
    //定义一个函数 sub 求差,函数体只有一行可以省去花括号{}
    def sub(a: Int,b: Int): Unit = println(s"${a} - ${b} = ${-b}")
    def fun(f: (Int,Int) => Unit):Unit = {
      f(1,2)
    }
    fun(sum)    //输出 1 + 2 = 3
    fun(sub)    //输出 1 - 2 = -1

       前面我们数据是写死的,也就是说,我们的高阶函数只能够对 1和2进行处理,显然很没用!这里我们直接将要计算的数据页当做参数传递进来。

def doEval(f: (Int,Int) => Int,a: Int ,b: Int): Int = {
      f(a,b)
    }
    def add(a: Int,b: Int): Int = {
      a + b
    }
    //使用普通函数作为参数
    doEval(add,1,2)
    //使用匿名函数做参数
    doEval((a,b) => a + b,1,2)
    //简化代码
    doEval(_ + _,1,2)
匿名函数作参数传递

我们可以直接将匿名函数作参数传入fun,更加简化代码。

//以函数作为参数
    def fun(f: (Int,Int) => Unit): Unit = f(1,2)
    //直接传入匿名函数
    fun((a: Int,b: Int) => println(s"${a} + ${b} = ${a+b}")))   //输出 1 + 2 = 3
    //高阶函数定义了函数参数的类型,匿名函数这里就不需要再指定函数类型了
    fun((a,b) => println(s"${a} - ${b} = ${a-b}")))   //输出 1 - 2 = -1
3、函数作为返回值传递

函数作为返回值后返回函数对象,我们可以通过函数对象来使用该函数。

//3.函数作为返回值传递
    def f6(): Int => Unit = {
      def f7(a: Int): Unit = {
        println("f7被调用 " + a)
      }
      f7  //返回函数对象
    }
    //获取f7函数对象 并使用
    val res  = f6()
    //输出:
    //  f7被调用 10
    //  ()
    //  因为f7的返回值为 Unit类型
    println(res(10))

匿名函数的至简规则

  1. 参数的类型可以省略,会根据形参进行自动的推导
  2. 类型省略之后,发现只有一个参数,则圆括号可以省略;其他情况:没有参数和参 数超过 1 的永远不能省略圆括号。
  3. 匿名函数如果只有一行,则大括号也可以省略
  4. 如果参数只出现一次,则参数省略且后面参数可以用下划线 _ 代替
    //定义一个函数,以函数作参数
    def say_Name(f: String => Unit): Unit = f("GG Bond")
    //普通调用
    say_Name((name: String) => {println(name)})
    //1.参数的类型可以省略,会根据形参进行自动的推导
    say_Name( (name) => {println(name)})
    //2.如果参数只有一个 括号省略
    say_Name( name => {println(name)} )
    //3.匿名函数如果只有一行,则大括号可以省略
    say_Name( name => println(name) )
    //4.如果参数只出现一次,则参数省略且后面参数可以用_代替
    say_Name( println(_) )
    //5.如果可以推断出当前传入的println()是一个函数体,而不是调用语句,可以直接省略_
    say_Name( println )

高阶函数的应用

高阶函数对集合的处理更加体现了Sala对于处理大数据的优势。

案例-将数组中的元素全部+1后返回
val arr: Array[Int] = Array(1,2,4,3,8)
    //对数组进行处理,把对数组的操作进行抽象
    /**
     *
     * @param array 待处理的数组
     * @param op 操作函数(Int => Int)将数组中的数处理后变为另一个数
     * @return 返回新数组
     */
    def arrayOperation(array: Array[Int], op: Int => Int): Array[Int] = {
      for (elem <- array) yield op(elem)
    }
    //定义操作函数
    def addOne(elem: Int): Int = elem + 1
    //调用函数
    val res_arr: Array[Int] = arrayOperation(arr,addOne)
    //用逗号间隔输出新数组的内容
    println(res_arr.mkString(","))
    //使用匿名函数实现
    val res2_arr = arrayOperation(arr,elem => elem + 1)
    //这里的后面的elem可以用下划线代替 _
    val res2_arr = arrayOperation(arr,elem => _ + 1)
    println(res2_arr.mkString(","))

匿名函数

前面已经用过了,这里在详细解释一下。

说明:没有函数名的函数就是匿名函数。

表达式:(a: Int,b: Int)=>{函数体}

注意匿名函数的定义没有返回值类型,但是可以作为高阶函数的参数时指定

//以函数作为参数
    def fun(f: (Int,Int) => Int): Unit = f(1,2)
    //直接传入匿名函数
    fun((a: Int,b: Int) => a + b ))   //返回 3
    //高阶函数中指定了参数函数的参数类似,所以这里可以省去匿名函数的参数类型
    fun((a,b) => a - b ))   //返回-1
    //如果每个参数只出现一次,可以这样写
    fun( _ + _)    //对应 a + b
    fun( _ - _)    //对应 a - b
    //如果要实现 b-a 可以写作 -a+b
    fun((a,b) => -a + b)    
    fun( -_ + _)    //对应 -a + b
练习1-匿名函数作为值传递:

定义一个匿名函数,并将它作为值赋给变量 fun。函数有三个参数,类型分别为 IntStringChar,返回值类型为 Boolean

要求:调用函数 fun(0, “”, ‘0’)得到返回值为 false,其它情况均返回 true

//匿名函数在没被高阶函数指定返回值类型的时候,需要声明参数名和类型
    val fun = (a: Int,b: String,c: Char) => {
      if (a == 0 && b == "" && c == '0')
        false
      else
        true
    }
    println(fun(1,"",'0'))    //true
练习2-函数作为返回值

       定义一个函数 func,它接收一个 Int 类型的参数,返回一个函数(记作 f1)。 它返回的函数 f1,接收一个 String 类型的参数,同样返回一个函数(记作 f2)。函数 f2 接 收一个 Char 类型的参数,返回一个 Boolean 的值。


       要求调用函数 func(0) (“”) (‘0’)得到返回值为 false,其它情况均返回 true。

//表示返回一个输入类型为String输出类型为(Char => Boolean)的函数
def func(a: Int): String => (Char => Boolean) = {
      //表示返回一个输入类型为Char输出类型为Boolean的函数
      def f1 (b: String): Char => Boolean = {
        //表示返回一个返回值类型为Boolean的值
        def f2(c: Char): Boolean = {
          if (a == 0 && b == "" && c == '0') false else true
        }
        f2
      }
      f1
    }
    println(func(0)("")('0'))    //false

使用匿名函数简写

函数柯里化&闭包

  • 闭包:如果一个函数 ,访问到了它外部变量的值,那么这个函数和它所处的环境,称为闭包
  • 函数柯里化:把一个参数列表的多个参数,变成多个参数列表。

      我们学习闭包和柯里化是为了更加简洁地实现调用上层函数的参数,因为在上面的案例2中,我们通过嵌套函数来实现读取上层函数的参数显然代码很复杂,所以我们可以使用柯里化来实现。

案例2的闭包实现:

//匿名函数简写
    def func1(a: Int): String => (Char => Boolean) = {
      //外层指定了返回值类型 内层的匿名函数就不需要指定参数类型
      b => c => if (a == 0 && b == "" && c == '0') false else true
    }
    println(func(1)("")('0'))    //tue

案例2的柯里化实现:

//柯里化
    def func2(a: Int)(b: String)(c: Char): Boolean = {
      if (a == 0 && b == "" && c == '0') false else true
    }
//调用
    println(func2(0)("")('0'))    //true

我们可以看到,柯里化更加简单,其实柯里化的底层就是闭包来实现的。

闭包案例 - 常量a+变量b

       这并不是一个简单的两数相加,因为实际开发中,我们可能遇到大量的变量对常量不断运算,比如大量数字分别对4相加、对5相加,这就需要定义两个函数addByFour、addByFive;但是如果常量有几百种呢,这就需要定义几百个函数,显然过于复杂。所以这就需要使用双层嵌套函数,以返回值为函数:第一层函数输入常量,第二层函数输入变量。

//A是任意常量
    def addByA(a: Int): Int=>Int = {
      def addB(b: Int): Int = {
        a + b
      }
      addB
    }
    //不断简化代码
//既然直接返回函数 我们直接定义一个匿名函数即可 scala默认返回最后一行(一个方法可以看作一行)作为返回值
    def addByA1(a: Int): Int=>Int = {
      (b: Int) => a + b
    }
    //指定外层函数指定了返回值类型 内层就不需要写输入类型了
    def addByA2(a: Int): Int=>Int = b => a + b
    //内层的变量 b 只在函数体出现了一次 所以可以用 _ 替换
    def addByA3(a: Int): Int=>Int = a + _
     //调用
    val fun = addByA3(5)    //接收一个函数(任意数+5)作为返回值 
    println(fun(10))    //调用函数 输出15

案例 - 柯里化

       在纯粹的函数式编程中其实是不存在多参数这样的定义的,只是Scala为了兼容其它语言才不做限制的。所以引入柯里化来实现这样的一个效果,将一个含多个参数的参数列表转变为多个仅含一个参数的参数列表。

       柯里化的底层就是闭包。

    //柯里化 底层是闭包实现分层调用
    def addCurrying(a: Int)(b: Int): Int = a + b
    //调用
    println(addCurrying(4)(36))

递归

斐波那契数列

def fb(i: Int): Int = {
      if (i ==1 || i == 2)
        1
      else
        fb(i-1) + fb(i+2)
    }

阶乘

def jieC(n: Int): Int = {
      if (n==1)
        1
      else
        jieC(n-1)*n
    }

尾递归

       我们知道,在做递归的时候,我们其实是方法不断调用,比如上面的阶乘案例,当我们希望得到5的阶乘时,函数的调用会不断压栈,到达阶乘的最小值1的时候原路返回。显然如果是10亿的阶乘,肯定会造成栈溢出的问题。

       所以这就需要引入尾递归的概念,上面的普通递归需要不断压栈最后出栈,比如5的阶乘,它的返回值是 1 -> 2*1 -> 3*2*1 -> 4*3*2*1 -> 5*4*3*2*1


       而我们的尾递归刚好相反,它的返回值是 5 -> 5*4 -> 5*4*3 -> 5*4*3*2 -> 5*4*3*2*1,这样,在调用函数时,不会创建新的栈帧,而是直接复用当前栈帧,因此占用的栈资源是固定的。

Scala实现尾递归求阶乘
def tailJieC(n: Int): Int = {
      def loop(n: Int,tmp: Int): Int = {
        if (n==0)
          return tmp
        loop(n - 1,tmp * n)
      }
      loop(n,1)
    }
Java实现尾递归求阶乘
public static int JC(int n){
        return loop(n,1);
    }
    public static int loop(int n,int tmp){
        if (n==0){
            return tmp;
        }else {
            return loop(n-1,tmp*n);
        }
    }

控制抽象

传值调用

参数类型:普通参数

println("=====传值调用======")
    //1.传值参数
    def f0(a: Int): Unit = {
      println("a = " + a)
    }
    //数值做参数
    f0(1)
    def f1(): Int = {
      println("f1被调用")
      2
    }
    //函数返回值做参数
    f0(f1())

输出结果

1
2

传名调用

参数类型:传递的不再是具体的值,而是代码块

    println("=====传名调用======")
    def f1(): Int = {
      println("f1被调用")
      2
    }
    //2.传名参数
    //f2的参数是代码块 要求代码块的返回值是Int
    def f2(a: => Int): Unit = {
      println("a = " + a)
      println("a = " + a) //a被调用几次 代码块就被执行几次
    }
    f2(23)    //输出两个23
    f2(f1())  //执行两次f1() 相当于把f2的函数体中的 a 都替换做 f1() 这也说明传名调用传递的是代码块而不是返回值
    f2({
      println("这是一个代码块")
      20
    })

输出结果

=====传名调用======
a = 23
a = 23
f1被调用
a = 2
f1被调用
a = 2
这是一个代码块
a = 20
这是一个代码块
a = 20

应用案例 - 自定义函数实现while  

//1.常规的while循环
    var n = 10
    while (n > 0){
      println(n)
      n -= 1
    }
    //2.自定义函数实现while
    // 用闭包实现一个函数,将代码块作为参数传入,递归调用
    def myWhile(condition: =>Boolean): (=>Unit)=>Unit = {
      //内层实现递归 参数就是循环体-代码块
      def doLoop(op: => Unit): Unit= {
        if (condition){
          op
          myWhile(condition)(op)
        }
      }
      doLoop _
    }
    //给n重新赋值
    n = 10
    myWhile(n >= 1)({
      println(n)
      n -=1
    })

简化代码

    //3.用匿名函数简化代码
    def myWhile2(condition: =>Boolean): (=>Unit)=>Unit = {
      //内层实现递归 参数就是循环体-代码块
      op => {
        if (condition){
          op
          myWhile2(condition)(op)
        }
      }
    }
    //4.用柯里化简化代码
    def myWhile3(condition: => Boolean)(op: =>Unit): Unit = {
      if (condition){
        op
        myWhile2(condition)(op)
      }
    }

惰性加载

       懒加载(Lazy Loading)是一种常见的优化策略,它可以延迟对象的初始化时间,减少应用程序的启动时间和内存消耗。在懒加载的策略下,只有在需要访问具体的对象时才会进行初始化。在Scala中,我们使用关键字 'lazy' 来实现懒加载。

lazy val res: Int = compute()
def compute(): Int = {
  // 进行复杂的计算
  Thread.sleep(1000)
  //返回计算结果
  100
}


相关文章
|
20天前
|
人工智能 安全 人机交互
Scala 05 —— 函数式编程底层逻辑
Scala讲座探讨了函数式编程的底层逻辑,强调无副作用和确定性。函数式编程的核心是纯函数,避免读写数据等副作用,将其移至代码边缘处理。函数输入输出应清晰定义,避免模糊参数。函数视为数据范畴间的映射,以范畴论为基础。业务逻辑转化为纯函数式,通过声明式编程实现解耦,关注输入输出而非过程,便于验证和自动编程。将业务逻辑视作流水线,每个函数处理数据,避免全局变量和`var`,优先确保正确性再优化效率。
13 1
Scala 05 —— 函数式编程底层逻辑
|
20天前
|
消息中间件 分布式计算 大数据
Scala学习--day03--函数式编程
Scala学习--day03--函数式编程
|
20天前
|
数据采集 监控 安全
通过Scala实现局域网监控上网记录分析:函数式编程的优雅之路
在当今数字时代,网络监控成为保障信息安全的必要手段之一。本文将介绍如何使用Scala编程语言实现局域网监控上网记录分析的功能,重点探讨函数式编程的优雅之路。通过一系列代码示例,我们将展示如何利用Scala的函数式特性和强大的语法来实现高效的监控和分析系统。
228 1
|
9月前
|
前端开发 Java 程序员
Scala高级用法 3
Scala高级用法
26 0
|
9月前
|
Java Scala
Scala高级用法 2
Scala高级用法
25 0
|
9月前
|
分布式计算 Java Scala
Scala高级用法 1
Scala高级用法
40 0
|
9月前
|
分布式计算 API Scala
Scala函数式编程
Scala函数式编程
49 0
|
分布式计算 Ubuntu Java
|
Java Scala
Scala的高级用法
Scala的高级用法
|
大数据 编译器 Scala
大数据开发基础的编程语言的Scala的函数式编程范式
Scala是一种支持函数式编程范式的编程语言,它允许开发者使用函数和不可变数据结构来实现程序逻辑。本文将介绍Scala中函数式编程范式的概念和用法,帮助开发者更好地理解和应用这门语言。
91 0