这是第二篇文章的第二篇,介绍了我建议的“一流程序”一词。 第一篇文章提供了一流程序的实际示例,以了解它们的实际作用。 本文研究有关一流程序如何演变的细节和一些理论。
“一流程序”的演变始于看功能。 该函数接受参数以产生结果:
type Function = Array[Any] => Any
// Example of first class function
def exampleFunction(paramOne: Int, paramTwo: Double): String
val firstClassFunction: Function = (parameters) => exampleFunction(parameters( 0 ).asInstanceOf[Int], parameters( 1 ).asInstanceOf[Double])
但是,在获得任何结果之前,该函数还需要运行一个线程。 这就是我所说的“隐式线程”。 该函数未定义有关要使用的线程的详细信息,因此默认为该函数的调用线程(隐式线程)。
理想情况下,我们应该增强函数签名以更明确地指示线程,以便我们可以对组成函数的执行方式进行更多控制:
type Executor = (Function, Array[Any]) => Any
def invoke(executor: Executor, function: Function, parameters: Array[Any]) =
executor(function, parameters)
现在,使执行该功能的线程变得明确。 请注意,执行程序是通过线程池使用隐式线程或单独的显式线程进行调用的手段。
一个论点可能是线程很难,应该留给编译器/ frameworks / etc。 但是,我相信我仍然希望对应用程序的执行配置文件进行一些控制。 原因如下。
我正在具有多个处理器的大型服务器上运行多用户应用程序,该服务器执行各种阻塞的I / O和昂贵的CPU计算以服务请求。 我真的很想优化CPU计算,使它们与内核具有亲和力,从而避免缓存未命中和线程上下文切换开销。 另外,我想将阻塞的I / O隔离到单独的线程池中,这样CPU的计算就不会阻塞而导致空闲的CPU。 在给定I / O负载的情况下,可能要隔离一个内核来执行所有I / O,而剩下的内核可以自由用于CPU计算。 相反,我可能会发现I / O很小,并且可以与CPU计算进行时间分割,因此我可以为CPU计算获得一个额外的内核,以提高吞吐量。 理想情况下,我想针对应用程序进行调整。
然后需求发生变化,我现在想使用同一应用程序来为在单个内核(例如便宜的便携式嵌入式系统)上运行该应用程序的单个用户提供服务。 在这种情况下,我很高兴在I / O进行时阻止CPU计算。 基本上,我只希望一个隐式线程运行所有功能。
在以上两种情况下,它都是相同的应用程序逻辑,只是基于应用程序在其中运行的环境而不同的执行配置文件。
因此,采用上述显式函数签名,我们现在需要让高阶函数提供适当的线程信息来调用每个组合函数:
def higher(executorIO: Executor, executorCpuIntensive: Executor, parameters: Array[Any]) =
executorIO(functionIO, Array(executorCpuIntensive(functionCpuIntensive, parameters)) ++ parameters)
现在,这需要更高阶的函数来确定每个所包含函数的线程。 这可能会炸毁高阶函数的签名。 因此,现在让我们创建一个可以确定函数执行器的函数:
type ExecutorLocator = Function => Executor
因此,现在将高阶函数更改为:
def higher(executorLocator: ExecutorLocator, parameters: Array[Any]) =
executorLocator(functionIO)(functionIO, Array(executorLocator(functionCpuIntensive)(functionCpuIntensive, parameters)) ++ parameters)
因此,我们已经停止了对高阶函数签名的打击,但是它开始突出显示在组合函数之间传递结果的问题。 传递结果不是那么重要,而是在哪个线程中执行哪个逻辑。
为了使功能A的结果传递到功能B,我们有两种方法:
- 当functionA完成时,系统将结果返回到高阶函数的线程,然后将其传递到functionB
- 功能A完成后,继续执行功能B
差异非常细微,但是对于线程上下文切换开销而言却非常重要。
注意:在我对函数式编程的幼稚理解中,我认为第一种方法可以考虑将每个函数转换为Actor,而第二种方法是Continuation(连续传递样式)。
无论如何,在误解过多的函数编程文献之前,第一种方法的问题是过多的线程上下文切换。 如果高阶函数与其组合函数是不同的执行器,则它将为每个组合函数创建两个线程上下文开关。 执行将是:
- 高阶函数执行
- 在另一个线程上调用组合函数
- 返回到高阶函数的结果,线程上下文切换回自身
- 在另一个线程(可能与步骤2所需的线程相同)上调用下一个组合函数
确实不需要在步骤3中进行线程上下文切换。 此外,切换线程的成本将比少数将第一个函数的结果传递给第二个函数的操作多。
注意:我指的是线程上下文切换开销,假设需要安排线程来处理每个部分。 但是,即使线程在单独的内核上连续运行,也必须在线程之间获取消息会产生开销。
还有第二个例外问题。 是的,我知道例外不是很实用,但是稍后我会在组合方面进行讨论。
因此,如果采用延续的方法2,则执行将如下所示:
- 高阶函数执行
- 在另一个线程上调用组合函数A(传入下一个组合函数B)
- 结果,组合函数A继续下一个组合函数B
在第一种方法中,我们通过使组合函数彼此继续来消除了额外的上下文切换。
现在,这并不过分。 这只是延续传递样式,具有通过隐式线程执行委派或委派给其他线程的能力。 为此,我们尝试尽可能多地使用隐式线程,以减少线程开销。 但是,当函数具有不同的执行特征(例如阻塞I / O,昂贵的CPU计算)时,我们将交换线程以启用适当的函数执行,以保持整体应用程序的性能。
考虑这种线程的另一种方法是考虑在单独的内核上运行的线程。 第一种方法是非常同步的通信。 调用组合函数,并且高阶函数有效地等待直到结果可用。 另一方面,连续性更类似于异步通信。 高阶函数触发组合函数,然后可以自由继续其他功能。 组合函数本身将继续下一个组合函数。
但是延续并不容易。
连续传递对函数签名有影响,因为我们必须将下一个连续传递给所有函数。 现在,我们的功能如下所示:
type Continuation = (Any) => Unit
def function(executorLocator: ExecutorLocator, parameters: Array[Any], continuation: Continuation)
现在我敢说一个功能有不止一个结果-无论如何对我而言。 是的,我们可以返回定义成功和错误的数据类型。 然后,案例陈述处理这些各种结果。 但是,对于每种新的错误类型,我们都必须添加新的case语句处理。 这使我想起了必须针对每种错误类型进行询问的反射问题。 就个人而言,我喜欢单独处理这些结果。
与其将结果和错误组合成一个下一个延续,我们可以为每个提供一个延续。 以我的理解,这与try / catch块没有太大区别(除了我们现在可以使用隐式线程或其他线程执行catch块)。 换句话说,我们提供了多个延续:
def function(executorLocator: ExecutorLocator, parameters: Array[Any],
successfulContinuation: Continuation,
errorOneContinuation: Continuation,
errorTwoContinuation: Continuation)
但是为什么停在那里。 我们也可以通过函数说if语句使用不同的路径。 如果条件为true,则遵循第一个继续,否则遵循第二个继续。
def function(executorLocator: ExecutorLocator, parameters: Array[Any],
trueContinuation: Continuation,
falseContinuation: Continuation,
errorOneContinuation: Continuation,
errorTwoContinuation: Continuation)
这开始再次吹散函数签名,尤其是在组成必须处理许多异常的高阶函数时。 这也是我倾向于发现许多框架正在远离受检查的异常的原因。 但是,对于一流的过程,我们非常喜欢检查异常(尽管这可能不是您通常所知道的那样)。 但是,我们很快会谈到。
因此,为避免签名崩溃,让我们按照选择执行器的方式进行操作,并将延续决策包装到函数中。 现在,让我们假设我们可以有一些键来帮助函数确定适当的延续。 该函数如下所示:
type ContinuationLocator = (Function, Any) => Continuation
然后,将我们所有的功能转换为以下功能:
def function(
executorLocator: ExecutorLocator,
parameters: Array[Any],
continuationLocator: ContinuationLocator)
因此,与最小化线程上下文切换相比,现在我们有了这种非常灵活的执行模型。
但是,我们如何实现executorLocator和continuationLocator函数?
函数的命名是故意的,因为它们遵循ServiceLocator模式。 给定一个密钥,请提供依赖关系。 但是,在这种情况下,它不是对象,而是线程选择的执行程序和调用下一个函数的继续。
是的,我们现在可以为系统中的每个功能创建键/值配置。 好吧,也许不是。
这种配置的粒度最终成为噩梦。 我可以肯定地说,实现一流程序的第一个版本确实显示了这种情况。 再加上假设我们有一个确定连续性的假定密钥,我们如何知道我们为一个完整的系统配置了每个连续性? 换句话说,如何使该编译安全?
为了解决这个问题,我们做通常在软件中做的事情,添加更多间接。
呵呵,创建一个类型安全的编译解决方案的更多动态间接方法? 嗯,是。
为了解释间接如何提供帮助,让我们从Continuation Injection开始。
首先,我们将给出功能状态(或者可能更好地描述为元数据)。 可以说这可能会将函数变成一个对象,但我将避免尝试立即将事物与文献联系起来,而将重点放在一流过程的发展上。
因此,我们现在有了一个包装对象:
class ManagedFunction(
val logic: Function,
val continuations: Map[Any, Continuation])
因此,我们已将延续性与函数相关联,但这实际上只是基于键的动态映射。 因此,为了安全起见,我们需要给密钥提供一些编译器/框架可以理解的含义。
为此,让我们回到分解后的函数签名:
def function(parameters: Array[Any],
trueContinuation: Continuation,
falseContinuation: Continuation)
给定参数总是有序的,我们可以使用延续的索引作为关键字。 它的ManagedFunction外观如下:
class ManagedFunction(
val logic: Function,
val continuations: Map[Any, Continuation]) {
def cont(key: Any): Continuation = continuations.get(key) match { case Some(cont) => cont }
def run(parameters: Array[Any]) = logic(parameters ++ Array(cont( 1 ), cont( 2 )))
}
现在,我提到了一流的过程,实际上就像使用检查异常。 原因是在签名上说明了检查的异常。 看起来像:
function(Object[] paramaeters) throws ErrorOne, ErrorTwo;
受检查的异常没有顺序,但是它们的类型是唯一的。 因此,我们可以使用检查的异常的类型作为键。 现在,将ManagedFunction转换为以下内容:
class ManagedFunction(
val logic: Function,
val continuations: Map[Any, Continuation]) {
def cont(key: Any): Continuation = continuations.get(key) match { case Some(cont) => cont }
def run(parameters: Array[Any]) = {
try {
logic(parameters ++ Array(cont( 1 ), cont( 2 )))
} catch {
case ex: Throwable => cont(ex.getClass())(ex)
}
}
}
现在,为了确保编译/框架的安全,我们提供了一个函数,该函数为逻辑函数生成所需的键列表:
def extractContinuations(logic: Function): Array[Any] = {
var parameterIndex = 0
extractParameterTypes(logic)
.filter((parameterType) => classOf[Continuation].isAssignableFrom(parameterType)).map((paramContinuation) => { parameterIndex += 1 ; parameterIndex }) ++
extractExceptionTypes(logic)
}
然后,编译器/框架将确认为每个键提供了配置映射。 这允许验证是否已配置所有继续以使功能运行。 此外,通过在应用程序内的所有ManagedFunction实例上执行此操作,我们可以确认完整的已配置应用程序。 现在,我们已经进行了编译/框架启动安全验证,可以配置所有延续。
但是,现在我们遇到了在ManagedFunction中包含的函数之间传递状态的问题。 由于连续只能传递一个参数,一个函数如何有多个参数?
理想情况下,我们希望每个ManagedFunction具有以下运行方法:
abstract class ManagedFunction {
def run(parameter: Any) // not, parameters: Array[Any]
}
那么,我们如何为函数提供其他参数呢?
在回答这个问题之前,我们需要考虑如何触发第一个延续以启动ManagedFunction执行链。 由于应用程序现在已经实现为延续到ManagedFunctions的映射,因此我们需要一种从ManagedFunction外部触发延续的手段。
好吧,为什么我们不能给对象连续?
我们可以创建一个包含延续的ManagedObject:
class ManagedObject(
@Inject val continuationOne: Continuation,
@Inject val continuationTwo: Continuation) {
// ... object methods
}
现在,这允许对象触发逻辑。 为什么这有用? 好吧,例如,我们可以有一个HTTP套接字侦听器对象,该对象接收请求并通过调用延续来服务该请求。 然后,其他ManagedFunction实例将使用请求的详细信息,以通过继续将其路由到适当的处理ManagedFunction实例,从而为请求提供服务。
HTTP示例实际上为我们指出了一种设计模式,该模式已经解决了功能的多个参数问题。 典型的每个请求线程Web服务器具有请求,会话和应用程序上下文。 现在让我们忽略会话和应用程序上下文,因为它们不是并发安全的。 请求上下文模式可以帮助我们。
请求上下文允许在控制器和视图呈现组件之间传递对象。 什么是控制器和视图渲染? 它们是逻辑片段,它们包含一个请求范围以访问/更改该请求范围以捕获足够的状态以提供响应(具有在数据库中保存状态,记录详细信息等的可能的副作用)。
这些逻辑片段非常适合ManagedFunction,并为从ManagedObject调用的每个连续树创建了请求范围。 ManagedObjects是在应用程序中创建的,这些应用程序被挂钩到ManagedFunctions的延续网络中。 当ManagedObject收到事件(HTTP请求,队列消息等)时,它将执行以下两项操作:
- 启动新的请求范围
- 触发具有范围的第一个延续,以触发所有进一步的延续
- ManagedFunctions现在可以从范围中获取其所需的参数
这可以进一步包括依赖项注入。 不是通过ManagedFunction来管理请求范围,而是通过依赖项注入来提供请求范围对象。 这是ManagedFunction的以下依赖项上下文:
type ServiceLocator = String => Any
class DependencyContext(val serviceLocator: ServiceLocator) {
val objects = scala.collection.mutable.Map[String, Any]()
def getObject(name: String) = {
objects.get(name) match {
case Some(obj) => obj
case None => {
val obj = serviceLocator(name)
objects(name) = obj
obj
}
}
}
}
提供依赖关系上下文的另一个好处是,我们可以重用现有的依赖注入框架来管理对象。 例如,ServiceLocator可以是Spring BeanFactory。 此外,我们还可以依赖注入ManagedObject实现来允许对象维护状态,而且还可以在后台触发连续性(例如,在提供JWT身份验证状态时对JWT密钥更改进行后台轮询)。
ManagedFunction现在变为:
type ContinuationFactory = DependencyContext => Continuation
class ManagedFunction(
val logic: Function,
val parameterScopeNames: List[String],
val continuations: Map[Any, ContinuationFactory]) {
def obj(index: Int, context: DependencyContext): Any = context.getObject(parameterScopeNames(index))
def cont(key: Any, context: DependencyContext): Continuation = continuations.get(key) match { case Some(factory) => factory(context) }
def run(parameterFromContinuation: Any, context: DependencyContext) = {
try {
logic(Array(parameterFromContinuation, obj( 1 , context), obj( 2 , context), cont( 1 , context), cont( 2 , context)))
} catch {
case ex: Throwable => cont(ex.getClass(), context)(ex)
}
}
}
要填充作用域名称,我们可以再次在逻辑签名上使用反射。 但是,不必提供显式配置,我们可以根据参数类型和可能的限定符使用自动装配配置。 然后,这将成为构造函数的常规依赖项注入,只是我们将其注入逻辑函数中。
现在我们可以为逻辑提供数据库连接,HTTP客户端等,但是它不能解决跨连续边界传递状态的问题。
为了解决传递状态,我们只需创建一个状态对象。 这个对象的行为很像一个变量。 它的值可以设置和检索。 但是,这引入了关于连续流的可变性和时序问题。 尚不清楚ManagedFunction是仅安全访问变量的值,还是不安全地对变量进行突变。 因此,对于变量,我们在ManagedFunction中提供了额外的支持,以识别变量的使用。
对于变量状态对象,我们允许ManagedFunction使用各种接口来标识使用变量的性质。 这允许以下接口处于可变状态:
trait Out[T] { def set(value: T) }
trait In[T] { def get(): T }
trait Var[T] Out[T] with In[T] extends Out[T] with In[T]
然后,ManagedFunctions可以使用适当的接口来识别其对变量状态的意图。
请注意,现在可以从ManagedObject的延续中遍历图形,以确认ManagedFunctions的变量状态输出始终位于相应输入的上游。 这创建了编译安全状态生成的功能。 此外,如果加载到范围变量的所有对象都是不可变的,则允许进行推理以识别产生错误状态的ManagedFunction(只需查找需要Out of变量的ManagedFunctions)。
现在,它还提供了多个输入和多个输出。 不再通过将一个函数的输出作为输入传递给下一个函数来派生组合。 在适当的范围内通过ManagedFunctions的拉/推状态维护状态。 现在,连续性不再需要考虑调用ManagedFunction所需的所有状态。
现在,以上实现假定参数的某些顺序,随后是逻辑调用的继续。 由于此信息是从逻辑功能中反射性地检索到的,因此不需要此顺序。 然后,我们可以看到ManagedFunction的外观如下:
class ManagedFunction(
val logic: Function,
val parameterScopeNames: List[String],
val continuations: Map[Any, ContinuationFactory]) {
def obj(index: Int, context: DependencyContext): Any = context.getObject(parameterScopeNames(index))
def cont(key: Any, context: DependencyContext): Continuation = continuations.get(key) match { case Some(factory) => factory(context) }
def run(parameterFromContinuation: Any, context: DependencyContext) = {
var continuationIndex = 0
var objectIndex = 0
val arguments = extractParameterTypes(logic).map(_ match {
case p if p.isAnnotationPresent(classOf[Parameter]) => parameterFromContinuation
case c if classOf[Continuation].isAssignableFrom(c) => cont({ continuationIndex += 1 ; continuationIndex }, context)
case _ => obj({ objectIndex += 1 ; objectIndex }, context)
})
try {
logic(arguments)
} catch {
case ex: Throwable => cont(ex.getClass(), context)(ex)
}
}
}
注意,不再需要函数(逻辑)的返回值。 因此,为什么我们要考虑这种“一流的程序”。
然后可以这样表示:
class ManagedFunction(
val procedure: Array[Any] => Unit,
val parameterScopeNames: List[String],
val continuations: Map[Any, ContinuationFactory]) {
def obj(index: Int, context: DependencyContext): Any = context.getObject(parameterScopeNames(index))
def cont(key: Any, context: DependencyContext): Continuation = continuations.get(key) match { case Some(factory) => factory(context) }
def run(parameterFromContinuation: Any, context: DependencyContext): Unit = {
var continuationIndex = 0
var objectIndex = 0
val arguments = extractParameterTypes(procedure).map(_ match {
case p if p.isAnnotationPresent(classOf[Parameter]) => parameterFromContinuation
case c if classOf[Continuation].isAssignableFrom(c) => cont({ continuationIndex += 1 ; continuationIndex }, context)
case _ => obj({ objectIndex += 1 ; objectIndex }, context)
})
try {
procedure(arguments)
} catch {
case ex: Throwable => cont(ex.getClass(), context)(ex)
}
}
}
因此,我们提供了状态管理的逻辑组成,但尚未解决引发此问题的原始隐式线程问题。
为了解决指定显式线程的问题,我们需要实现ExecutorLocator。 这可以通过查看函数的参数类型来实现。 由于所有状态(对象)现在都是从DependencyContext注入的,因此我们可以从参数确定执行特征。 换句话说,如果逻辑依赖于数据库连接,则很可能正在进行阻塞调用。 因此,我们可以使用参数类型来实现ExecutorLocator:
class ManagedFunction(
val procedure: Array[Any] => Unit,
val parameterScopeNames: List[String],
val continuations: Map[Any, ContinuationFactory],
val executorConfiguration: Map[Class[_], Executor]) {
def obj(index: Int, context: DependencyContext): Any = context.getObject(parameterScopeNames(index))
def cont(key: Any, context: DependencyContext): Continuation = continuations.get(key) match { case Some(factory) => factory(context) }
def executorLocator(): Executor = {
var executor: Executor = (logic, arguments) => logic(arguments) // default executor is synchronous (implicit thread)
extractParameterTypes(procedure).map((parameterType) => executorConfiguration.get(parameterType) match {
case Some(e) => { executor = e; e } Some(e) => { executor = e; e } // matched so override
case None => executor
})
executor
}
def run(parameterFromContinuation: Any, context: DependencyContext): Unit = {
var continuationIndex = 0
var objectIndex = 0
val arguments = extractParameterTypes(procedure).map(_ match {
case p if p.isAnnotationPresent(classOf[Parameter]) => parameterFromContinuation
case c if classOf[Continuation].isAssignableFrom(c) => cont({ continuationIndex += 1 ; continuationIndex }, context)
case _ => obj({ objectIndex += 1 ; objectIndex }, context)
})
executorLocator()((arguments) => {
try {
procedure(arguments)
} catch {
case ex: Throwable => cont(ex.getClass(), context)(ex)
}
}, arguments)
}
}
这使得可以在配置中管理执行器的选择。 这使它脱离了对组成和状态管理的关注。
现在,您可以掌握一流程序背后的一般概念。
但是,请注意实际实现使用了更多的备忘录,因为函数签名是静态的,允许在编译/启动时进行反射。
此外,总体效果是,高阶函数不需要提供所有参数来调用该函数(过程)。 控件是反向的,因此配置和过程本身定义了注入到其中的内容。 高阶组成只需要使用延续来调用一流的过程。
另外,我发现它在函数式编程中增加了一个约束,即必须通过函数返回类型的小键Kong来拟合所有结果。 函数的返回类型需要提供成功和错误的详细信息,并且必须将其通过组成函数的链来传递。 一流的过程通过变量解耦,因此任何上游过程都可以输出供任何下游过程使用的值。 此外,受检查的异常会继续产生错误流,从而将其从函数返回类型(输出变量类型)中移除。
一流的过程还建立了其他概念,例如:
- 进程,线程,函数范围的并发/并行处理的依赖关系上下文
- 高阶作文(部分)
- 线程相似性和其他线程管理(通过Executive)
- 提供状态的上下文,例如交易(治理)
- 插入类似于流程(管理)流程的其他ManagedFunctions
但是,本文重点关注一流的过程,并且已经足够长。
因此,总而言之,一流的过程是将控制反转应用于功能以注入状态,连续性和线程(通过执行器)。 这意味着一流的过程不再需要通过函数的返回值进行合成。 这使得将不纯/纯功能组合在一起非常容易。 此外,它允许在部署时配置应用程序的执行策略。
要了解所有这些操作,请参阅第一篇文章 。
翻译自: https://www.javacodegeeks.com/2019/05/function-ioc-first-class-procedure.html