в реализацииПрименение методов оптимизации React в трассировке лучей для Интернета》, у меня есть требование, чтобы петля не начиналась от начала до конца, а начиналась с середины и расширялась в обе стороны. добиться следующих эффектов
Картинка представлена поступательно, не сверху вниз, а от середины.
Сначала я использовал цикл for для добавления различных переменных для переключения, что было болезненно отлаживать, и в конце концов я потерял терпение. Может быть очень прямое решение этого требования, но я не думал об этом в то время.
Так что я достал мощное оружие из «арсенала программирования» и решил эту маленькую проблему. И я обнаружил, что эта сцена поражения зенитными орудиями комаров очень подходит в качестве объяснения. Отсюда и эта статья.
Шаблон комбинатора является распространенным шаблоном в функциональном программировании.Известная библиотека синтаксического анализа parsec в Haskell использует этот шаблон, также известный как комбинатор парсеров.
Слово «Комбинатор» имеет много значений. Здесь шаблон комбинатора описывает вычислительную структуру, состоящую из примитивов и комбинаторов.
Мне нужно сделать особый акцент на термине «вычислительная структура», хотя это не совсем термин, я думаю, что его стоит распространять. Мы все слишком хорошо знаем, что программы — это структуры данных + алгоритмы, и они действительно полезны при написании низкоуровневого кода. Но для кода уровня приложения нам нужен другой взгляд, поэтому я хочу подчеркнуть вычислительную структуру.
Вычислительные структуры, на мой взгляд, представляют собой особый способ написания алгоритмов. Один и тот же алгоритм можно записать разными способами. Некоторые из них имеют хорошую производительность, некоторые имеют небольшой объем кода, некоторые интуитивно понятны и легко читаются, а некоторые обладают хорошей компонуемостью и высокой разумностью, и они могут представлять собой иерархический вычислительный процесс.
Возьмем пример алгоритма цикла, развернутого посередине. Демонстрирует, как шаблон комбинатора может представлять элегантные вычислительные иерархии.
Давайте поговорим о первом компоненте шаблона комбинатора, конструкторе. Он отвечает за помещение обычных данных в структуру расчета. Эквивалентно построению вычисления, тип функции примерно такой: a -> m a.
a — произвольное значение, а m a — вычисление, которое может быть произведено на основе a.
Отношения между конструктором здесь и методом конструктора в классах ES2015 и методом конструктора в функциональном прототипе заключается в том, что последний является одним из вариантов первого. Конструктор выражает универсальное строительство, а конструктор в классе является одним из универсальных конструкций, которые используются для построения экземпляра класса.
Второй компонент паттерна комбинатора, комбинаторы. Он отвечает за преобразование одной вычислительной структуры в другую вычислительную структуру или объединение двух вычислительных структур в одну. В любом случае это переход от вычислительной структуры к вычислительной структуре. Тип функции выглядит так: m a -> m b.
m a — вычисление, которое может быть произведено на основе a, а m b — вычисление, которое может быть произведено на основе b. m a -> m b преобразует вычисление, которое может быть произведено на основе a, в вычисление, которое может быть произведено на основе b.
Если вы думаете, что это абстрактно и странно, посмотрите на пример ниже, чтобы почувствовать это.
Как упоминалось ранее, Конструктор и Комбинаторы не являются фиксированными, все зависит от нас, чтобы определить и удовлетворить условия. Нам не нужно использовать класс, мы определяем итератор для представления вычислительной структуры, которая вычисляет следующее значение при вызове следующего метода.
Итератор можно рассматривать как {next}, у которого есть метод next, выполняющий фактические вычисления.
Что ж, конструктор — это то, что превращает a в итератор. Все функции-генераторы здесь являются естественными конструкторами. Поскольку параметр функции-генератора равен а, возвращаемое значение — итератор. Таким образом, выполняется: a -> {next}.
Наша функция incre является конструктором структуры вычисления итератора. Вы задаете ему параметры start, end, step, и он возвращает структуру вычисления итератора. При повторном вызове next он вычисляет каждое допустимое значение.
Наша функция decre также является конструктором итератора, содержащиеся в ней вычисления обратны функции incre, одна от малого к большему, а другая от большого к меньшему.
Наша функция диапазона тоже является Конструктором, хотя и не Генераторной функцией, хотя в ней используются другие Конструкторы. Это не меняет тип его функции, он по-прежнему создает итератор на основе параметров начала, конца и шага. Структура вычислений, которую он содержит, заключается в том, что если start
И наша функция-переключатель — это не просто конструктор. Он принимает две структуры вычисления итератора и возвращает новую структуру вычисления итератора. Вычисление, содержащееся в новой структуре, заключается в непрерывном переключении вычислений в a и b до тех пор, пока не будут израсходованы все значения.
Если хотите, можете последовать за так называемым Higher Order Component в React и назвать его Higher Order Constructor, то есть конструктор более высокого порядка. Однако слово Combinator для меня лучше.
Конструктор и комбинатор являются функциями, и обе возвращают структуру вычисления (здесь итератор).Разница между ними заключается в том, что параметры конструктора не являются структурами вычисления, а параметры комбинатора содержат структуры вычисления. Это полезное различие.
Constructor :: a -> m a
Combinator :: m a -> m b
С диапазоном и переключателем мы можем легко создать новый конструктор. Спред внутренне основан на диапазоне и переключении и реализует структуру расчета переключения между серединой -> начало и серединой -> вершиной.
Это именно то, что нам нужно для алгоритма развернутого цикла в середине.
Как видите, мы не используем много локальных переменных в функции, изменяем значения переменных в соответствии с различными условиями в цикле for, а затем решаем разовую задачу. Мы просто реализуем их в соответствии с поведением, описанным именами функций, такими как увеличение, уменьшение, диапазон, переключение, распространение и т. д.
Мы решили несколько задач поэтапно. Они являются общими и могут быть повторно использованы в других сценариях. И любая функция incre, decre, range, toggle, spread соответствует принципу программирования: делать только одну вещь и делать ее хорошо; каждую из них можно протестировать по отдельности.
Мы больше не теряемся в отслеживании локальных переменных внутри сложных функций и не тратим много времени на отладку. Мы можем легко расширять новые конструкторы и комбинаторы по мере необходимости. Например:
Комбинатор карт реализуется всего в 3 строчках кода, а возвращаемый им результат вычисления определяется функцией параметра f. В приведенном выше примере число удваивается.
Пока есть смысл прицела rxjs?
Да, мы также можем легко реализовать операторы в rxjs, такие как filter и take.
Когда несколько комбинаторов должны работать вместе, нам нужно написать канал вспомогательной функции, чтобы его было легче читать. В будущем, после того, как фича Pipeline Operators будет доработана, трубу можно будет опустить и заменить символом |>.
Как вы можете видеть, код для карты, фильтра, взятия и т. д. Combinators чист и просто делает то, что должен делать.
Такая операция, как concat, проще, всего два for-of + простой выход.
Итак, какова связь между нашим итератором и rxjs? Почему их API могут быть такими последовательными?
На этот вопрос можно ответить очень просто, rxjs также является шаблоном комбинатора. Как правило, их API-интерфейсы могут быть похожими.
rxjs of, Observable.create и другие функции принадлежат конструкторам. Такие функции, как concat, merge, CombineLatest и т. д., являются комбинаторами. Все операторы Rxjs являются комбинаторами.
Так называемые операторы, его тип примерно таков: a -> m b -> m c. И concat равно (ma, m b) -> m c. На вид разные. На самом деле, рассматривайте несколько одиночных параметров функций высшего порядка как частный случай многопараметрических функций. Или думайте о функциях с несколькими параметрами как о частных случаях нескольких функций с одним параметром более высокого порядка. Они согласны. Ничего, кроме каррирования или отмены каррирования друг друга, этот процесс не производит существенных вычислений (т.е. конечный результат одинаков).
Кроме того, если вы сравните вычислительную структуру rxjs и нашего итератора. Вы заметите, что у итераторов есть только вычисления {next}. Структура, содержащаяся в rxjs, намного сложнее.Во-первых, его a->{subscribe} возвращает структуру, на которую можно подписаться.
В subscribe({ next, complete, error }) передается структура, содержащая три вычисления: next, complete и error, и возвращается unsubscription для отмены подписки.
Следовательно, вычислительная мощность, содержащаяся в rxjs, гораздо более иерархична и шире, чем в итераторе. Чтобы использовать итератор для реализации сложных вычислений в rxjs, вам придется самостоятельно выполнять много дополнительной обработки.
Как видите, с точки зрения вычислительной структуры у нас есть надежный способ анализа фактической выразительности библиотеки. Мы знаем, что если два API выглядят одинаково, это не означает, что у них одинаковые возможности.
Итак, мы знаем, что такое конструктор и комбинатор и что такое примитивы?
Это самая интересная часть комбинаторного паттерна.
В предыдущей статье мы видели, что новые конструкторы и комбинаторы можно писать непрерывно. пока они удовлетворяют требованиям к типу и поведению a -> m a и ma -> m b. Это открытая перспектива. Мы также видели, что можно использовать другие конструкторы и комбинаторы внутри одного конструктора или комбинатора. На данный момент у нас есть конвергентная перспектива.
Какие конструкторы и комбинаторы нельзя комбинировать с другими конструкторами и комбинаторами?
Можем ли мы найти набор конструкторов и комбинаторов, из которых можно построить другие конструкторы и комбинаторы?
Если бы мы могли, мы могли бы назвать их Примитивами.
Например, вычислительные процессы, содержащиеся в наших incre и decre, настолько похожи, кто такие Примитивы?
Ответ заключается в том, что с обратным комбинатором, реализующим либо incre, либо decre, один реализует другой.
Не все примитивы являются врожденными или необходимыми; во многих случаях они связаны с нашим собственным выбором. Это не должно казаться капризным и игривым, это драгоценная характеристика. Некоторые примитивы сложнее реализовать, а другие проще и могут выражать друг друга. Так что у нас есть возможность выбрать тот, который прост в реализации.
Что более интересно, иногда конструктор можно реализовать вручную, даже если он может состоять из других примитивов и комбинаторов.
Например, когда мы используем incre и reverse для реализации decre, он получает тот же результат, но стоимость отличается. Реверс внутренне полностью включает все вычисления в приращениях, а затем инвертирует вывод. Нельзя сказать, что пока первый, происходит только одно вычисление. Реализованный вручную декре может быть рассчитан по запросу.
Мы можем думать, что примитивы и комбинаторы дают нам некоторые бесплатные API, и мы можем создавать более сложные вычислительные структуры бесплатно или дешево. Но бесплатность имеет свою цену. На этапе быстрого прототипирования мы можем использовать паттерн комбинатора, чтобы быстро получить доступную вычислительную структуру; когда функция станет стабильной, мы можем провести рефакторинг и оптимизировать некоторые конструкторы и комбинаторы вручную.
Как показано выше, вместо использования диапазона и переключателя для создания спредов мы напрямую встраиваем содержащиеся в них вычисления, снижая нагрузку на производительность.
Мы были удивлены, обнаружив, что эта рефакторинговая версия была именно тем, что мы хотели, с локальными переменными и циклами в первую очередь? В то время мы долго отлаживали и ничего не получили, а сейчас это легко получить, просто распутав несколько Конструкторов и Комбинаторов. Это удивительный процесс.
Размышляя над этим, мы обнаружим, что прямое использование нескольких локальных переменных и циклов для реализации — это, по сути, процесс проб и ошибок грубой силы, чтобы выяснить, сколько вычислений требуется для распространения. Взаимодействие между несколькими локальными переменными затрудняет отладку кода.
Если мы примем шаблон комбинатора, мы сможем изолировать множество конструкторов и комбинаторов, которые имеют меньше кода и могут быть протестированы по отдельности, а также могут быть объединены в более сложные вычислительные структуры. Когда мы используем их для составления разворота, мы знаем, какие нужны вычисления в развороте, и тогда мы можем провести чистую, ритмичную, спланированную оптимизацию рефакторинга.
И, зная отношения между Примитивами, мы также можем рассуждать о новых идеях. Например, мы уже знаем, что incre и decre могут реализовывать друг друга через reverse. Следовательно, нам не нужно отслеживать две локальные переменные incre и decre, нам нужно отслеживать только одну, а затем получать другую с помощью операций реверса/инверсии.
Как показано выше, мы отслеживаем только локальные переменные COUNT, а Incre и Decre получаются через + смещение и -смещение, и достигается тот же алгоритм.
От почесывания ушей и почесывания щек методом слепых проб и ошибок ничего не вышло, теперь мы можем реализовать функцию распространения тремя разными способами. В этом сила комбинаторного паттерна. Как методология, она может привести и вдохновить нас на решение проблем, которые не могли быть решены раньше, и лучше решать проблемы, которые уже были решены.
То, что показано в этой статье, — это лишь верхушка айсберга функционального программирования. Нас ждет более полезное оружие. Вы можете получить больше навыков, изучая функциональные языки, такие как Haskell. Затем применяйте его в повседневной работе, например, при разработке внешнего интерфейса, чтобы оптимизировать наш код.
Если вам интересно, как это может выглядеть, когда вы пишете на Haskell, картинка выше — это упрощенный перевод. Переведите версию JS на Haskell.
Чтобы узнать больше о случаях применения Combinator Pattern, вы можете нажать "Демистификация наиболее потенциального API Vue-3.0«Содержимое просмотра, которое рассматривает значение реактивности как структуру расчета и сочетает в себе более сложные структуры расчета, такие как представление реактивности.