函数式编程

Scala让你以面向对象的风格,函数式的风格,或两者混合的风格 来写代码。

函数式编程是一种编程风格,只用纯函数和不可变值来写程序。这是一个很大的话题,很难一下全部说清,下面的部分带你稍微看一下FP,并看看Scala是提供了怎样的工具给开发者来进行函数式编程。

纯函数

Scala提供的帮助你写函数式代码的第一个特性就是写纯函数的能力,有人定义纯函数是:

  • 函数的输出只取决于它的输入值
  • 它不会改变任何隐藏的状态
  • 它没有任何“后门”:它不从外部世界(包括命令行,web服务器,数据库,文件等)读数据,或写数据带外部世界。
纯函数的例子

根据上面的定义,可以看到 scala.math._ 包里的一些方法是纯函数:

  • abs
  • ceil
  • max
  • min
    字符串中的一些方法也是纯函数:
  • isEmpty
  • length
  • substring
    甚至一些集合中提供的方法也是纯函数:
  • drop
  • filter
  • map
不是纯函数的例子

集合中的 foreach 方法不是穿函数,因为我们使用它是为了它的副作用。

	forecah 的类型签名中也暗示了它不是纯函数,它的返回类型为 Unit,
	也就是说我们为了它的副作用才使用它,例如打印到控制台。
	相应的,其他任何返回类型为Unit的方法,都不是纯函数。

日期和时间相关的方法,像getDayOfWeek, getHour, 和getMinute,因为它们的输出取决于它们隐藏的I/O,而不是它们的输入参数。
一般来说,非纯函数会做下面的一项或几项事情:

  • 读取隐藏的输入,就是它们访问的数据或变量不是明确的作为参数传进来的。
  • 写出某些内部产生的结果
  • 改变传入的参数
  • 与外界进行某种I/O

但还是需要非纯函数的

当然,如果程序不能对外部世界读取和输入那就不是很有用了。有人作如下评价:

用纯函数写程序的核心,然后写非纯函数把核心包起来与外界进行交互。
如果你喜欢用食物类比,就像是在纯蛋糕外面裹上不纯的糖衣。

有很多方法可以使与外界不纯的交互变得更纯一些。例如,你会听说有像IO Monad 这样的东西来除了用户输入,文件,网络和数据库。但最终,FP 应用程序是纯函数的核心与其他跟外界进行交互的函数进行结合而构成的。

写纯函数

在Scala中写纯函数是函数式编程中很一个简单的部分。你只需要用方法语法来写纯函数。

def double(i: Int): Int = i * 2

递归:

def sum(list: List[Int]): Int = list match {
     
    case Nil => 0
    case head :: tail => head + sum(tail)
}

再次明确纯函数的定义

它的输出结果只取决于它声明的输入和内部的算法,它不读取任何来自“外部世界”的值(函数作用域之外),也不会改变任何外界的值。

传递函数

虽然其他语言可能使你能够写纯函数,但是Scala对于函数式编程的第二个很好的特性是,使你能够像定义变量一样定义函数,就像创建String 和 Int 类型的变量。这个特性有很多好处,最常见的就是使你能够把函数作为参数传给其他函数。

val nums = (1 to 10).toList

val doubles = nums.map(_ * 2)
val lessThanFive = nums.filter(_ < 5)

在上面的例子中,匿名函数作为参数传递进去。
也与下面的实现方式一样的效果:

def double(i: Int): Int = i * 2   //a method that doubles an Int
val doubles = nums.map(double)

这就是技术名词上说的,高阶函数:一个函数接收另一个函数作为输入参数。

函数 或 方法?

scala使用 def 语法来定义函数也是更受欢迎的:

  • def语法使得来自java,C#等语言的人会 感到更熟悉
  • 你可以使用 def方法 就像它们是 val 值
    第二条的意思就是:你用def定义了一个方法:
def double(i: Int): Int = i * 2

你可以把这个方法当做变量一样传递给方法:

val x = ints.map(double)
                 ------

这样的形式可以使得你的代码简洁又可读性好。

示例

List("foo", "bar").map(_.toUpperCase)
List("foo", "bar").map(_.capitalize)
List("adam", "scott").map(_.length)
List(1,2,3,4,5).map(_ * 10)
List(1,2,3,4,5).filter(_ > 2)
List(5,1,3,11,7).takeWhile(_ < 6)

没有 null 值

函数式编程就像写很多代数方程,你不会在代数方程里使用空值,你也不会再函数式编程中使用空值。这带来了一个有趣的问题:在Java的面向对象编程时,你会经常使用 null 值,你是在做什么?

Scala的解决方式是构造 Option/None/Some 类。

小示例

假设 你要对接收到的 数字字符串 转换为数值类型,但或许会遇到非数值的字符串,这样就会报异常,你首先或许会这样处理:

def toInt(s: String): Int = {
     
    try {
     
        Integer.parseInt(s.trim)
    } catch {
     
        case e: Exception => 0
    }
}

但你当你接收到的“0” 或 “foo” 时,就会产生误解。

使用Option/Some/None

Some和None 是Option的子类。

def toInt(s: String): Option[Int] = {
     
    try {
     
        Some(Integer.parseInt(s.trim))
    } catch {
     
        case e: Exception => None
    }
}
scala> val a = toInt("1")
a: Option[Int] = Some(1)

scala> val a = toInt("foo")
a: Option[Int] = None

你会发现这种方法的使用贯穿整个库类和第三方类库

如何处理返回的值

方法一:

toInt(x) match {
     
    case Some(i) => println(i)
    case None => println("That didn't work.")
}

方法二:

val stringA = "1"
val stringB = "2"
val stringC = "3"
scala> val y = for {
     
     |     a <- toInt(stringA)
     |     b <- toInt(stringB)
     |     c <- toInt(stringC)
     | } yield a + b + c
y: Option[Int] = Some(6)

当年改变任何一个值为非数值的字符串,结果会:

y: Option[Int] = None

可以把Option看做一个盒子,None表示里面没有东西,Some表示里面有一个东西。

使用Option取代null

class Address (
    var street1: String,
    var street2: String,
    var city: String, 
    var state: String, 
    var zip: String
)
val santa = new Address(
    "1 Main Street",
    null,               // <-- D'oh! A null value!
    "North Pole",
    "Alaska",
    "99705"
)

改善后:

class Address (
    var street1: String,
    var street2: Option[String],
    var city: String, 
    var state: String, 
    var zip: String
)
val santa = new Address(
    "1 Main Street",
    None,
    "North Pole",
    "Alaska",
    "99705"
)
val santa = new Address(
    "123 Main Street",
    Some("Apt. 2B"),
    "Talkeetna",
    "Alaska",
    "99676"
)

伴生对象

伴生对象是一个 object , 与 class 在同一个文件中,且同名。

class Pizza {
     
}

object Pizza {
     
}

伴生对象和它的类可以互相访问彼此的私有成员(字段或方法)。

class SomeClass {
     
    def printFilename() = {
     
        println(SomeClass.HiddenFilename)
    }
}

object SomeClass {
     
    private val HiddenFilename = "/tmp/foo.bar"
}

不使用new创建一个新的实例

你可以注意到:

val zenMasters = List(
    Person("Nansen"),
    Person("Joshu")
)

上面的效果就是来自List的伴生对象。
当在伴生对象中定义apply方法时,Scala编译器会做特殊处理。

class Person {
     
    var name = ""
}

object Person {
     
    def apply(name: String): Person = {
     
        var p = new Person
        p.name = name
        p
    }
}
val p = Person("Fred Flinstone")

编译器会把上面的代码变成下面这样:

val p = Person.apply("Fred Flinstone")

创建多个构造器

class Person {
     
    var name: Option[String] = None
    var age: Option[Int] = None
    override def toString = s"$name, $age"
}

object Person {
     

    // a one-arg constructor
    def apply(name: Option[String]): Person = {
     
        var p = new Person
        p.name = name
        p
    }

    // a two-arg constructor
    def apply(name: Option[String], age: Option[Int]): Person = {
     
        var p = new Person
        p.name = name
        p.age = age
        p
    }

}
val p1 = Person(Some("Fred"))
val p2 = Person(None)

val p3 = Person(Some("Wilma"), Some(33))
val p4 = Person(Some("Wilma"), None)

添加unapply方法

class Person(var name: String, var age: Int)

object Person {
     
    def unapply(p: Person): String = s"${p.name}, ${p.age}"
}
val p = new Person("Lori", 29)
scala> val result = Person.unapply(p)
result: String = Lori, 29

在伴生对象中添加unapply方法,意味着你创建了一个提取器方法,从对象中提取字段。

unapply 可以返回任何类型:
例如:

class Person(var name: String, var age: Int)

object Person {
     
    def unapply(p: Person): Tuple2[String, Int] = (p.name, p.age)
}
scala> val (name, age) = Person.unapply(p)
name: String = Lori
age: Int = 29

你可能感兴趣的:(Scala,Book,scala)