Статьи

Функция IoC для процедуры первого класса

Это вторая статья из двух, в которой я предлагаю термин «процедура первого класса». Первая статья предоставила рабочий пример процедуры первого класса, чтобы увидеть их в действии. Эта статья углубляется в детали и некоторые теории о том, как развивалась процедура первого класса.

Эволюция «процедуры первого класса» начинается с рассмотрения функции. Функция принимает параметры для получения результата:

1
2
3
4
5
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])

Однако, прежде чем получить какие-либо результаты, функция также требует запуска потока. Это то, что я называю «неявной нитью». Функция не определяет подробности относительно используемого потока, поэтому по умолчанию используется вызывающий поток функции (неявный поток).

В идеале, мы должны улучшить сигнатуру функции, чтобы указать поток более явно, чтобы мы могли дать больше контроля над тем, как выполняются составные функции:

1
2
3
4
type Executor = (Function, Array[Any]) => Any
 
  def invoke(executor: Executor, function: Function, parameters: Array[Any]) =
    executor(function, parameters)

Теперь поток для выполнения функции сделан явным. Обратите внимание, что исполнитель — это средство для вызова с неявным потоком или отдельным явным потоком через пул потоков.

Аргументом может быть то, что многопоточность сложна и должна быть оставлена ​​для компиляторов / фреймворков / и т.д. Тем не менее, я считаю, что я все еще хочу некоторый контроль над профилем выполнения приложения. Причиной этого будет следующий пример.

Я запускаю многопользовательское приложение на большом сервере с несколькими процессорами, которые выполняют различные блокирующие операции ввода-вывода и дорогостоящие вычисления ЦП для обслуживания запроса. Я действительно хочу оптимизировать вычисления ЦП, чтобы они имели сходство с ядром, чтобы избежать промахов кэша и накладных расходов на переключение контекста. Кроме того, я хочу изолировать блокирующий ввод / вывод в отдельном пуле потоков, чтобы вычисления ЦП не блокировали вызывающие неработающие ЦП. Учитывая нагрузку ввода-вывода, может потребоваться изолировать одно ядро ​​для выполнения всех операций ввода-вывода, оставляя оставшиеся ядра свободными для вычислений ЦП. С другой стороны, я мог бы найти, что ввод / вывод минимален и может срезать время с вычислениями ЦП, чтобы я мог получить дополнительное ядро ​​для вычислений ЦП для увеличения пропускной способности. В идеале, я хотел бы средства для настройки этого приложения.

Затем требования меняются, и теперь я хочу использовать одно и то же приложение для обслуживания одного пользователя, который запускает приложение на одном ядре (например, дешевая портативная встроенная система). В этих обстоятельствах я рад блокированию вычислений ЦП во время ввода-вывода. По сути, я просто хочу, чтобы один неявный поток выполнял все функции.

В обоих случаях это та же логика приложения, но только разные профили выполнения в зависимости от среды, в которой выполняется приложение.

Таким образом, взяв нашу явную сигнатуру функции выше, мы теперь требуем, чтобы функции более высокого порядка предоставляли соответствующую информацию о потоках для вызова каждой составной функции:

1
2
def higher(executorIO: Executor, executorCpuIntensive: Executor, parameters: Array[Any]) =
    executorIO(functionIO, Array(executorCpuIntensive(functionCpuIntensive, parameters)) ++ parameters)

Теперь для этого требуется функция высшего порядка, чтобы определить поток для каждой содержащейся функции. Это может уничтожить сигнатуру функции высшего порядка. Поэтому давайте пока создадим функцию, которая может определить исполнителя для функции:

1
type ExecutorLocator = Function => Executor

Так что теперь это меняет функцию высшего порядка на:

1
2
def higher(executorLocator: ExecutorLocator, parameters: Array[Any]) =
    executorLocator(functionIO)(functionIO, Array(executorLocator(functionCpuIntensive)(functionCpuIntensive, parameters)) ++ parameters)

Таким образом, мы остановили удар по сигнатуре функции более высокого порядка, но это начинает выдвигать на первый план проблему передачи результатов между составленными функциями. Ну, не столько в результате передачи результатов, сколько в части логики для выполнения с каким потоком.

