molyeo 阅读(20) 评论(0)

函数

本篇主要讲解函数的声明、定义和调用,同时对scala中各种函数进行了实例说明。

scala中方法(methods)是类的一部分;函数(functions)则是一个对象,可以赋值给一个变量,即定义在类中的函数即为方法。

函数的声明

scala中函数的声明如下:

def functionName ([list of parameters]) : [return type]

如果没有使用等于号和函数体,则隐式定义为抽象函数。

函数的定义

def functionName ([list of parameters]) : [return type] = {
   function body
   return [expr]
}

如下,addInt函数定义了两个Int型变量相加,并返回其和。

object add {
   def addInt( a:Int, b:Int ) : Int = {
      var sum:Int = 0
      sum = a + b
      return sum
   }
}

函数的调用

Scala函数的调用主要通过如下标准形式调用:

functionName( list of parameters )  

如果使用对象实例调用某个函数,那么我们将使用类似于Java的点符号,如下所示 -

[instance.]functionName( list of parameters )  

上述addInt函数的调用示例如下:

object Demo {
   def main(args: Array[String]) {
      println( "Returned Value : " + addInt(5,7) );
   }
   
   def addInt( a:Int, b:Int ) : Int = {
      var sum:Int = 0
      sum = a + b

      return sum
   }
}

可变参数的函数

Scala允许用户将可变长度参数列表传递给函数。如下,打印字符串函数内部的args类型被声明为类型“String *”,实际上是Array [String]。

object Demo {
   def main(args: Array[String]) {
      printStrings("Hello", "Scala", "Python");
   }
   
   def printStrings( args:String* ) = {
      var i : Int = 0;
      
      for( arg <- args ){
         println("Arg value[" + i + "] = " + arg );
         i = i + 1;
      }
   }
}

命名参数的函数

在普通函数调用中,调用中的参数按被调用函数的参数顺序逐个匹配。命名参数允许您以不同的顺序将参数传递给函数。语法很简单,每个参数前面都有一个参数名称和一个等号。

object Demo {
   def main(args: Array[String]) {
      printInt(b = 5, a = 7);
   }
   
   def printInt( a:Int, b:Int ) = {
      println("Value of a : " + a );
      println("Value of b : " + b );
   }
}

传值调用和传名调用

scala解释器在解析函数参数时有两种方式:

  • 传值调用:先计算函数参数的值,后应用到函数内部,无特殊说明,通常scala为传值调用。
  • 传名调用:将未计算的函数参数直接应用到函数内部。即每次访问参数时,执行具体的代码块并计算值。

我们先看传值调用:

object CallByValueFunctionDemo {
  def main(args: Array[String]) {
    delayed(time())
  }

  def time() = {
    println("Getting time in milliseconds")
    System.currentTimeMillis()
  }

  def delayed(t: Long) = {
    println("In delayed method")
    println("Param: " + t)
  }
}

运行结果如下:

Getting time in milliseconds
In delayed method
Param: 1530449794437  

可以看到是先执行time函数,以确定delayed的输入参数t的值,然后再将参数代入计算。

传名调用示例如下:

object CallByNameFunctionDemo {
  def main(args: Array[String]) {
    delayed(time())
  }

  def time() = {
    println("Getting time in milliseconds")
    System.currentTimeMillis()
  }

  def delayed(t: => Long) = {
    println("In delayed method")
    println("Param: " + t)
  }

运行结果如下:

In delayed method
Getting time in milliseconds
Param: 1530450176853

可以看到先进入到delayed函数内部,执行打印In delayed method后,再通过time函数去计算参数t的值。
可以看到我们是通过在变量名和变量类型使用 => 符号来设置传名调用。

传值调用和传名调用在不同情况下,各有优势:

  • 如果函数内部多次重复使用输入参数时,传值调用由于输入时即已确定参数,在一定程度上可以避免重复计算,提高效率。
  • 如果函数内部没有使用到输入参数,传名调用则不用去计算输入参数的值,这种情况下传名调用效率则会较高。

具体何时用传值调用和传名调用,具体问题具体分析。

匿名函数

匿名函数在源码中称为函数字面量(function literals),运行的时候被实例化函数值对象(function values)。
如下,创建一个匿名函数,将名为x的Int变量加1,并传递匿名函数,将其保存至变量addOne

scala> val addOne = (x: Int) => x + 1
addOne: (Int) => Int = <function1>

scala> addOne(1)
res4: Int = 2

嵌套函数

scala允许我们在函数内部定义函数,在其他函数内部定义的函数称为局部函数。
如下我们用嵌套函数计算菲波那切数列的值,在函数factorial内部定义局部函数fact

object NestFunctionDemo{
  def main (args: Array[String]) {
    for(i:Int <-0 to 10){
      println( "Factorial of " + i + ": = " + factorial(i) )
    }
  }
  def factorial(i:Int): Int ={
    def fact(i:Int,accumulator:Int): Int ={
      if(i<=1){
        accumulator
      }else{
        fact(i-1,i*accumulator)
      }
    }
    fact(i,1)
  }
}

递归函数

递归在纯函数式编程中起着重要作用,Scala非常支持递归函数,递归意味着函数可以重复调用自身。
上面我们用嵌套函数计算斐波拉切数列,其在函数内部用变量accumulator记录了前面调用的累乘值,下面看看使用递归函数的实现。

object RecursionFunctionDemo{
  def main(args: Array[String]) {
    for(i:Int <-0 to 10){
      println( "Factorial of " + i + ": = " + factorial(i) )
    }
  }

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

其中上面的嵌套函数和递归函数依次输出斐波拉切数列的值:

Factorial of 0: = 1
Factorial of 1: = 1
Factorial of 2: = 2
Factorial of 3: = 6
Factorial of 4: = 24
Factorial of 5: = 120
Factorial of 6: = 720
Factorial of 7: = 5040
Factorial of 8: = 40320
Factorial of 9: = 362880
Factorial of 10: = 3628800

高阶函数

高阶函数指参数为函数或者返回结果值为函数的函数。
参数为函数的函数
如下apply函数接收另一个函数f和Int型变量v,并将函数f应用于v

object HighOrderFunctionDemo {
  def main(args: Array[String]) {
    println(apply(layout, 10))
  }

