DevGang
Авторизоваться

Декларативный JavaScript

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

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

Это может показаться смелым, не так ли? Но как часто вам приходится писать подобный код для переключения видимости компонента в JSX?

<div>
  {hasComments ? <Comments/> : null}
</div>

Мы можем переписать его более декларативным образом:

<div>
  <Comments visible={hasComments}/>
</div>

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

const Comments = ({visible}) => {
  if (!visible) return null
  ...
}

Свойство visible побуждает нас писать код в декларативной манере, даже несмотря на то, что сам компонент частично реализован в императивном стиле.

Давайте взглянем на логический оператор switch, который не только тесно связан с if, но и является любимым среди многих разработчиков:

const App = () => {
  const role = useUserRole()
  let Component
  switch(role) {
    case 'ADMIN': {
      Component = AdminView
      break
    }
    case 'EDITOR': {
      Component = EditorView
      break
    }
    case 'USER': {
      Component = UserView
      break
    }
    default: {
      Component = GuestView
      break
    }
  }

  return (
    <main>
      <NavBar/>
      <Component/>
    </main>
  )
}

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

const App = () => (
  <main>
    <NavBar/>
    <Switch test={useUserRole()}>
      <Case when='ADMIN' use={AdminView}/>
      <Case when='EDITOR' use={EditorView}/>
      <Case when='USER' use={UserView}/>
      <Otherwise use={GuestView}/>
    </Switch>
  </main>
)

Давайте рассмотрим этот код с точки зрения функционального программирования, где теги HTML/JSX рассматриваются как функции, генерирующие выходные данные HTML, а атрибуты тегов действуют как входные параметры функции.

// it will return HTML: <main id="app">Hello World<main/>
main({id: 'app', children: 'Hello world!'})

// The second parameter can be used as a children attribute
main({id: 'app'}, 'Hello world!')

Опираясь на приведенную выше концепцию, давайте представим код JSX через наборы функций. Уместно отметить, что switch/case — это зарезервированные ключевые слова JavaScript, поэтому мы добавим подчеркивание:

const app = () => (
  main(
    navbar(),
    switch_({test: useUserRole()},
      case_({when: 'ADMIN', use: AdminView}),
      case_({when: 'EDITOR', use: EditorView}),
      case_({when: 'USER', use: UserView}),
      otherwise({use: GuestView}),
    )
  )
)

Как видите, декларативный код не всегда означает HTML или JSX. В JavaScript мы можем использовать функции для представления синтаксиса этих языков. Согласно соглашениям JS, функции должны представлять действия и обычно начинаются с глагола, например find или setTitle. Однако в этом случае функции также могут представлять сущность. Например, SQL-запрос можно записать так:

// a function composition
query(
  select('name', 'email', 'country'),
  from('users'),
  where({age: less(21)}),
  groupBy('country'),
)

// or as a chaining function like Promise
select('name', 'email', 'country')
  .from('users')
  .where({age: less(21)})
  .groupBy('country')

// SELECT name, email, country FROM users WHERE age < 21 GROUP BY country

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

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

const selectComponent= ({test, cases, defaultValue}) => {
  const found = cases.find([value] => test === value)
  return found?.at(1) || defaultValue
}

const App = () => {
  const role = useUserRole()
  const Component = selectComponent({
    test: role,
    cases: [
      ['ADMIN', AdminView],
      ['EDITOR', EditorView],
      ['USER', UserView],
    ],
    defaultValue: GuestView,
  })

  return (
    <main>
      <NavBar/>
      <Component/>
    </main>
  )
}

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

const select = (...fns) => value => fns.reduce(
  (found, fn) => found || fn(value), null
)

const when = (test, wanted) => value => {
  const matched = typeof test === 'function' ? test(value) : test === value
  return matched && wanted
}

const selectComponent = select(
  when('ADMIN', AdminView),
  when('EDITOR', EditorView),
  when('USER', UserView),
  () => GuestView,
)

const Component = selectComponent('EDITOR') // -> EditorView

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

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

import {Suspense, lazy} from 'react'

const when = (test, path) => value => (
  test === value && lazy(() => import(path))
)

const selectComponent = select(
  when('ADMIN', './admin-view'),
  when('EDITOR', './editor-view'),
  when('USER', './user-view'),
  () => GuestView,
)

const App = () => {
  const role = useUserRole()
  const Component = selectComponent(role)

  return (
    <main>
      <NavBar/>
      <Suspense fallback={<div>Loading...</div>}>
        <Component/>
      </Suspense>
    </main>
  )
}

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

const between = (min, max) => n => (
  min >= n && n <= max
)

// range of values
const toGrade = select(
  when(val => val > 90, 'A'),
  when(between(80, 89), 'B'),
  when(between(70, 79), 'C'),
  when(between(50, 69), 'D'),
  () => 'F',
)

const grade = toGrade(81) // -> 'B'

В следующем посте мы подробно рассмотрим эту технику на примере создания редюсеров Redux. Мы узнаем, как заменить подход try/catch, и продолжим обсуждение функционального программирования.

const authReducer = createReducer(
  initialState,
  on('SIGN_IN', signIn),
  on('SIGN_OUT', signOut),
  on('SIGN_OUT', clearCookies),
)

Источник:

#JavaScript #React
Комментарии 2
Никита Лебедев 26.10.2023 в 17:02

Статья плохая: Во-первых, в заголовке указан JavaScript, и в первых же примерах используется JSX. Во-вторых, автор предлагает повторно реализовывать то, что и так есть в языке. Может быть, нужно ещё и функцию add реализовать, чтобы складывать два числа? В-третьих, не понятно, почему автор считает проп visible более декларативным, чем тернарный оператор. Я уже не говорю о том, что пример со Switch, Case нельзя нормально перевести на TypeScript, защитившись от опечаток в значениях пропа when

sultan99 31.10.2023 в 21:06

Никита, спасибо за отзыв.

  1. Я использовал пример JSX, чтобы показать плавный переход от декларативного языка к JavaScript. Я представил JSX и SQL в виде функций, чтобы показать, что синтаксис этих языков можно выразить в виде функций. Поэтому я начал с JSX, но дальше речь и примеры уже на JavaScript.
  2. Вы правы. Я предлагал заменить некоторые императивные конструкции на функции. Функция "add" есть в коллекции библиотеки Ramda не потому, что в языке отсутствует этот оператор, а потому что она нужна только там, где нужно строить композиции из этих функций.
  3. Сама идея декларативности заключается в том, что вы описываете желаемый результат, а не даёте пошаговую инструкцию. Тернарный оператор является альтернативой оператору "if". В начале статьи я сделал дисклеймер о более декларативном подходе, поэтому вариант с "visible" является более декларативным по сравнению с логическим операндом.
  4. Пример с операторами "Switch" и "Case" был просто переходным, чтобы показать, то что описанное декларативно в JSX, можно представить в виде функции. Таким образом, я пытался провести параллель между функциональным программированием. Если вы можете типизировать обычный код с помощью “switch/case/break”, то вы также можете типизировать код в JSX или функциональной версии. JSX поддерживает генерики: https://mariusschulz.com/blog/passing-generics-to-jsx-elements-in-typescript.

Тут статья скорее машинный перевод, по этой ссылке статья на русском: https://tproger.ru/articles/deklarativnyj-javascript

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

Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу