首页 > 其他分享 >【Scala系列】上下文参数一探究竟

【Scala系列】上下文参数一探究竟

时间:2024-08-31 21:55:18浏览次数:10  
标签:一探 通讯 String Scala 通知 参数 上下文 Sendable

【Scala系列】上下文参数一探究竟

文章目录

阅读须知

  1. 本文所有概念和代码都基于Scala3.x版本,其中部分代码在Scala2.x中无法使用。
  2. 阅读本文需要了解基础的Scala语法,以及函数的柯里化。
  3. 本文所有代码均已经过Scala3.4.2编译和运行验证。

什么是上下文参数

上下文参数,英文名context parameter,Scala提供这一特性以解决调用函数时入参样板代码过多的问题。这个特性通过标识特定入参为上下文参数,以启用编译器自动填充合适的参数最终完成函数调用。

模拟场景实例

代码文件结构先贴到最前面:

|contextparameter
|--BuzNotify.scala
|--Entity.scala
|--EntryPoint.scala

考虑这样一个场景:有一个通知组件,它支持以短信、邮件或企微通知的方式,给指定目标发送通知内容。我们可以先定义一套密封类表示该组件支持的渠道,以及一个通讯名册,将之定义到Entity.scala中:

// Entity.scala
// 可以使用的通知渠道
sealed trait NotifyChannel() {
  val describe: String = "未知渠道"
}
object SMS extends NotifyChannel {  // 短信
  override val describe: String = "短信渠道"
}
object Email extends NotifyChannel {  // 邮件
  override val describe: String = "邮件渠道"
}
object WxWorkGroup extends NotifyChannel {  // 企微群组
  override val describe: String = "企微群组渠道"
}

/**
 * 通讯名册
 * @param id              id标识
 * @param desc            通讯名册的描述
 * @param book            通讯名册。其中的元组定义为:姓名-通讯码-联系渠道
 */
case class ContactBook(id: String, desc: String, book: Seq[(String, String, NotifyChannel)])

我们支持3种通知:短信渠道、邮件渠道和企微群组渠道;我们有了一个通讯名册,定义了名册标识符、描述、包括姓名和联系方式在内的名册列表。

这个组件的核心通知方法,我们单独放在BuzNotify.scala中,以打印的方式简单模拟其真实实现,它应该是这样的:

// BuzNotify.scala
/**
 * @param content        通知内容
 * @param to             通讯名册
 * @param targetChannels 目标通知渠道
 * @return
 */
def notify1(content: String)(using to: ContactBook, targetChannels: Set[NotifyChannel]): Boolean =
  if targetChannels.isEmpty then return false
  to.book filter { t => targetChannels.contains(t._3)} foreach { (contactName, contactDest, channel) =>
    println(s"通过[${channel.describe}]给[${contactName}]发送通知\n通知目的地为${contactDest}\n通知内容为:${content}\n")
  }
  true

可以看到,我们在notify1方法中,接收三个参数:content-通知内容、to-要通知的通讯手册和targetChannels-通知的目标渠道。

这样一来,我们就可以尝试新建一个通讯名册,并对这个名册进行消息通知了,我将这段代码放置到EntryPoint.scala文件中:

// EntryPoint.scala
val buzDepContactBook1 = ContactBook(
  "1",
  "商务部门领导名册",
  Seq(("领导A", "12345678900", SMS), ("领导B", "19987654321", SMS), ("领导C", "[email protected]", Email)),
  Set(SMS, Email)
)

def invoke1(): Unit =
  // 经过一些业务后,给领导们发通知
  notify1(
    "今天晚上8点线上会议,请各位领导留意会议APP的通知,务必准时参加,谢谢~",
    buzDepContactBook1,
    Set(SMS)
  )
  // 又经过一些业务后,继续通知
  notify1(
    "A同志 今天晚上无故缺席会议,特此通报批评,希望各位领导遵守公司规章制度,谢谢~",
    buzDepContactBook1,
    Set(SMS)
  )

@main def main(args: String*): Unit =
  invoke1()

通常来说,使用notify1函数需要完整传入三个参数,比如上述例子中的会议通知调用和通报批评调用。可以看到,使用它的时候,我们会重复的传入buzDepContactBook1实例和一个Set(SMS)集合,未免稍显啰嗦。此时,我们就可以请出本文的主角登场:上下文参数

先看看改造后我们的调用,它变得简洁很多:

// BuzNotify.scala
/**
 * @param content        通知内容
 * @param to             通讯名册
 * @param targetChannels 目标通知渠道
 * @return
 */
def notify2(content: String)(using to: ContactBook, targetChannels: Set[NotifyChannel]): Boolean =
  if targetChannels.isEmpty then return false
  to.book filter { t => targetChannels.contains(t._3) } foreach { (contactName, contactDest, channel) =>
    println(s"通过[${channel.describe}]给[${contactName}]发送通知\n通知目的地为${contactDest}\n通知内容为:${content}\n")
  }
  true

// EntryPoint.scala
val buzDepContactBook1 = ContactBook(
  "1",
  "商务部门领导名册",
  Seq(("领导A", "12345678900", SMS), ("领导B", "19987654321", SMS), ("领导C", "[email protected]", Email)),
  Set(SMS, Email)
)
// 在此作用域内赋予Destination类型一个默认上下文buzDepLeaderDest1
given ContactBook = buzDepContactBook1
// 在此作用域内赋予Set[NotifyChannel]类型一个默认上下文Set(SMS)
given Set[NotifyChannel] = Set(SMS)

def invoke2(): Unit =
  // 经过一些业务后,给领导们发通知
  notify2("今天晚上8点线上会议,请各位领导留意会议APP的通知,务必准时参加,谢谢~")         // 只传一个参数即可
  // 又经过一些业务后,继续通知
  notify2("A同志 今天晚上无故缺席会议,特此通报批评,希望各位领导遵守公司规章制度,谢谢~")  // 只传一个参数即可

@main def main(args: String*): Unit =
  invoke2()

这段代码中,我们定义了一个新的通知函数notify2,并在使用这个函数时,省略了由using标识的两个参数。其中的要点有:

  • 对原有参数列表使用柯里化进行分割,对比notify1函数而言,notify2中的参数列表变为了两组,其中一组是(content: String),另一组是(to: ContactBook, targetChannels: Set[NotifyChannel])
  • 对在业务中只需初始化一次而需多次传参的参数,使用using关键字标记它们为"上下文参数"。本例中,它们是to: ContactBook参数和targetChannels: Set[NotifyChannel]参数。为了便于演示,使用新的函数名notify2

[!NOTE]

具体本例来说,业务使用本函数时,通讯名册往往只需要初始化一次,而通知它们时,往往会多次重复调用函数来执行通知,因此,"通讯名册"这个参数在商务部门的通知业务中,就是典型的只需要初始化一次,而需要多次传参的参数,因此我们将之标记为上下文参数。"目标通知渠道"这个参数同理。

另外,描述中对to参数和targetChannels参数都带上了其类型,这不是啰嗦,而是因为上下文参数的推断过程与其参数类型高度相关。Scala编译器推断上下文参数时往往根据其类型查找适用的实例。

  • 在实际业务中,我们使用given关键字标记一个通讯名册的实例为上下文参数,标记一个通知渠道的集合(也就是Set[NotifyChannel]类型)为上下文参数。
  • 调用新函数notify2时,只需传入一个content参数,剩余的两个上下文参数将由Scala编译器在编译期间自动推导填入。

最后,我们跑一下EntryPoint.scala文件中的main函数,将得到如下结果:

通过[短信渠道]给[领导A]发送通知
通知目的地为12345678900
通知内容为:今天晚上8点线上会议,请各位领导留意会议APP的通知,务必准时参加,谢谢~

通过[短信渠道]给[领导B]发送通知
通知目的地为19987654321
通知内容为:今天晚上8点线上会议,请各位领导留意会议APP的通知,务必准时参加,谢谢~

