Компоненты. Сегодня они повсюду. JavaScript их часто использует. Объектно-ориентированное программирование тоже делает это. И они правы.

Vous pouvez retrouver la version française de cet article sur mon blog для всех франкофонов! Эта статья взята из французской версии моего блога здесь.

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

Компоненты - прекрасный тому пример. Поскольку они вездесущи в JavaScript и полностью отсутствуют в вязе, новички часто недоумевают. Позволь мне объяснить:

В зависимости от того, как фреймворки (например, React) определяют компоненты, они представляют собой заранее определенные повторно используемые блоки, способные взаимодействовать с другими компонентами. Это те компоненты, о которых я говорю.

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

Возьмем конкретный пример. Согласно предыдущему определению, каждый процесс в многопоточной программе является компонентом, когда они могут взаимодействовать друг с другом. На более высоком уровне сервис в архитектуре микросервисов также является компонентом, особенно когда они обмениваются данными друг с другом через REST.

Компонент всегда определяется определенной характеристикой: он взаимодействует с другими компонентами (и большую часть времени имеет внутреннее состояние).

В типичном одностраничном приложении React, не использующем редукторы, такие как Redux, «компонент» имеет смысл. Каждая часть приложения имеет внутреннее состояние, функцию рендеринга и коммуникационный «интерфейс». Он может иметь определенное поведение и взаимодействовать со всеми остальными компонентами. Когда мы говорим об объектах в целом, «компонент» действительно идеально подходит, потому что объектно-ориентированное программирование было создано с учетом внутреннего состояния и передачи сообщений.

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

Поскольку пример лучше, чем длинная речь, продолжайте работу с веб-приложением, созданным с помощью elm. В вязе все данные остаются в огромной записи: модели. Каждое событие может изменить состояние приложения. Новая модель создается в зависимости от старой модели и полученного события. С помощью этой новой модели можно создать новое представление для приложения. В этом процессе нет коммуникации или передачи сообщений. Фактически, единственное сообщение приходит из внешнего мира. Из портов и всего остального. Согласно предыдущему определению, само приложение представляет собой огромный компонент, способный взаимодействовать с остальной частью JavaScript и веб-страницей.

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

Давайте продолжим работу с приложением вяза. Во время генерации представления (т. Е. Генерации HTML) мы часто разделяем данные в зависимости от того, как мы их визуально используем. Если на панели инструментов находится средство выбора даты, мы обычно помещаем данные даты в структуру панели инструментов модели. А что, если эти данные также пригодятся в нижнем колонтитуле приложения? Новички часто хотят использовать одну и ту же дату повторно. И они правы. Однако, поскольку данные существуют только на панели инструментов, они предполагают, что должны сгенерировать сообщение для передачи данных в нижний колонтитул с датой с панели инструментов. Это тот самый случай, когда нижний колонтитул рассматривается как независимый компонент от панели инструментов. Но, как мы уже видели, в вяз не бывает компонентов. Почему такая проблема?

Возьмем более простой пример. (Конечно, пример тривиален, но проблема межкомпонентного взаимодействия присутствует.)

type alias Model =
  { toolbar :
    { date : Date
    , color : Color
    }
  , footer :
    { date : Date }
  }

Представим себе функцию просмотра, тоже простую, но отражающую проблему:

view : Model -> Html msg
view { toolbar, footer } =
  Html.div []
    [ toolbarView toolbar
    , footerView footer
    ]
toolbarView : { date : Date, color : Color } -> Html msg
toolbarView { date, color } =
  Html.div []
    [ datepicker date ]
footerView : { date : Date } -> Html msg
footerView { date } =
  Html.div []
    [ dateView date ]

Мы сразу увидели проблему. Дата здесь используется дважды, и мы можем представить функцию обновления, обновляющую панель инструментов. Как отправить информацию в нижний колонтитул с новой датой?

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

view : Model -> Html msg
view { toolbar, footer } =
  Html.div []
    [ Html.div []
      [ datepicker toolbar.date ]
    , Html.div []
      [ dateView footer.date ]
    ]

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

