Scala函数式编程详解

函数式编程强调使用纯函数和使用不可变类型值

Functional programming is a style of programming that emphasizes writing applications using only pure functions and immutable values.

一个中肯的建议是在应用程序中:核心代码用纯函数编写,与外部交互使用非纯函数

A common recommendation is to write the core of your application using pure functions, and then to use impure functions to communicate with the outside world.

函数式编程强调不对输入值做任何改变

1 纯函数 PURE FUNCTIONS

In Functional Programming, Simplified, Alvin Alexander defines a pure function like this:

  • The function’s output depends only on its input variables
  • It doesn’t mutate any hidden state
  • It doesn’t have any “back doors”: It doesn’t read data from the outside world (including the console, web services, databases, files,etc.), or write data to the outside world

As a result of this definition, any time you call a pure function with the same input value(s), you’ll always get the same result.

1.1 纯函数例子

类似 scala.math._ 包中的函数就是纯函数:

  • abs
  • ceil
  • max
  • min

类似地 Scala String 中的方法也是纯函数:

  • isEmpty
  • length
  • substring

Scala 中集合也有很多方法和纯函数工作方相似,包括 drop, filter, and map.

1.2 非纯函数

foreach 不是一个纯函数,因为只是用它来实现附加功能,比如输出

The foreach method on collections classes is impure because it’s only used for its side effects, such as printing to STDOUT.

A great hint that foreach is impure is that it’s method signature declares that it returns the type Unit. Because it returns nothing, logically the only reason you ever call it is to achieve some side effect. Similarly, any method that returns Unit is going to be an impure function.

Date and time related methods like getDayOfWeek, getHour, and getMinute are all impure because their output depends on something other than their input parameters. Their results rely on some form of hidden I/O, hidden input in these examples.

非纯函数或多或少有如下特点

In general, impure functions do one or more of these things:

  • 函数计算所利用的变量不仅仅来自于输入参数,
    Read hidden inputs, i.e., they access variables and data not
    explicitly passed into the function as input parameters
  • Write hidden outputs
  • 改变了输入变量
    Mutate the parameters they are given
  • 与外界有 I/O操作
    Perform some sort of I/O with the outside world

非纯函数是很有必要的,因为可以读取或将结果传递给外部

函数式程序应用汇将纯函数式编程和非纯函数编程相结合,以便和外部进行交互
FP applications have a core of pure functions combined with other functions to interact with the outside world.

1.3 写纯函数

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

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

2 函数作为变量传递 PASSING FUNCTIONS AROUND

Scala 的另一个特点就是你可以将函数作为变量传递给其他函数,就像是传递 Int 或 String 类性值一样。

将函数作为变量传递给其他函数,能使代码变得更简洁,也不会影响其可读性

例如集合中的 map, filter 方法传入的就是匿名函数

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)
// REPL 表示在交互式命令行演示
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)

任何匿名函数都可以声明之后,再传给函数

def toUpper(s: String): String = s.toUpperCase
List("foo", "bar").map(toUpper)
List("foo", "bar").map(s => toUpper(s))
List("foo", "bar").map(_.toUpperCase)

3 函数式编程中不会用 null 值 NO NULL VALUES

在 Java/OOP 编程中我们都会遇到空值,那 Scala 是如何处理空值的呢?

Scala 解决空值的办法就是使用像 Option/Some/None 等构造类

// 转换字符为 Int
def toInt(s: String): Int = {
    try {
        Integer.parseInt(s.trim)
    } catch {
        case e: Exception => 0
    }
}

这种方式存在的问题是,传入 “0” 返回的值是 0 , 传入 “foo” 或 “bar” 返回的值也是 0,这样你就不能判定你输入的是什么类型的值。

3.1 使用 Option/Some/None

Some 和 None 类是 Option 类的子类

解决上述问题的方法可以分为以下几步:

  1. 声明一个函数 toInt 返回类型为 Option
  2. 如果 toInt 接收一个 String 可以转为整数,就将这个整数写进 Some 中。you wrap the Int inside of a Some
  3. 如果 toInt 不能进行转换,就返回一个 None
def toInt(s: String): Option[Int] = {
    try {
        Some(Integer.parseInt(s.trim))
    } catch {
        case e: Exception => None
    }
}

当输入字符能够转换为整数,返回一个包含转换后数字的 Some, 不能转换就返回 None

This code can be read as, “When the given string converts to an integer, return the integer wrapped in a Some wrapper, such as Some(1). When the string can’t be converted to an integer, return a None value.”

// REPL 表示在交互式命令行演示
scala> val a = toInt("1")
a: Option[Int] = Some(1)

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

Option/Some/None 的本质就是可以以相同的方式处理异常和空值

As shown, the string “1” converts to Some(1), and the string “foo” converts to None. This is the essence of the Option/Some/None approach. It’s used to handle exceptions (as in this example), and the same technique works for handling null values.

