Scala编程:类型功能构建与纯函数实践
类型相关功能构建
在Scala编程中,类型系统为我们提供了强大的功能。例如,由于
numeric.plus
方法为所有不同的数值类型实现,我们可以创建一个适用于
Int
、
Double
、
Float
等类型的
add
方法:
println(add(1, 1))
println(add(1.0, 1.5))
println(add(1, 1.5F))
这个
add
方法不仅能按预期对所有数值类型起作用,还具有类型安全性。如果尝试传入一个
String
类型,代码将无法编译:
// won't compile
add("1", 2.0)
另外,
makeHumanLikeThingSpeak
方法与
add
方法类似。它能让
Dog
类型发声,但由于
HumanLike
特质没有为
Cat
定义类似行为,当前该方法无法使用
Cat
实例。可以通过添加一个针对
Cat
类型的
speak
方法作为另一个隐式对象来解决这个问题,或者保持现有代码以防止
Cat
发声。
下面我们通过两个例子来进一步展示如何利用类型构建功能。
示例1:创建计时器
在Unix系统中,可以使用
time
命令(某些系统上是
timex
)来查看命令的执行时间:
$ time find . -name "*.scala"
该命令会返回
find
命令的结果以及执行所需的时间,这对于排查性能问题很有帮助。在Scala中,我们可以创建一个类似的计时器方法:
val (result, time) = timer(someLongRunningAlgorithm)
println(s"result: $result, time: $time")
计时器代码如下:
def timer[A](blockOfCode: => A) = {
val startTime = System.nanoTime
val result = blockOfCode
val stopTime = System.nanoTime
val delta = stopTime - startTime
(result, delta/1000000d)
}
这个
timer
方法使用了Scala的按名调用语法来接受一个代码块作为参数。通过声明返回类型为泛型类型参数,我们可以传入各种算法,包括不返回任何值的算法:
scala> val (result, time) = timer{ println("Hello") }
Hello
result: Unit = ()
time: Double = 0.544
或者是读取文件并返回迭代器的算法:
scala> def readFile(filename: String) = io.Source.fromFile(filename).getLines
readFile: (filename: String)Iterator[String]
scala> val (result, time) = timer{ readFile("/etc/passwd") }
result: Iterator[String] = non-empty iterator
time: Double = 32.119
示例2:编写自己的“Try”类
在Scala 2.10之前,没有
scala.util
包中的
Try
、
Success
和
Failure
类。我们可以创建自己的
Attempt
、
Succeeded
和
Failed
类来实现类似功能:
// version 1
sealed class Attempt[A]
object Attempt {
def apply[A](f: => A): Attempt[A] =
try {
val result = f
return Succeeded(result)
} catch {
case e: Exception => Failed(e)
}
}
final case class Failed[A](val exception: Throwable) extends Attempt[A]
final case class Succeeded[A](value: A) extends Attempt[A]
为了让这个API更有用,我们需要添加一个
getOrElse
方法:
// version 2
sealed abstract class Attempt[A] {
def getOrElse[B >: A](default: => B): B = if (isSuccess) get else default
var isSuccess = false
def get: A
}
object Attempt {
def apply[A](f: => A): Attempt[A] =
try {
val result = f
Succeeded(result)
} catch {
case e: Exception => Failed(e)
}
}
final case class Failed[A](val exception: Throwable) extends Attempt[A] {
isSuccess = false
def get: A = throw exception
}
final case class Succeeded[A](result: A) extends Attempt[A] {
isSuccess = true
def get = result
}
getOrElse
方法的签名很有趣:
def getOrElse[B >: A](default: => B): B = if (isSuccess) get else default
这里的
B >: A
是一个下界,表示类型参数
B
是
A
的超类型。
Scala编程最佳实践
在Scala编程中,有一些最佳实践可以帮助我们写出更符合Scala风格的代码。
应用层面
- 遵循80/20规则 :在应用设计层面,尝试将80%的应用程序写成纯函数,在这些函数之上添加一层薄的代码来处理I/O等操作。
- 学习“面向表达式编程” :这有助于提高代码的简洁性和可读性。
- 使用Actor类实现并发 :Scala的Actor类为并发编程提供了强大的支持。
- 将行为从类转移到更细粒度的特质 :可以参考Scala的可堆叠特质模式。
编码层面
- 学习编写纯函数 :纯函数不仅简化了测试,还提高了代码的可维护性。
- 学会将函数作为变量传递 :这是Scala函数式编程的重要特性之一。
-
学习使用Scala集合API
:了解最常见的类和方法,如
map、filter等。 -
优先使用不可变代码
:使用
vals和不可变集合可以避免许多潜在的错误。 -
避免使用
null关键字 :使用Option/Some/None和Try/Success/Failure类来处理可能的空值。 - 使用TDD和/或BDD测试工具 :如ScalaTest和specs2。
代码之外
- 学习使用SBT :它是Scala事实上的构建工具。
- 保持REPL会话打开 :在编码时不断进行小实验,有助于快速验证想法。
纯函数的概念与实践
在函数式编程中,纯函数是一个重要的概念。
引用透明性
如果一个表达式可以被其结果值替换而不改变程序的行为,那么这个表达式就是引用透明的(RT)。例如,假设
x
和
y
是应用程序某个作用域内的不可变变量,表达式
x + y
可以赋值给另一个变量
z
:
val z = x + y
在该作用域内,任何使用
x + y
的地方都可以用
z
替换,而不会影响程序的结果。
纯函数的定义
维基百科对纯函数的定义如下:
1. 给定相同的参数值,函数总是计算出相同的结果值。它不能依赖任何隐藏状态或值,也不能依赖任何I/O操作。
2. 计算结果不会导致任何语义上可观察到的副作用或输出,如可变对象的突变或输出到I/O设备。
简单来说,纯函数是引用透明的且没有副作用的。以下是一些纯函数的例子:
- 数学函数,如加法、减法、乘法。
-
String
类的
split
和
length
方法。
-
String
类的
to*
方法(
toInt
、
toDouble
等)。
- 不可变集合的方法,如
map
、
drop
、
take
、
filter
等。
而以下函数不是纯函数:
-
getDayOfWeek
、
getHour
或
getMinute
等方法,它们的返回值取决于调用时间。
-
getRandomNumber
函数。
- 读取用户输入或打印输出的函数。
- 写入或读取外部数据存储的函数。
从Java类转换为纯函数
下面我们通过一个
Stock
类的例子来展示如何将OOP类中的方法转换为纯函数。
// a poorly written class
class Stock (var symbol: String, var company: String,
var price: BigDecimal, var volume: Long) {
var html: String = _
def buildUrl(stockSymbol: String): String = { ... }
def getUrlContent(url: String):String = { ... }
def setPriceFromHtml(html: String) { this.price = ... }
def setVolumeFromHtml(html: String) { this.volume = ... }
def setHighFromHtml(html: String) { this.high = ... }
def setLowFromHtml(html: String) { this.low = ... }
// some dao-like functionality
private val _history: ArrayBuffer[Stock] = { ... }
val getHistory = _history
}
这个类存在一些问题,如所有字段都是可变的,所有
set
方法都会改变类的字段,
getHistory
方法返回一个可变的数据结构。
我们可以通过以下步骤来修复这些问题:
1.
分离概念
:将
Stock
和
StockInstance
分离为两个不同的
case class
。
case class Stock(symbol: String, company: String)
case class StockInstance(symbol: String,
datetime: String,
price: BigDecimal,
volume: Long)
- 移动方法 :将与网络请求和数据提取相关的方法移动到不同的对象中。
object NetworkUtils {
def getUrlContent(url: String): String = { ... }
}
object StockUtils {
def buildUrl(stockSymbol: String): String = { ... }
def getPrice(symbol: String, html: String): String = { ... }
def getVolume(symbol: String, html: String): String = { ... }
def getHigh(symbol: String, html: String): String = { ... }
def getLow(symbol: String, html: String): String = { ... }
}
object DateUtils {
def currentDate: String = { ... }
def currentTime: String = { ... }
}
-
创建实例
:通过一系列表达式创建
StockInstance实例。
val stock = new Stock("AAPL", "Apple")
val url = StockUtils.buildUrl(stock.symbol)
val html = NetUtils.getUrlContent(url)
val price = StockUtils.getPrice(html)
val volume = StockUtils.getVolume(html)
val high = StockUtils.getHigh(html)
val low = StockUtils.getLow(html)
val date = DateUtils.currentDate
val stockInstance = StockInstance(symbol, date, price, volume, high, low)
通过这些步骤,我们将原本存在问题的OOP类转换为了使用纯函数的代码,提高了代码的可维护性和可测试性。
综上所述,Scala的类型系统和函数式编程特性为我们提供了强大的工具,通过遵循最佳实践和编写纯函数,我们可以编写出更高效、更易维护的代码。
Scala编程:类型功能构建与纯函数实践
纯函数转换的流程图分析
为了更清晰地展示将
Stock
类从 OOP 风格转换为使用纯函数的过程,我们可以使用 mermaid 流程图来表示。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(定义原 Stock 类):::process
B --> C{分析问题}:::decision
C -->|存在可变字段、副作用方法| D(分离概念):::process
D --> E(定义 Stock 类):::process
D --> F(定义 StockInstance 类):::process
E --> G(移动方法):::process
F --> G
G --> H(定义 NetworkUtils 对象):::process
G --> I(定义 StockUtils 对象):::process
G --> J(定义 DateUtils 对象):::process
H --> K(创建 StockInstance 实例):::process
I --> K
J --> K
K --> L([结束]):::startend
从这个流程图中可以清晰地看到,整个转换过程从定义原
Stock
类开始,经过问题分析,然后进行概念分离,将不同的功能方法移动到不同的对象中,最后创建
StockInstance
实例完成转换。
纯函数与非纯函数的对比表格
为了更好地区分纯函数和非纯函数,我们可以通过表格来进行对比。
| 类别 | 特点 | 示例 |
|---|---|---|
| 纯函数 |
1. 给定相同参数,结果始终相同
2. 不依赖隐藏状态或 I/O 3. 无副作用 |
1. 数学函数:
+
、
-
、
*
2.
String
类方法:
split
、
length
、
toInt
3. 不可变集合方法:
map
、
filter
|
| 非纯函数 |
1. 结果依赖调用时间或外部状态
2. 涉及 I/O 操作 3. 有副作用 |
1. 时间相关方法:
getDayOfWeek
、
getHour
2. 随机数函数:
getRandomNumber
3. I/O 函数:读取用户输入、写入文件 |
通过这个表格,我们可以更直观地看到纯函数和非纯函数的区别,在编程过程中能够更准确地判断一个函数是否为纯函数。
最佳实践的应用案例
在实际的 Scala 项目中,遵循最佳实践可以带来很多好处。下面我们通过一个简单的案例来展示如何应用前面提到的最佳实践。
假设我们要开发一个简单的图书管理系统,该系统需要完成图书的添加、查询和显示功能。
应用层面实践
按照 80/20 规则,我们将大部分核心逻辑写成纯函数,只留一小部分处理 I/O 操作。例如,图书的添加和查询逻辑可以使用纯函数实现:
// 定义图书类
case class Book(id: Int, title: String, author: String)
// 定义图书管理类
object BookManager {
// 纯函数:添加图书
def addBook(books: List[Book], newBook: Book): List[Book] = {
newBook :: books
}
// 纯函数:根据标题查询图书
def findBooksByTitle(books: List[Book], title: String): List[Book] = {
books.filter(_.title.contains(title))
}
}
而图书的显示功能涉及到 I/O 操作,我们可以将其放在一个单独的模块中:
object BookDisplay {
def displayBooks(books: List[Book]): Unit = {
books.foreach(book => println(s"ID: ${book.id}, Title: ${book.title}, Author: ${book.author}"))
}
}
编码层面实践
-
使用纯函数
:上述的
addBook和findBooksByTitle方法都是纯函数,它们的结果只依赖于输入参数,没有副作用,便于测试和维护。 -
函数作为变量传递
:我们可以将
findBooksByTitle函数作为参数传递给其他函数,实现更灵活的功能。例如:
def searchBooks(books: List[Book], searchFunction: (List[Book], String) => List[Book], title: String): List[Book] = {
searchFunction(books, title)
}
val allBooks = List(Book(1, "Scala Programming", "Author1"), Book(2, "Java Programming", "Author2"))
val result = searchBooks(allBooks, BookManager.findBooksByTitle, "Scala")
-
使用不可变集合
:在
BookManager中,我们使用List作为图书的存储结构,List是不可变集合,避免了数据被意外修改的问题。 -
避免使用
null:使用Option类型来处理可能为空的情况。例如,如果查询结果为空,我们可以返回None:
def findBookById(books: List[Book], id: Int): Option[Book] = {
books.find(_.id == id)
}
代码之外的实践
-
使用 SBT
:在项目中使用 SBT 进行构建和管理依赖。在
build.sbt文件中添加必要的依赖:
name := "BookManagementSystem"
version := "1.0"
scalaVersion := "2.13.8"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.10" % Test
-
使用 REPL 进行实验
:在开发过程中,打开 REPL 会话,不断测试和验证代码。例如,在 REPL 中测试
addBook方法:
scala> val books = List(Book(1, "Book1", "Author1"))
books: List[Book] = List(Book(1,Book1,Author1))
scala> val newBook = Book(2, "Book2", "Author2")
newBook: Book = Book(2,Book2,Author2)
scala> val updatedBooks = BookManager.addBook(books, newBook)
updatedBooks: List[Book] = List(Book(2,Book2,Author2), Book(1,Book1,Author1))
总结
通过以上的案例和分析,我们可以看到 Scala 的类型系统和函数式编程特性为我们提供了强大的编程能力。在实际开发中,遵循最佳实践,如编写纯函数、使用不可变代码、避免使用
null
等,可以提高代码的可维护性、可测试性和可扩展性。同时,利用 Scala 的工具和特性,如 SBT 和 REPL,能够提高开发效率。希望通过本文的介绍,读者能够更好地掌握 Scala 编程,编写出高质量的代码。
超级会员免费看
960

被折叠的 条评论
为什么被折叠?



