Вступление

В этой статье мы обсудим возможные этапы развития простого приложения React с некоторыми интерактивными счетчиками. Мы начнем с довольно простой реализации, построенной на JavaScript (ES6), а затем добавим поверх нее мощные абстракции, чтобы уменьшить количество ошибок и воспользоваться некоторыми обобщениями, чтобы писать меньше шаблонного кода. Первый элемент, который мы представим, - это статическая типизация в сочетании с TypeScript, и мы увидим, как дополнительная структура, предоставляемая выразительной и ненавязчивой системой типов, спасает нас от некоторых категорий ошибок. По мере того, как мы продолжаем использовать TypeScript и React вместе, мы также начнем замечать некоторые шаблоны, которые появляются во многих местах нашего кода. Чтобы напрямую выразить эти шаблоны и, следовательно, написать более короткий и более полезный код, мы представим некоторые рассуждения более высокого порядка в форме монад, и, в частности, библиотеку monadic react (доступную через npm и github ).

Мы предполагаем некоторое базовое понимание JSX JavaScript, и, хотя мы будем объяснять основные концепции React, мы не будем углубляться в структуру фреймворка и предположим, что аспекты React, которые читатель все еще находит неясными, лучше объяснены в другом месте.

Ванильная реакция

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

import * as React from "react"
import * as ReactDOM from "react-dom"
class Sample extends React.Component {
  constructor(props, context) {
    super(props,context)
this.state = {}
  }
render() {
    return <div>
        Hello world!
      </div>
  }
}

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

class Sample extends React.Component {
  constructor(props, context) {
    super(props,context)
    this.state = { counter:0 }
  }
render() {
    return <div>
        Hello world {this.state.counter} times!
      </div>
  }
}

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

...
  render() {
    return <div>
        Hello world {this.state.counter} times!
        <button onClick={() => 
          this.setState({...this.state,               
                            counter:this.state.counter+1}) }>
          +1
        </button>
      </div>
  }
}

В этот момент мы замечаем кое-что особенное: внутри кнопки обратного вызова onClick вместо простого изменения состояния мы должны вызвать метод setState, чтобы сделать это за нас. Это выдает гибридную функциональную / объектно-ориентированную природу фреймворка, поскольку позволяет нам видеть, что операции, влияющие на состояние, должны быть опосредованы уровнем, который будет обрабатывать распространение изменений за нас и который эффективно заставляет React вести себя так, как если бы он был повторен. создание экземпляров и повторный рендеринг компонентов с новым состоянием вместо выполнения простого локального изменения переменной экземпляра.

Нетривиальный пример

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

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

Это приводит нас к следующей реализации Counter:

export class Counter extends React.Component {
  constructor(props, context) {
    super(props,context)
    this.state = {}
  }
render() {
    return <div>
      Hello world, {this.props.counter} times.
      <button onClick={() => this.props.increment() } >+1</button>
      </div>
  }
}

Обратите внимание, что состояние счетчика теперь пусто, и при рендеринге сообщения выполняется поиск значения счетчика в this.props.counter вместо this.state.counter. Более того, когда нажимается кнопка +1, мы просто вызываем this.props.increment, тем самым делегируя родительскому компоненту задачу увеличения счетчика. Это освобождает Counter компонент от любых знаний о хранении и извлечении данных, а это означает, что мы приобрели гибкость хранения счетчика в базе данных, локальной переменной или любой другой системе хранения, о которой мы могли бы подумать.

Конечно, в нашей простой реализации хранилище будет просто выполняться в состоянии родительского компонента, который, следовательно, также является контейнером. Состояние будет иметь несколько счетчиков и, следовательно, будет создавать экземпляр компонента Counter несколько раз:

export class Sample extends React.Component {
  constructor(props, context) {
    super(props,context)
    this.state = { counter1:0, counter2:0 }
  }
render() {
    return <div>
        <Counter counter={this.state.counter1} increment={() =>
          this.setState(Object.assign({}, 
           this.state, {counter1: this.state.counter1+1}))} />
        <Counter counter={this.state.counter2} increment={() =>
          this.setState(Object.assign({}, 
           this.state, {counter2: this.state.counter2+1}))} />
      </div>
  }
}

Обратите внимание, что каждый Counter компонент получает то, что мы можем видеть как пару геттер / сеттер в свойствах: counter - это переменная только для чтения, следовательно, геттер, тогда как increment - это метод записи в counter, следовательно, (ограниченный) сеттер.

Первый выпуск

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

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