通过[短信渠道]给[领导A]发送通知
通知目的地为12345678900
通知内容为:A同志 今天晚上无故缺席会议,特此通报批评,希望各位领导遵守公司规章制度,谢谢~

通过[短信渠道]给[领导B]发送通知
通知目的地为19987654321
通知内容为:A同志 今天晚上无故缺席会议,特此通报批评,希望各位领导遵守公司规章制度,谢谢~

上下文参数的高级用法:类型族、类型参数简写

使用上下文参数定义类型族

类型族值得单独再开一篇,本文简要提及,后续在专栏中我们继续深入这个神奇的特性(挖个坑)。

在此,我们对上文中的例子做一些改造进阶:如果要强制使用方只能使用包含可以发送的通讯码的通讯名录来发送通知(什么是不可发送的?比如,通讯名录中填写的是家庭住址,那它就是不可发送的通讯码),我们考虑泛化ContactBook类,并定义一个"可发送"特质,使得通讯录中只能传入"可发送"的通讯码,方法如下:

// Entity.scala
/**
 * 通讯名册-泛化版
 *
 * @param id   id标识
 * @param desc 目的地的描述
 * @param book 通讯名册。其中的元组定义为:姓名-通讯码-联系渠道
 * @tparam A 通讯码的类型
 */
case class ContactBookGn[A](id: String, desc: String, book: Seq[(String, A, NotifyChannel)])(using s: Sendable[A])

/**
 * 可发送特质
 * @tparam T 可发送的类型
 */
trait Sendable[T] {
  def send(content: String, sendDestCode: T): Unit
}

// 手机通讯码
class MobileContactCode(val mobile: String)
object MobileContactCode {
  // 给手机通讯码提供一个Sendable[MobileContactCode]类型的上下文参数
  given Sendable[MobileContactCode] with
    def send(content: String, sendDestCode: MobileContactCode): Unit =
      println(s"给通讯码${sendDestCode.mobile}发送短信通知\n通知内容:${content}\n")
}
// 邮件通讯码
class EmailContactCode(val email: String)
object EmailContactCode {
  // 给邮件通讯码提供一个Sendable[MobileContactCode]类型的上下文参数
  given Sendable[EmailContactCode] with
    def send(content: String, sendDestCode: EmailContactCode): Unit =
      println(s"给通讯码${sendDestCode.email}发送邮件通知\n通知内容:${content}\n")
}

上面的代码的要点是:

  • 定义了新的带类型参数的通讯名册-泛化版ContactBookGn[A],并为其指定一个上下文参数s: Sendable[A],这个写法的意思是,要实例化ContactBookGn[A],我们必须提供一个Sendable[A]的上下文。
  • 定义了一个"可发送"特质,它带有一个send抽象方法,以实现具体的发送功能。
  • 定义了两个通讯码,分别是手机通讯码MobileContactCode和邮件通讯码EmailContactCode;这两个通讯码的伴生实例分别提供了一个上下文实现,以此定义在不实现Sendable特质的情况下,仍然可以符合里氏替换原则。

[!NOTE]

里氏替换原则是说,如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致。

有一种更简单的解释:子类型(subtype)必须能够替换掉他们的基类型(base type)

同时,我们还需对通知函数进行类似的泛化和特质限制:

// BuzNotify.scala
/**
 * @param content        通知内容
 * @param to             通讯名册
 * @param targetChannels 目标通知渠道
 * @tparam A             能支持Sendable[A]的类型参数
 * @return
 */
def notify3Gn[A](content: String)(using to: ContactBookGn[A], targetChannels: Set[NotifyChannel])(using s: Sendable[A]): Boolean =
  if targetChannels.isEmpty then return false
  to.book filter { t => targetChannels.contains(t._3) } foreach { (contactName, contactDest, channel) =>
    s.send(content, contactDest)
  }
  true

这里定义一个新的函数notify3Gn[A],并在原先的基础上,添加另一组上下文参数:(using s: Sendable[A])。与通讯名册的定义类似,这里也使用s: Sendable[A]来限制类型参数A能传入的实际类型。除了这个改动外,我们还使用上下文参数s的方法send来代替原先裸露的发送实现。