view : Model -> Html msg
view { toolbar } =
  Html.div []
    [ Html.div []
      [ datepicker toolbar.date ]
    , Html.div []
      [ dateView toolbar.date ]
    ]

Вдруг проблема исчезает! Дата на панели инструментов, ранее недоступная в toolbarView, становится доступной напрямую. В то же время исчезает проблема со связью. Настоящий вопрос сводится к следующему: «Как продолжить, чтобы получить мои структуры панели инструментов и нижнего колонтитула?». Это вопрос уже не о языке, а о дизайне. Путем слияния двух структур панели инструментов и нижнего колонтитула раскрывается бесполезность записи нижнего колонтитула; когда такая структура была бы необходима в модели, основанной на компонентах. Пора переписать модель.

type alias Model =
  { date : Date
  , color : Color
  }

С исчезновением обеих моделей панели инструментов и нижнего колонтитула структура стала более компактной, а код - намного проще:

view : Model -> Html msg
view { date, color } =
  Html.div []
    [ toolbarView date color
    , footerView date
    ]
toolbarView : Date -> Color -> Html msg
toolbarView date color =
  Html.div []
    [ datepicker date ]
footerView : Date -> Html msg
footerView date =
  Html.div []
    [ dateView date ]

И вуаля! Код стал проще, необходимость в общении отпала!

В общем, «потребность в коммуникации между компонентами» является симптомом более широкой проблемы понимания. Симптом проблемы дизайна. Когда мы хотим уменьшить объем каждой переменной, мы в конечном итоге хотим сделать коммуникационные части кода, которые в этом не нуждаются. Затем важно определить общие ценности и объединить их. Мы должны переработать модель, еще раз подумать о данных. Приведенный выше код - прекрасный пример. В этом примере это кажется очень простым, но представьте себе модель с 4 уровнями вложенности, каждый раз с 6 полями. При сложной разработке легко упустить то, что очевидно в программе с несколькими строками кода.

-- Yes, I'm talking about a model like this.
type alias Model =
  { home : Home
  , articles : Articles
  }
type alias Articles =
  { user : User
  , content : List Article
  }
type alias Article =
  { title : String
  , text : Content
  , userId : Int
  }
type alias Content =
  { images : List String
  , texts : List String
  }
type alias User =
  { id : Int
  , name : Maybe String
  }

В приведенной выше модели мы можем отметить, что в дизайне не было мозгового штурма. Действительно, статья всегда принадлежит пользователю. Тогда почему у него userId? Почему статьи не входят непосредственно в структуру User? В первый раз это не кажется серьезным. Но что будет при добавлении функций? Требуется ли searchUserById каждый раз, когда мы получаем статью? Придется ли нам добавлять userId для всего, что принадлежит пользователю? Это может быстро привести к проблемам со связью или передачей данных (что приведет к созданию сообщения в elm)…

В заключение я хотел бы настоять на том, что представление - это только один из способов отображения данных из модели. Для той же модели могут быть другие виды. Мнение основывается на данных, а не наоборот. Это означает, что в приложении elm модель всегда должна разрабатываться до представления, потому что вы можете легко делать с моделью все, что хотите. В зависимости от вида, используемого для рендеринга, это только конкретное видение, всегда усеченное из модели. На странице редактирования профиля пользователя нас не интересуют последние тенденции на Medium. Мы сосредоточены на редактировании нашего профиля. При просмотре последних тенденций на Medium нас не волнует, как выглядит наш профиль. Но все эти данные, от информации о пользователях до статей о последних тенденциях, находятся в браузере, в оперативной памяти. Это всего лишь выбор, выбрать только часть модели, потому что вид позволяет это. Использование только части модели позволяет пользователю сосредоточиться на том, что для него важно. Представление должно отражать их не потому, что данные в нашем приложении организованы определенным образом. Представление максимально не связано с данными.

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

Эта статья пришла мне в голову после моих дискуссий на встрече вязов в Париже (на которой вас, конечно, ждут!). Многие новички постоянно говорили о компонентах и ​​пытались отправлять сообщения между компонентами, как в React. Но все это не имело смысла. Через несколько дней я наконец смог сформулировать то, что меня беспокоило, в надежде успокоить всех, кто хочет заняться функциональным программированием.