export class Sample extends React.Component {
  constructor(props, context) {
    super(props,context)
    this.state = { counter1:0, counter2:0, counter3:0 }
  }
render() {
    return <div>
        <Counter counter={this.state.counter1} increment={() =>
          this.setState(Object.assign({}, this.state, {counter1: this.state.counter1+1}))} />
        <Counter counter={this.state.counter2} increment={() =>
          this.setState(Object.assign({}, this.state, {counter2: this.state.counter2+1}))} />
        <Counter counter={this.state.counter3} incr={() =>
          this.setState(Object.assign({}, this.state, {counter3: this.state.counter3+1}))} />
      </div>
  }
}

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

Введите проверку типа

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

Более простым решением было бы воспользоваться преимуществами самого компилятора, чтобы автоматизировать такую ​​небольшую проверку. Мы могли бы просто «аннотировать» наши компоненты таким образом, чтобы указать, какие данные они ожидают в свойствах, а затем позволить языку проверять правильность перед запуском программы и уведомлять нас о найденных проблемах. даже без проведения ни одного теста.

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

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

TypeScript спешит на помощь

TypeScript - это статически типизированное расширение JavaScript. Это означает, что рядом с кодом JavaScript, который является подмножеством TypeScript, мы также добавляем аннотации типов, которые определяют структуры, которые мы ожидаем от наших данных (переменных, параметров и т. Д.).

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

module Basics {
  let x1:number = 100
  let x2:string = "a string"
  // WARNING: THIS DOES NOT WORK! let x3:string = true
}

Затем язык гарантирует, что все, что мы выбираем для присвоения переменной, действительно соответствует структуре, указанной в объявлении типа. Если компилятор замечает несоответствие, пользователю будет выдана ошибка компилятора:

let x3:string = true // THIS PRODUCES A COMPILER ERROR!

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

module SimpleTypes {
  export type Student = { name:string, surname:string, average:number }
  export type Teacher = { name:string, surname:string, subject:string }
  export let pietje:Student = { name:"Pietje", surname:"Ejteip", average:7 }
  export let jannetje:Teacher = { name:"Jannetje", surname:"Ejtennaj", subject:"Quantum rocket surgery" }
}

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

let jannetje_the_second:Teacher = { name:"Jannetje", surname:"Ejtennaj", average:7 }

Реагировать и TypeScript

Конечно, мы можем комбинировать TypeScript и React. Мы делаем это с помощью внешней библиотеки, которая содержит определения типов для различных данных React. Основываясь на этих определениях, мы можем направить компилятор TypeScript так, чтобы он проверял наши программы с помощью React, сравнивая наши вызовы с предписанными структурами.

Поскольку мы знаем, что компоненты React содержат как состояние, так и свойства, теперь они будут указаны как параметры универсального типа для класса React.Component. Несмотря на то, что код для контейнера Sample в основном такой же, теперь мы можем явно указать тип свойств и состояние:

type SampleState = { counter1:number, counter2:number }
type SampleProps = {  }
export class Sample extends React.Component<SampleProps, SampleState> {
  constructor(props, context) {
    super(props,context)
    this.state = { counter1:0, counter2:0 }
  }
render() {
    return <div>
        <Counter counter={this.state.counter1}
          increment={() => this.setState({...this.state, counter1: this.state.counter1+1})} />
        <Counter counter={this.state.counter2}
          increment={() => this.setState({...this.state, counter2: this.state.counter2+1})} />
      </div>
  }
}

Если бы мы сейчас попытались изменить инициализацию состояния в конструкторе на что-то бессмысленное, то мы получили бы ошибку компилятора:

this.state = { counter1:0, cnt2:0 } // THIS DOES NOT COMPILE!

Компонент Counter также обрабатывается таким же образом, указав, что он не имеет внутреннего состояния, а конкретный интерфейс поступает через свойства:

type CounterState = { }
type CounterProps = { counter:number, increment:() => void }
export class Counter extends React.Component<CounterProps, CounterState> {
  constructor(props, context) {
    super(props,context)
    this.state = {}
  }
  render() {
    return <div>
      Hello world, {this.props.counter} times.
      <button onClick={() => this.props.increment() } >+1</button>
      </div>
  }
}

Если мы неправильно используем свойства внутри компонента Counter, тогда ошибка компилятора предупредит нас о проблеме и предложит исправить ее. Более того, если бы мы создали экземпляр компонента Counter из компонента Sample, но с неправильными свойствами, то мы также получили бы ошибку, тем самым гарантируя, что граничные ожидания между компонентами не нарушаются:

<Counter counter={this.state.counter2}
          incr={() => this.setState({...this.state, counter2: this.state.counter2+1})} /> // THIS DOES NOT COMPILE!

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