好了,定义完这一切,我们可以尝试调用这个全新的版本:

// EntryPoint.scala
def invoke3Gn(): Unit =
  // 给出ContactBookGn[MobileContactCode]的上下文参数
  given ContactBookGn[MobileContactCode] = ContactBookGn[MobileContactCode]("2", "研发部领导手机号通讯名册", Seq(("领导X", MobileContactCode("19911223344"), SMS), ("领导Y", MobileContactCode("19955667788"), SMS)))
  notify3Gn("新版需求上线已完成,请研发部领导关注服务器告警通知")

@main def main(args: String*): Unit =
  invoke3Gn()

执行它会得到以下输出:

给通讯码19911223344发送短信通知
通知内容:新版需求上线已完成,请研发部领导关注服务器告警通知

给通讯码19955667788发送短信通知
通知内容:新版需求上线已完成,请研发部领导关注服务器告警通知

一切如预期般正常运行。

那么章节标题中的类型族去哪了?实际上,MobileContactCodeEmailContactCode就是符合Sendable[T]约束的类型族,它们的重要特点是,这两个类与"可发送"特质之间并无子类或超类的关系,却可以正常填入需要使用Sendable[T]的地方(即符合里氏替换),就好像这两个类就是Sendable[T]的子类一样。

给大家看一个编译不通过的例子,希望可以与上文的代码互相印证,加深大家对类型族的理解:

// Entity.scala
// 错误示例
val c = ContactBookGn[String]("2", "研发部领导手机号通讯名册", Seq(("领导X", "19911223344", SMS), ("领导Y", "19955667788", SMS)))
// 编译不通过:No given instance of type Sendable[String] was found for a context parameter of method apply in object ContactBookGn

这句话编译不通过,因为String既不是Sendable[T]的子类,也不是符合Sendable[T]约束的类型族。

上下文参数省略名称

这个章节的内容较为晦涩,且写出来之后对同事的心智负担也比较大,敬请读者在使用时慎之又慎。

由于上下文参数实际上根据类型来查找,因此given 实例名称using 参数名称中的名称并不是必须的,这一点实际上前文代码中也有提及,此处重复说明一下,它是这样的:

// EntryPoint.scala
// 可以不给实例名,直接匿名指定上下文参数
given ContactBook = ContactBook(
  "1",
  "商务部门领导名册",
  Seq(("领导A", "12345678900", SMS), ("领导B", "19987654321", SMS), ("领导C", "[email protected]", Email)),
  Set(SMS, Email)
)
given Set[NotifyChannel] = Set(SMS)  // 此处写法实际上与前文相同

case class ContactBookGn[A](id: String, desc: String, book: Seq[(String, A, NotifyChannel)])(using Sendable[A])  // 这里就使用了省略上下文参数名的正确使用场景

还有更简单的写法:上下文绑定

在上文提及的ContactBookGn[A]notify3Gn[A]还有更简单的写法:

// Entity.scala
// 可以这样写。与原先的写法等效。
case class ContactBookGn[A: Sendable](id: String, desc: String, book: Seq[(String, A, NotifyChannel)])

// BuzNotify.scala
// 错误示例。
// 不能这样写,因为原先定义的上下文参数s: Sendable[A]是有用的,我们会在函数体中使用s.send()方法。
// 这个写法将省略上下文参数的实例名,导致我们不可能对send()方法进行调用。
def notify3Gn[A: Sendable](content: String)(using to: ContactBookGn[A], targetChannels: Set[NotifyChannel]): Boolean

我们可以使用[A: Sendable]来表示如下含义:类型参数A需要一个匿名上下文参数Sendable[A]

标签:一探,通讯,String,Scala,通知,参数,上下文,Sendable
From: https://blog.csdn.net/TonyBee_Bei/article/details/141759036