Чтобы получить результат от функции A для передачи в функцию B, у нас есть два подхода:

  • После завершения функции A система возвращает результат в поток функции более высокого порядка, который затем передает его функции B
  • Когда функция A завершает работу, она продолжает выполнение функции B

Разница очень тонкая, но существенная для издержек переключения контекста потока.

Примечание: в моем наивном понимании функционального программирования я считаю, что первым подходом можно считать превращение каждой функции в актера, а вторым подходом является продолжение (стиль прохождения продолжения).

В любом случае, до неправильного понимания слишком большого количества литературы по функциональному программированию проблема с первым подходом заключается в чрезмерном переключении контекста потока. Если функция высшего порядка отличается от своих составных функций, она создает два переключателя контекста потока для каждой составной функции. Исполнение будет:

  1. Функция высшего порядка выполняется
  2. Вызывает составную функцию в другом потоке
  3. Результат возвращается в функцию более высокого порядка, в которой контекст потока переключается обратно на себя
  4. Вызывает следующую составную функцию в другом потоке (возможно, тот же самый поток, необходимый для шага 2)

Переключение контекста потока, которое происходит на шаге 3, на самом деле не требуется. Кроме того, стоимость переключения потока будет больше, чем несколько операций для передачи результата первой функции во вторую функцию.

Примечание: я ссылаюсь на издержки переключения контекста потока, предполагая, что потоки должны быть запланированы для обработки каждой части. Однако, даже если потоки работали непрерывно на отдельных ядрах, возникает необходимость в получении сообщений между потоками.

Существует также вторая проблема исключений. Да, я понимаю, что исключения не очень функциональны, но я вернусь к этому позже в отношении композиции.

Так что, если мы примем подход 2 продолжений, то выполнение будет следующим:

  1. Функция высшего порядка выполняется
  2. Вызывает составную функцию A в другом потоке (передавая следующую составную функцию B)
  3. В результате составленная функция A продолжается со следующей составной функции B

Мы исключили дополнительное переключение контекста в первом подходе, позволив составным функциям продолжать работу друг с другом.

Теперь это не слишком особенное. Это просто стиль передачи продолжения с возможностью выполнения продолжения с неявным потоком или делегирования другому потоку. При этом мы стараемся максимально использовать неявный поток, чтобы уменьшить накладные расходы на потоки. Однако, когда функции имеют разные характеристики выполнения (такие как блокировка ввода-вывода, дорогостоящие вычисления ЦП), мы меняем потоки, чтобы обеспечить надлежащее выполнение функций, чтобы поддерживать производительность всего приложения.

Еще один способ думать об этой многопоточности — рассмотреть потоки, работающие на разных ядрах. Первый подход — очень синхронное общение. Вызывается составная функция, и функция более высокого порядка эффективно ждет, пока результат не станет доступен. С другой стороны, продолжения больше похожи на асинхронную связь. Функция более высокого порядка запускает составленную функцию и затем может продолжить выполнение других функций. Составленная функция сама продолжит выполнение следующей составной функции.

Но продолжения не приходят легко.

Передача продолжения имеет значение для сигнатуры функции, так как мы должны передать следующее продолжение во все функции. Теперь наши функции выглядят так:

1
2
3
type Continuation = (Any) => Unit
 
  def function(executorLocator: ExecutorLocator, parameters: Array[Any], continuation: Continuation)

Могу ли я сказать, что у функции есть более одного результата — в любом случае, для меня. Да, мы можем вернуть тип данных, определяющий как успех, так и ошибку. Заявления о случаях затем обрабатывают эти различные результаты. Однако для каждого нового типа ошибки мы должны добавить новую обработку оператора case. Это напоминает мне о проблемах отражения необходимости опроса для каждого типа ошибки. Лично мне нравится обрабатывать эти результаты отдельно.