Монадическая реакция

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

Более того, дополнительная многословность вступает в игру, когда мы учитываем необходимость объявить два типа (один для свойств, один для состояния), один класс с его конструктором и т. Д.

Monadic React - это библиотека, которая объединяет TypeScript и React в библиотеку потока данных, которая подчеркивает типобезопасную композицию компонентов React с помощью наиболее распространенного механизма из всех: функций. Компонент Monadic React рассматривается как нечто, что принимает в качестве входных данных в одном формате, скажем, T, и производит выходные данные в другом формате, например, U. В TypeScript и React это станет по крайней мере одним свойством типа T и по крайней мере одним обратным вызовом типа U, объединенным в компонент, унаследованный от React.Component<{ in: T, out:U }, {}>.

Monadic React понимает, что этот шаблон настолько важен, что он встроен в гораздо более простую сигнатуру: T => C<U>, то есть функция, которая принимает в качестве входных данных значение типа T (в конце концов, это входные данные!) И возвращает C<U>, ( C - это фактическая структура данных Monadic React), которая является оболочкой Monadic React компонента React, который производит (в обратном вызове) значения типа U, которые являются результатом внутренней обработки, выполняемой компонентом. Способ получения этого результата (ввод пользователя, вызовы API и т. Д.) Абстрагируется на этом уровне: мы заботимся только о том, что компонент производит данные определенного типа, которые затем перенаправляются в другие компоненты. C<A> в каком-то смысле очень близок к такому типу, как Promise<A>, но заключен в аромат React. C<A>, как и Promise<A>, имеет метод цепочки (называемый then) и будет генерировать значения типа A и передавать их в обратный вызов, указанный в then. Основное различие между Promise и C состоит в том, что C также может отображать что-то для пользователя, а Promise - нет. В остальном они эквивалентны, и действительно можно преобразовать Promise<A> в C<A> без потери информации и функциональности (тогда как обратное невозможно, поскольку Promise ничего не может отобразить).

Примитивы

Примитивные типы данных управляются готовыми компонентами. Все эти компоненты имеют одну и ту же сигнатуру: A => C<A>, где A - это обрабатываемый примитивный тип. Входными данными является значение, которое мы хотим показать пользователю, и, конечно же, каждое действие пользователя с этим компонентом создает новое значение типа A.

Например, компонент Monadic React, который одновременно показывает и позволяет манипулировать пользователем, строка текста будет иметь подпись string => C<string>, компонент числа будет иметь подпись number => C<number> и так далее.

Компонент string работает со строками, но перед тем, как дать нам желаемый string => C<string>, ему требуются некоторые параметры конфигурации. Таким образом, полная подпись компонента string:

string: (mode: Mode, type?: StringType, key?: string, dbg?: () => string) => (value: string) => C<string>

Сначала мы передаем Mode в качестве входных данных, это либо "view", либо "edit". Затем мы определяем тип входного объекта, например "text", "url" и т. Д. Наконец, мы предоставляем React key, который необходим React для последовательного сопоставления этого компонента с правильными элементами DOM.

Пример использования string был бы довольно простым:

string("view", "text", "hello_world")("Hello world!")

который покажет нам ожидаемое обычное приветственное сообщение.

Мы можем встроить это в существующее приложение React, вызвав:

{ MonadicReact.simple_application(
    string("view", "text", "hello_world")("Hello world!"), 
    x => console.log(JSON.stringify(x))) }

в любом месте нашего обычного кода React, тем самым получая:

Благодаря выводу типа нам не нужно указывать общий аргумент для simple_application, поскольку компилятор может легко угадать, что это string.

Обратите внимание, что в консоли JavaScript мы теперь получаем некоторый вывод, потому что компонент string передает свой ввод, который MonadicReact.simple_application затем передает его второму параметру (обратный вызов, который печатает значение для удобства отладки).

Мы можем захотеть подавить это поведение, попросив компонент string никогда не транслировать какие-либо данные, просто заявив:

string("view", "text", "hello_world")("Hello world!").never()

Консоль теперь пуста:

Состояние / повтор

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

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

string("edit", "text", "hello_world")("Hello world!")

К сожалению, при попытке редактирования мы заметим, что текст в поле ввода не меняется. Если мы откроем консоль, то увидим, что компонент выдает какой-то вывод после каждой попытки редактирования. Тогда действительно имеет смысл, что компонент все еще показывает "Hello world!" (это именно то, что говорится в коде!) И транслирует изменения всем, кто его слушает. Наша цель затем состоит в том, чтобы настроить экземпляр нашего string компонента так, чтобы результат каждого редактирования также передавался обратно в string, чтобы он мог обновляться.