相关文章

  • AIGC时代,仅用合成数据训练模型到底行不行?来一探究竟 | CVPR 2024
    首个针对使用合成数据训练的模型在不同稳健性指标上进行详细分析的研究,展示了如SynCLIP和SynCLR等合成克隆模型,其性能在可接受的范围内接近于在真实图像上训练的对应模型。这一结论适用于所有稳健性指标,除了常见的图像损坏和OOD(域外分布)检测。另一方面,监督模型SynViT-B在除形状偏......
  • 一探究竟:免费提供API接口的原理揭秘
    API接口是软件系统中不同组件之间进行交互的一种方式。它定义了不同软件组件之间的通信规范和数据格式,使得这些组件能够相互调用和交换数据。API的全称是“ApplicationProgrammingInterface”,意为“应用程序编程接口”API接口的原理定义与规范:API接口首先定义了一套规范,......
  • 一门多范式的编程语言Scala学习收尾-函数的使用
    4、集合(接着上次的集合继续学习)4.4可变集合1、ListBuffervallistBuffer1:ListBuffer[Int]=newListBuffer[Int]println(s"$listBuffer1")listBuffer1.+=(11)listBuffer1.+=(22)listBuffer1.+=(33)listBuffer1.+=(11)listBuffer1.+=(55)listBuffer1.+=(22)listBuffe......
  • 一门多范式的编程语言Scala学习的第二天-函数的使用
    2.12scala中的函数式编程*scala中的函数式编程**面向对象编程:将对象当作参数一样传来传去*1、对象可以当作方法参数传递*2、对象也可以当作方法的返回值返回*当看到类,抽象类,接口的时候,今后无论是参数类型还是返回值类型,都需要提供对应的实现类对象**面向函数式编程......
  • 一门多范式的编程语言Scala学习的第一天-简介
    Scala1、Scala简介1.1Scala的介绍scala是一门多范式的编程语言Scala是把函数式编程思想和面向对象编程思想结合的一种编程语言大数据计算引擎spark是由Scala编写的1.2Scala的特性1.2.1多范式1.2.1.1面向对象特性Scala是一种高度表达性的编程语言,它结合了面向对象编程......
  • Paper Reading: SAFE: Scalable Automatic Feature Engineering Framework for Indust
    目录研究动机文章贡献本文方法整体框架特征生成特征组合关系排序特征组合生成特征特征选择去除无信息特征去除冗余特征复杂度分析实验结果数据集和实验设置对比实验特征重要性比较运行时间特征稳定性不同迭代次数的性能大规模数据集实验优点和创新点PaperReading是从个人角度进......
  • 轻松理解es6执行上下文
    想要学好js,深入理解js的如何运行肯定是少不了的。这篇文章是结合各个网站上的大佬们的文章在加上自己的理解形成的,如有错误的地方,请帮我纠正一下,谢谢。前言当js引擎去执行一段代码的时候,如何确定代码的执行顺序,以及变量何时被定义,this的指向等,想要更深入理解这些问题,必须了......
  • python with 上下文管理器
    简介1、什么是上下文管理器?上下文管理器是一种实现了上下文管理协议(ContextManagementProtocol)的对象,它通过定义__enter__()和__exit__()两个特殊方法来实现资源的获取和释放。上下文管理器通常使用with语句进行调用,确保资源在使用完毕后能够被正确释放。python中上下文管理......
  • Linux 进程调度(二)之进程的上下文切换
    目录一、概述二、上下文切换的实现1、context_switch2、switch_mm3、switch_to三、观测进程上下文切换一、概述进程的上下文切换是指在多任务操作系统中,当操作系统决定要切换当前运行的进程时,将当前进程的状态保存起来,并恢复下一个要运行的进程的状态。上下文切换......
  • Flink开发:Java vs. Scala - 代码对比分析,选择你的最佳拍档
    一、引言1.1Flink简介ApacheFlink是一个开源的流处理框架,它支持高吞吐量、低延迟以及复杂的事件处理。Flink的核心是一个流式数据流执行引擎,它的针对数据流的分布式计算提供了数据分发、通信、容错机制。Flink提供了多种API,包括DataStreamAPI(用于构建流处理程序)、D......