Вместо того, чтобы объединять результат и ошибку в одно следующее продолжение, мы можем предоставить продолжение для каждого. В моем понимании, это не слишком отличается от блоков try / catch (за исключением того, что теперь мы можем выполнить блок catch с неявным потоком или другим потоком). Другими словами, мы предоставляем несколько продолжений:

1
2
3
4
def function(executorLocator: ExecutorLocator, parameters: Array[Any],
      successfulContinuation: Continuation,
      errorOneContinuation: Continuation,
      errorTwoContinuation: Continuation)

Но зачем останавливаться на достигнутом? Мы также можем иметь различные пути через функцию для высказываний if. Если условие истинно, следуйте первому продолжению, иначе следуйте второму продолжению.

1
2
3
4
5
def function(executorLocator: ExecutorLocator, parameters: Array[Any],
      trueContinuation: Continuation,
      falseContinuation: Continuation,
      errorOneContinuation: Continuation,
      errorTwoContinuation: Continuation)

Это начинает снова уничтожать сигнатуру функции, особенно при компоновке в функции более высокого порядка, которые должны обрабатывать множество исключений. По этой же причине я склоняюсь к тому, что многие фреймворки отходят от проверенных исключений. Однако в процедурах первого класса нам очень нравятся проверенные исключения (хотя, вероятно, это не так, как вы обычно их знаете). Но мы скоро к этому придем.

Поэтому, чтобы избежать потери подписи, давайте сделаем то, что мы сделали с выбором исполнителя, и перенесем решение о продолжении в функцию. Пока давайте предположим, что у нас может быть ключ, чтобы помочь функции определить соответствующее продолжение. Эта функция будет выглядеть следующим образом:

1
type ContinuationLocator = (Function, Any) => Continuation

Это тогда превращает все наши функции в следующее:

1
2
3
4
def function(
      executorLocator: ExecutorLocator,
      parameters: Array[Any],
      continuationLocator: ContinuationLocator)

Так что теперь у нас есть очень гибкая модель выполнения, которая сводит к минимуму переключение контекста потока.

Однако как мы реализуем функции executorLocator и continueationLocator?

Ну что ж, именование функций является преднамеренным, так как они следуют шаблону ServiceLocator. Данный ключ обеспечивает обратную зависимость. Однако в этом случае это не объект, а исполнитель для выбора потока и продолжения для вызова следующей функции.

Теперь мы можем создать конфигурацию ключ / значение для каждой функции в системе. Ну, может и нет.

Эта гранулярность конфигурации становится кошмаром. Я, конечно, могу сказать, что мои первые версии реализации первоклассных процедур показали, что это очень важно. Кроме того, учитывая, что у нас есть предполагаемый ключ для определения продолжения, откуда мы знаем, что каждое продолжение настроено для полной системы? Другими словами, как мы можем сделать этот компилятор безопасным?

Чтобы решить эту проблему, мы делаем то, что мы обычно делаем в программном обеспечении, добавляя больше косвенности.

Да, более динамическая косвенность для создания безопасного скомпилированного решения? Ну да.

Чтобы объяснить, как косвенное обращение помогло, давайте начнем с инъекции продолжения.

Во-первых, мы собираемся дать состояние функции (или, возможно, лучше описать как метаданные). Возможно, это потенциально превращает функцию в объект, но я собираюсь избегать попыток связать вещи с литературой прямо сейчас и сосредоточиться на том, как развивалась процедура первого класса.

Итак, теперь у нас есть объект обтекания:

1
2
3
class ManagedFunction(
    val logic: Function,
    val continuations: Map[Any, Continuation])

Итак, мы связали продолжения с функцией, но это действительно только динамическая карта, основанная на ключе. Поэтому, чтобы быть безопасным для компиляции, нам нужно дать ключу некоторое значение, понятное компилятору / фреймворку.

Хорошо, чтобы сделать это, давайте вернемся к разорванной сигнатуре функции:

1
2
3
def function(parameters: Array[Any],
      trueContinuation: Continuation,
      falseContinuation: Continuation)

Поскольку параметры всегда упорядочены, мы можем использовать индекс продолжения в качестве ключа. Это имеет ManagedFunction выглядит следующим образом:

1
2
3
4
5
6
7
8
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)))
  }

Теперь я упомянул процедуры первого класса, которые на самом деле любят использовать проверенные исключения. Причина в том, что проверенное исключение указано в подписи. Это будет выглядеть так:

1
function(Object[] paramaeters) throws ErrorOne, ErrorTwo;

Проверенные исключения не упорядочены, но их типы являются уникальными. Поэтому мы можем использовать тип проверенного исключения в качестве ключа. Теперь это превращает ManagedFunction в следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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)
      }
    }
  }

Теперь, чтобы сделать компиляцию / фреймворк безопасным, мы предоставляем функцию, которая создает требуемый список ключей для логической функции:

1
2
3
4
5
6
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 имела следующий метод run:

1
2
3
abstract class ManagedFunction {
    def run(parameter: Any) // not, parameters: Array[Any]
  }

Итак, как мы можем предоставить дополнительные параметры для функции?

Прежде чем ответить на этот вопрос, мы должны рассмотреть, как запускается первое продолжение, чтобы запустить цепочку выполнения ManagedFunction. Поскольку приложение теперь реализуется как отображение продолжения в ManagedFunctions, нам нужны средства для запуска продолжения вне ManagedFunction.

Ну, а почему мы не можем дать продолжение объектов?

Мы можем создать ManagedObject, который содержит продолжения:

1
2
3
4
5
class ManagedObject(
    @Inject val continuationOne: Continuation,
    @Inject val continuationTwo: Continuation) {
    // ... object methods
  }

Это теперь позволяет объектам запускать логику. Почему это полезно? Ну, например, у нас может быть объект прослушивателя сокета HTTP, который получает запрос и обслуживает запрос, вызывая продолжение. Далее экземпляры ManagedFunction будут использовать детали запроса, чтобы направить его через продолжения к соответствующей обработке экземпляров ManagedFunction для последующего обслуживания запроса.

Пример HTTP фактически указывает на шаблон проектирования, уже решающий проблему нескольких параметров для функции. Типичный веб-сервер потока-на-запрос имеет контекст запроса, сеанса и приложения. Теперь давайте проигнорируем контексты сеанса и приложения, поскольку они не безопасны для параллелизма. Нам помогает шаблон контекста запроса.

Контекст запроса позволяет передавать объекты между контроллерами и компонентами рендеринга представления. Какие контроллеры и вид рендеринга? Они представляют собой фрагменты логики, которые принимают в области запроса доступ / видоизменение области запроса, чтобы захватить достаточное количество состояний для предоставления ответа (с возможными побочными эффектами сохранения состояния в базах данных, подробностей регистрации и т. Д.).

Эти фрагменты логики хорошо вписываются в ManagedFunction с областями запросов, созданными для каждого дерева продолжения, вызываемого из ManagedObject. ManagedObjects создаются в приложении, которые подключаются к сети продолжений ManagedFunctions. Когда ManagedObject получает событие (HTTP-запрос, сообщение очереди и т. Д.), Он делает две вещи:

  1. Запускает новую область запроса
  2. Запускает первое продолжение с областью действия, которая выполняется для всех последующих продолжений, запущенных
  3. ManagedFunctions теперь может получить необходимые параметры из области видимости.

Это может быть продолжено, чтобы включить внедрение зависимости. Вместо того, чтобы ManagedFunction отвечала за управление областью запросов, объекты области запросов предоставляются посредством внедрения зависимостей. Это следующий контекст зависимости для ManagedFunction:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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
        }
      }
    }
  }

Дополнительным преимуществом предоставления Dependency Context является то, что мы можем повторно использовать существующие платформы Dependency Injection для управления объектами. Например, ServiceLocator может быть Spring BeanFactory. Кроме того, мы также можем внедрить реализации ManagedObject в зависимости, чтобы позволить объектам поддерживать состояние, но также запускать продолжения в фоновом режиме (например, фоновый опрос для изменений ключа JWT при предоставлении состояния аутентификации JWT).

Управляемая функция теперь становится:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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 использовать различные интерфейсы для определения характера использования переменной. Это позволяет следующие интерфейсы для состояния переменной:

1
2
3
trait Out[T] { def set(value: T) }
  trait In[T] { def get(): T }
  trait Var[T] extends Out[T] with In[T]

Затем ManagedFunctions может использовать соответствующий интерфейс для определения своего намерения относительно состояния переменной.

Обратите внимание, что теперь можно просматривать график из продолжений ManagedObject, чтобы подтвердить, что выходные данные переменных состояний ManagedFunctions всегда находятся выше соответствующих входных данных. Это создает возможность для генерации безопасного состояния генерации. Кроме того, если все объекты, загруженные в переменные области видимости, являются неизменяемыми, это позволяет обосновывать идентификацию ManagedFunction, создающей неправильное состояние (просто ищите ManagedFunctions, требующие Out of variable).

То, что это теперь также обеспечивает, является множественными входами и множественными выходами. Композиция больше не выводится выводом одной функции, передаваемой в качестве ввода следующей функции. Состояние поддерживается в области действия, когда ManagedFunctions извлекает / толкает состояние в соответствии с областью действия. Продолжения теперь отделены от необходимости заниматься всем состоянием, необходимым для вызова ManagedFunction.

Теперь вышеприведенная реализация предполагает некоторый порядок параметров, за которым следуют продолжения при вызове логики. Поскольку эта информация рефлексивно извлекается из логической функции, порядок не требуется. Затем мы можем иметь ManagedFunction выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
      }
    }
  }

Обратите внимание, что возвращаемое значение из функции (логики) больше не требуется. Следовательно, почему мы рассматриваем это «первоклассные процедуры».

Это может быть представлено следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 } // 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)
    }
  }

Это позволяет управлять выбором исполнителя в конфигурации. Это отделяет это от забот состава и государственного управления.

И теперь вы в курсе общих концепций первоклассной процедуры.

Тем не менее, обратите внимание, что фактическая реализация использует намного больше запоминания, так как сигнатуры функций являются статическими, что позволяет выполнять отражение во время компиляции / запуска.

Кроме того, общий эффект состоит в том, что функциям более высокого порядка не нужно предоставлять все аргументы для вызова функции (процедуры). Элемент управления инвертируется, поэтому конфигурация и сама процедура определяют, что вводится в него. Композиция высшего порядка должна использовать только продолжения для вызова процедур первого класса.

Кроме того, я считаю, что в функциональном программировании снимается ограничение необходимости подгонки всех результатов через маленькое отверстие для ключа в типе возврата функции. Тип возвращаемого значения функции должен предоставлять сведения об успехе и ошибке и должен быть связан с передачей этого через цепочку составных функций. Процедуры первого класса, через переменные, разъединяют это так, что любая процедура восходящего потока может вывести значение для любой процедуры нижестоящего уровня, чтобы потреблять. Кроме того, проверенные исключения продолжают потоки ошибок, чтобы удалить это из типов возвращаемых функций (типов выходных переменных).

Есть также другие концепции, основанные на процедурах первого класса, такие как:

  • контексты зависимостей области действия процесса, потока, функции для параллельной / параллельной обработки
  • композиции высшего порядка (разделы)
  • сходство потоков и другое управление потоками (через Executive)
  • предоставление контекста для состояния, такого как транзакции (управление)
  • дополнительные ManagedFunctions вставляются в потоки, похожие на аспекты (Администрирование)

Однако эта статья посвящена первоклассной процедуре и уже достаточно длинна.

Итак, в заключение, процедура первого класса — применение инверсии управления к функции для ввода в состояние, продолжения и поток (через Executor). Это означает, что процедура первого класса больше не требует композиции через возвращаемые значения функции. Это значительно облегчает объединение нечистой / чистой функциональности. Кроме того, он позволяет настраивать стратегии выполнения приложения во время развертывания.

И чтобы увидеть все это в действии, пожалуйста, смотрите первую статью .

Смотрите оригинальную статью здесь: Функция IoC для процедуры первого класса

Мнения, высказанные участниками Java Code Geeks, являются их собственными.