Этот вид «цикла данных» реализуется компонентом более высокого порядка: repeat. repeat передает данные обратно в компонент, а также транслирует все изменения своим собственным слушателям. Просто написав:

repeat<string>("hello_world_repeater")(
  string("edit", "text", "hello_world")
)("Hello world!")

Тогда добиваемся желаемого результата:

Обратите внимание на некоторые особенности приведенного выше кода. Прежде всего, мы указываем тип данных, которые repeat будут хранить как общий параметр. Поскольку типы и значения существуют в разных пространствах имен, нет никакой путаницы между string (тип) и string (компонент Monadic React).

Также обратите внимание, что теперь мы передаем начальную строку "Hello world" в repeat, а не в сам string. Таким образом, мы просто сообщаем repeat, что такое начальное состояние, которое сразу же передается внутреннему компоненту, а затем всякий раз, когда внутренний компонент создает новую строку, мы отбрасываем старую и вместо нее используем новую.

Мультиплексирование / любое

Хорошо, пока все хорошо: мы можем поместить строку на экран и сделать ее редактируемой. Мы также можем использовать другие комбинаторы, такие как number, date_time, bool, и даже поэкспериментировать с selector или multi_selector. Все основные элементы есть, с правильным монадическим интерфейсом.

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

Мы определяем тип данных, содержащий данные формы:

type FormData = { n:number, s:string }

Затем мы используем комбинатор any для создания экземпляров двух компонентов в правом элементе состояния. any принимает в качестве входных данных массив функций общего типа A => C<B> и объединяет их вместе в единый A => C<B>, который передает свой вход типа A всем функциям массива параметров, а затем выдает в качестве собственного вывода выходные данные первая функция параметра, которая сама произвела вывод. Это очень похоже на Promise.race.

В результате получается следующий код:

any<FormData, {}>("form_any")([
  fd => string("view", "text", "form_s")(fd.s).never(),
  fd => number("view", "form_n")(fd.n).never()
])({ n:0, s:"Hello form!" })

Обратите внимание, что комбинатор any обертывает функции, в которых входные и выходные данные могут отличаться, поэтому мы должны указать два аргумента универсального типа: один для ввода, один для вывода. Поскольку нас не волнует вывод, мы используем пустой тип {}.

Чтобы сделать эту форму редактируемой, мы не можем «просто» изменить режим с "view" на "edit". Мы также должны преобразовать вывод компонентов string и number обратно в FormData. Это просто делается путем сопоставления каждого компонента так, чтобы он давал FormData:

any<FormData, FormData>("form_any")([
  fd => string("edit", "text", "form_s")(fd.s)
        .map(s => ({...fd, s:s})),
  fd => number("edit", "form_n")(fd.n)
        .map(n => ({...fd, n:n}))
])({ n:0, s:"Hello form!" })

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

repeat<FormData>("form_repeat")(
  any<FormData, FormData>("form_any")([
    fd => string("edit", "text", "form_s")(fd.s)
            .map(s => ({...fd, s:s}), "form_s_map"),
    fd => number("edit", "form_n")(fd.n)
            .map(n => ({...fd, n:n}), "form_n_map")
  ])
)({ n:0, s:"Hello form!" })

Благодаря тому, что каждый компонент и комбинатор (а также два maps) имеют ключ, мы не получаем раздражающего предупреждения React и получаем желаемый результат:

Преобразование / отзыв

Отдельные элементы только что построенной формы следуют шаблону, который настолько распространен, что имеет свой собственный комбинатор. Давайте сначала разберем узор:

fd => string("edit", "text", "form_s")(fd.s)
          .map(s => ({...fd, s:s})

Начнем со «слишком большого количества данных»: параметр fd, имеющий тип FormData, содержит больше, чем требуется string. string может рисовать только текст, а не FormData, поэтому нам нужен способ выполнения преобразования FormData => string. Мы видим это неявно в действии в выражении fd.s, которое проецирует необходимые данные из более крупной структуры. Когда у нас есть результат редактирования, в нашем случае новая строка s, нам нужно вернуть его в FormData. Мы делаем это, создавая новый экземпляр FormData, который содержит все исходные данные, с одним изменением: s обновляется. Мы видим это внутри комбинатора map, который вставляет s внутрь fd.

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

retract: <A, B>(key?: string) => 
  (inb: (_: A) => B, out: (_: A) => (_: B) => A, 
   p: (_: B) => C<B>) => (_: A) => C<A>

Первый аргумент retract - обычный key. Это просто необходимо React. inb - это проекция из более крупной структуры данных A в более простую B, необходимую нашему внутреннему компоненту. out - это встраивание B в существующий A для создания обновленного A. Наконец, p - это внутренний компонент, который не понимает большего A, но понимает более простой B.

В нашем примере мы избавляемся от map и получаем:

retract<FormData, string>("form_s_retract")(
  fd => fd.s, fd => s => ({...fd, s:s}),
  string("edit", "text", "form_s"))

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

Вернуться к прилавку

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

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

type Counter = { counter:number }

Компонент счетчика - это просто any строки (которая сообщает нам, сколько раз мы подсчитали до сих пор, и закрывается never, поскольку нам не важен ее вывод) и кнопка (которая увеличивает счетчик на единицу):

let counter : (_:string) => (_:Counter) => C<Counter> = k =>
  any<Counter, Counter>("counter_any")([
    c => string("view")(`Hello world, ${c.counter} times.`).never("counter_never"),
    retract<Counter, number>("counter_retract")(
      c => c.counter, c => cnt => ({...c, counter:cnt}),
      n => button<number>("+1")(n + 1))
  ])

Обратите внимание, что button просто принимает значение (в нашем случае n+1) и возвращает его при нажатии.

Затем мы помещаем два счетчика в отдельный компонент с отдельным состоянием:

type TwoCounters = { counter1:Counter, counter2:Counter }
let counters =
  repeat<TwoCounters>("two_counters_repeater")(
    any<TwoCounters, TwoCounters>("two_counters_any")([
      retract<TwoCounters, Counter>("counter1-retract")(
        tc => tc.counter1, tc => c => ({...tc, counter1:c}), 
        counter("counter1")),
      retract<TwoCounters, Counter>("counter2-retract")(
        tc => tc.counter2, tc => c => ({...tc, counter2:c}), 
        counter("counter2"))
    ])
  )({ counter1:{ counter:0 }, counter2:{ counter:0 } })

Это дает желаемый конечный результат:

Больше комбинаторов HTML

Конечно, мы хотим придать нашей веб-странице желаемую структуру или добавить определенные элементы HTML, которые не учитываются в потоке данных, такие как метки и заголовки. Существует множество «сквозных» декораторов Monadic React, которые не производят собственных данных, а вместо этого просто выводят некоторые элементы HTML, а затем делегируют некоторому внутреннему компоненту работу по фактическому манипулированию данными. Например, декоратор div имеет следующую подпись:

div<A, B>(className?: string, key?: string): (p: (_: A) => C<B>) => ((_: A) => C<B>)

Обратите внимание, что div просто принимает в качестве входных данных компонент p и просто украшает его. Таким образом, div делает вид, что имеет ту же сигнатуру, что и p, но на самом деле все входные данные типа A, переданные div, перейдут в p, а все выходы типа B, произведенные div, фактически исходят от p.

В заключение: на момент написания React 16 только что был выпущен. React 16 позволяет использовать меньше div элементов в абстрактных элементах, таких как retract, any и т. Д. Сейчас идет перенос, который приведет к гораздо более красивому HTML без множества зашумленных монадических элементов, которые нужны только в качестве клея. Эта версия Monadic React все еще не является общедоступной, но будет тщательно протестирована и опубликована в ближайшие недели.

Заключение

Вкратце, обратите внимание на плотность информации нашего кода Monadic React: образец насчитывает примерно половину меньше строк кода, чем типизированная версия, при этом он, по крайней мере, так же безопасен по типу (возможно, больше, поскольку также состав и преобразование компонентов статически типизирован). Используя фреймворки с более высокой плотностью информации, мы можем очень быстро сосредоточиться на выражении информации, которая нам важна, не увязая в шаблонах. На практике это может быть совершенно очевидно: реальный рефакторинг в реальном проекте сократил более 400 строк TypeScript и React до менее 20 строк Monadic React.

Более того, фактическое использование Monadic React на практике показывает, что высокий уровень абстракции вынуждает разработчиков «сначала проектировать свой код», и когда все зависимости и преобразования данных понятны, реализация становится тривиальной задачей. Я не буду делать никаких заявлений о том, что «Monadic React сокращает время разработки на 3000%» или о каком-либо подобном триумфализме, но я буду утверждать, что упор на композицию и поток данных заставляет интересные и сложные части проблемы бросаться в глаза, приводя к чтобы мы работали более прямым, целенаправленным образом, что в руках опытного инженера-программиста действительно приводит к повышению производительности и весьма приятному опыту.

Примечание: весь код, написанный в этой статье, доступен.