3.2 使用返回 Option 类型的函数

toInt 返回的是 Option[Int],那该如何处理返回类型呢?

有两种主要的方式:使用 match 表达式; 使用 for-expression

match 表达式

// 如果 x 能转换为 Int, 第一个 case 会执行,如果不能转换,第二个 case 会执行
toInt(x) match {
    case Some(i) => println(i)
    case None => println("That didn't work.")
}

for/yield

将 3 个string 转换为整数,并将三个整数累加作为结果返回。

// 返回 Some(6)
val stringA = "1"
val stringB = "2"
val stringC = "3"
val y = for {
    a <- toInt(stringA)
    b <- toInt(stringB)
    c <- toInt(stringC)
} yield a + b + c

返回有两种情况,一种是全部能够转换就返回转换后累加值,若其中有一个值不能进行转换,就返回 None.

3.3 理解 Option

可以将 Option 类当做一个容器,在这个容器中可能有 0 个或 1 个元素

One good way to think about the Option classes is that they represent a container, more specifically a container that has either zero or one item inside:

  • Some 容器就表示里面有一个元素 Some is a container with one item in it

  • None 容器表示里面没有元素 None is a container, but it has nothing in it

3.4 使用 foreach

更进一步地可以将 Some 和 None 当做集合类对待。这样就可以将集合中的方法应用于 Some 和 None 中,包括 map, filter, foreach, etc.
Because Some and None can be thought of containers, they can be further thought of as being like collections classes.

// 输出 1, toInt("1") 返回 Some(1), foreach 清楚怎样取出 Some 中的值,将取出的值交给 println
toInt("1").foreach(println)

// 没有输出,不进行任何操作。toInt("x") 返回 None, foreach 方法知道 None 类型不包含任何值,所以 foreach 什么都没做
toInt("x").foreach(println)

再次强调,None 是一个空的容器

Again, None is just an empty container.

可以看出,Scala 可以以相同的方式处理 Option 对象,这也是 Option 对象的本质

toInt("1").foreach(println)
toInt("x").foreach(println)
// for/yield
val y = for {
    a <- toInt(stringA)
    b <- toInt(stringB)
    c <- toInt(stringC)
} yield a + b + c

对于分情况处理 Option 对象的地方也就是在 match 中:

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

3.5 替代 null 值

// 定义一个地址类, street2 是一个可选项,可能为 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"
)

对于可选项,一种明智的方式是将它定义为 Option

// street2 为 Option
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"
)

这样,就可以在值上使用 match 表达式、for 表达式 和 foreach 等方法了

3.6 替代方案

Try/Success/Failure 常用于与 文件、数据库和网络服务有交互的程序中,因为这些方法都比较容易抛出异常。

4 伴随对象 COMPANION OBJECTS

在下面 Pizza.scala 文件中,可以认为 Pizza object 是 Pizza class 的一个伴随对象

Pizza.scala, the Pizza object is considered to be a companion object to the Pizza class:

// Pizza.scala
class Pizza {
}

object Pizza {
}

伴随对象有几点好处,首先,类和它的伴随对象可以随意访问对方的私有变量和方法

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

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

4.1 创建类实例不用 new 关键字

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

不使用 new 关键字实例化对象的能力就来自于伴随对象,伴随对象会定义一个 apply 方法, Scala 解释器能够识别这类方法。过程类似于

val p = Person("Fred Flinstone")

// Scala 解释器会转换代码
val p = Person.apply("Fred Flinstone")
class Person {
    var name = ""
}

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

// 不用 apply
val p = Person("Fred Flinstone")

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

不用 apply 实例化的过程val p = Person("Fred Flinstone"): Scala 解释器在Person 之前没有发现 new 关键字,解释器就会在伴随类中去找 apply 方法,如果找不到 apply 方法,就会抛出解释器错误

创建多个构造器

在一个伴随类中可以创建多个 apply 方法,这下面的例子中还可以看到如何使用 Option 对象。

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

这样你就可以实例化 Person 类了

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

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

4.2 增加 unapply 方法

使用 apply 方法可以构造一个实例,unapply 方法会分解一个对象实例

Just as adding an apply method in a companion object lets you construct new object instances, adding an unapply lets you de-construct object instances.

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

object Person {
    def unapply(p: Person): String = s"${p.name}, ${p.age}"
}

定义一个 unapply 方法,接收一个 Person 参数,返回一个 String.

val p = new Person("Lori", 29)

scala> val result = Person.unapply(p)
result: String = Lori, 29

在伴随类中定义一个 unapply 方法就意味着定义了一个抽取对象中字段的方法

思考: 在 Person 类中定义一个返回 String 的函数就可以实现相似功能,为何要在伴随类中定义一个 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 result = Person.unapply(p)
result: (String, Int) = (Lori,29)

// 获得 Tuple 中的值
scala> val (name, age) = Person.unapply(p)
name: String = Lori
age: Int = 29
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值