50、Scala编程:类型功能构建与纯函数实践

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)
  1. 移动方法 :将与网络请求和数据提取相关的方法移动到不同的对象中。
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 = { ... }
}
  1. 创建实例 :通过一系列表达式创建 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 编程,编写出高质量的代码。

内容概要:本文是一份针对2025年中国企业品牌传播环境撰写的《全网媒体发稿白皮书》,聚焦企业媒体发稿的策略制定、渠道选择效果评估难题。通过分析当前企业面临的资源分散、内容同质、效果难量化等核心痛点,系统性地介绍了新闻媒体、央媒、地方官媒和自媒体四大渠道的特点适用场景,并深度融合“传声港”AI驱动的新媒体平台能力,提出“策略+工具+落地”的一体化解决方案。白皮书详细阐述了传声港在资源整合、AI智能匹配、舆情监测、合规审核及全链路效果追踪方面的技术优势,构建了涵盖曝光、互动、转化品牌影响力的多维评估体系,并通过快消、科技、零售等行业的实战案例验证其有效性。最后,提出了按企业发展阶段和营销节点定制的媒体组合策略,强调本土化传播政府关系协同的重要性,助力企业实现品牌声量实际转化的双重增长。; 适合人群:企业市场部负责人、品牌方管理者、公关传播从业者及从事数字营销的相关人员,尤其适用于初创期至成熟期不同发展阶段的企业决策者。; 使用场景及目标:①帮助企业科学制定媒体发稿策略,优化预算分配;②解决渠道对接繁琐、投放不精准、效果不可衡量等问题;③指导企业在重大营销节点(如春节、双11)开展高效传播;④提升品牌权威性、区域渗透力危机应对能力; 阅读建议:建议结合自身企业所处阶段和发展目标,参考文中提供的“传声港服务组合”“预算分配建议”进行策略匹配,同时重视AI工具在投放、监测优化中的实际应用,定期复盘数据以实现持续迭代。
先展示下效果 https://pan.quark.cn/s/987bb7a43dd9 VeighNa - By Traders, For Traders, AI-Powered. Want to read this in english ? Go here VeighNa是一套基于Python的开源量化交易系统开发框架,在开源社区持续不断的贡献下一步步成长为多功能量化交易平台,自发布以来已经积累了众多来自金融机构或相关领域的用户,包括私募基金、证券公司、期货公司等。 在使用VeighNa进行二次开发(策略、模块等)的过程中有任何疑问,请查看VeighNa项目文档,如果无法解决请前往官方社区论坛的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得! 想要获取更多关于VeighNa的资讯信息? 请扫描下方二维码添加小助手加入【VeighNa社区交流微信群】: AI-Powered VeighNa发布十周年之际正式推出4.0版本,重磅新增面向AI量化策略的vnpy.alpha模块,为专业量化交易员提供一站式多因子机器学习(ML)策略开发、投研和实盘交易解决方案: :bar_chart: dataset:因子特征工程 * 专为ML算法训练优化设计,支持高效批量特征计算处理 * 内置丰富的因子特征表达式计算引擎,实现快速一键生成训练数据 * Alpha 158:源于微软Qlib项目的股票市场特征集合,涵盖K线形态、价格趋势、时序波动等多维度量化因子 :bulb: model:预测模型训练 * 提供标准化的ML模型开发模板,大幅简化模型构建训练流程 * 统一API接口设计,支持无缝切换不同算法进行性能对比测试 * 集成多种主流机器学习算法: * Lass...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值