  def apply(f: Int => String, v: Int) = f(v)

  def layout[A](x: A) = s"[ ${x.toString} ]"
}

再看一个参数值为函数的例子,你们公司打算根据今年收益普调一下工资。效率一般则增加10%;效益很好,则大幅调整;效益巨好,则海量调整。

object SalaryRaiser {
  def main(args: Array[String]) {
    val salaries = List(10000.0, 15000.0, 30000.0)
    println(raise(salaries, 1))
  }

  def raise(salaries: List[Double], raiserMode: Int): List[Double] = {
    val salariesAfer = raiserMode match {
      case 0 => smallPromotion(salaries)
      case 1 => bigPromotion(salaries)
      case 2 => hugePromotion(salaries)
      case _ => salaries
    }
    salariesAfer
  }

  private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] =
    salaries.map(promotionFunction)

  def smallPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * 1.1)

  def bigPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * salary)
}

返回值为函数的函数
我们要根据请求协议、访问接口和具体的参数拼接完整的url,示例如下:

object HighOrderFunctionDemo2{
  def main(args: Array[String]) {
    val domainName = "www.example.com"
    def getURL = urlBuilder(ssl=true, domainName)
    val endpoint = "users"
    val query = "id=1"
    val url = getURL(endpoint, query)
  }
  def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
    val schema = if (ssl) "https://" else "http://"
    (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
  }
}

其中urlBuilder的返回类型(String, String) => String。即返回的匿名函数输入两个字符串并返回一个String,本例子中的具体匿名函数为(endpoint: String, query: String) => s"https://www.example.com/$endpoint?$query"

部分应用函数

当调用一个函数时,如果只发送部分参数,则会返回部分应用的函数。这允许我们先绑定一些参数,后续再绑定其余参数。
这里我们使用部分应用函数来改写一下拼接完整的url的例子,如下:

object PartiallyAppliedFunctionDemo {

  def main(args: Array[String]) {
    val domainName = "www.example.com"
    val ssl = true
    val urlBuilderWithDomainName = urlBuilder(ssl, domainName, _: String, _: String)

    val endpoint = "users"
    val query = "id=1"
    println(urlBuilderWithDomainName(endpoint, query))
  }

  def urlBuilder(ssl: Boolean, domainName: String, endpoint: String, query: String): String = {
    val schema = if (ssl) "https://" else "http://"
    s"$schema$domainName/$endpoint?$query"
  }

}

urlBuilder函数有4个参数,我们想多次调用该方法,其中认证方式和域名相同,接口和请求参数不同。在变量urlBuilderWithDomainName中,我们先绑定ssldomainName,用下划线占位符表示不绑定endpointquery

柯里化函数

简单直接的定义,就是有多个参数列表的函数就是柯里化函数,Currying化最大的意义在于把多个参数的function等价转化成多个单参数function的级联,方便做lambda演算。 在scala里,Currying化对类型推演也有帮助,scala的类型推演是局部的,在同一个参数列表中后面的参数不能借助前面的参数类型进行推演,Currying化以后,放在两个参数列表里,后面一个参数列表里的参数可以借助前面一个参数列表里的参数类型进行推演。

我们应用柯里化函数改写拼接完整url的例子,如下:

object PartiallyAppliedFunctionDemo {

  def main(args: Array[String]) {
    val domainName = "www.example.com"
    val ssl = true
    val urlBuilderWithDomainName = urlBuilder(ssl, domainName, _: String, _: String)

    val endpoint = "users"
    val query = "id=1"
    println(urlBuilderWithDomainName(endpoint, query))
  }

  def urlBuilder(ssl: Boolean, domainName: String, endpoint: String, query: String): String = {
    val schema = if (ssl) "https://" else "http://"
    s"$schema$domainName/$endpoint?$query"
  }

}

结合本文,可以看到scala中函数相当灵活,上面拼接url网址的功能我们分别用高阶函数、部分应用函数、柯里化函数实现了一次。实际项目中具体编码,看实际应用需求。

本文参考内容如下:
https://docs.scala-lang.org/
https://www.scala-lang.org/api/current/
http://twitter.github.io/scala_school/zh_cn/index.html
http://twitter.github.io/effectivescala/index-cn.html