Функціональний каррінг: реальний приклад використання

Коли ви починаєте писати компонент, зазвичай ви визначаєте всі функції та інші утиліти в одному файлі, але з часом, коли код зростає, ви захочете перемістити їх у окремі файли, щоб зберегти код чистим.

Але що робити, якщо одна з ваших функцій приймає параметр, який ви не зможете імпортувати? Це може бути стан, який ви визначили, або дані, які ви отримали з API.

Це можна вирішити за допомогою Function Currying.

Що таке Function Currying?

У двох словах, це спосіб перетворити функцію на функції з одним аргументом, класичний приклад — це метод множення:

 // оригінальна функція  
 const multiply = (a, b) => {  
   return a * b  
 }  
multiply(2, 4) // повертає 8

// Function Currying  
const curryMultiply = (a) => {  
  const multiply = (b) => {  
    return a * b  
  }  
  return multiply  
}  
curryMultiply(2)(4) // повертає 8

Параметри діляться для кожної функції, і це буде корисно для нашого наступного прикладу.

Компонент

Кілька тижнів тому мені потрібно було реалізувати побудовувач запитів і я вирішив використати популярну бібліотеку react-query-builder, дуже настроюваний інструмент для побудови запитів з різними функціями, включаючи групування, комбінацію різних правил і перевірку полів.

Що мені потрібно було побудувати — це модуль, де користувачі з певними ролями в додатку могли б фільтрувати профілі клієнтів, використовуючи різні критерії, визначені під час виконання. Ці критерії стосувалися не тільки інформації про профіль клієнта, але й даних в інших пов’язаних таблицях.

Архітектурно додаток розділений на 2 частини: React фронтенд і Ruby on Rails бекенд (тільки API додаток на Ruby on Rails). Бекенд виконував би запит, створений на фронтенді, але також повертав би на фронтенд колекцію attributes, з яких користувач міг би вибирати з відповідними types і operators. Описуючи це словами, бекенд міг би сказати: «Ви можете запитати дату народження, яка є датою, а оператори будуть більше ніж, дорівнює, менше ніж тощо».

Метою React Query Builder є створення запиту користувача в форматі, який бекенд зможе зрозуміти. Після обробки запиту бекенд повертає відфільтровані дані.

Проблема

Незважаючи на те, що був список операторів для кожного атрибуту, я міг мати тільки один тип значення на атрибут. Наприклад, поле з назвою reservation_date завжди матиме тип значення date, і в цьому випадку я маю оператори equals, greater than, less than тощо. Але для того самого атрибуту інколи хочеться запитувати, чи є reservation_date «до x днів тому», або «дні з того часу». В такому випадку ми не хочемо вводити дату, а хочемо ввести число.

React Query Builder підготовлений для таких ситуацій, оскільки надає функцію під назвою getInputType, яка визначає, який інпут має бути відображений, коли вибрано певне поле. Однак проблема була в тому, що вона працює на основі атрибута, а не оператора, тому мені довелося внести деякі зміни в код.

export default function App() {  
  const [fields, setFields] = useState(searchFields)  
  const getInputType = (field, operator) => {  
    // Робимо щось, коли вибрано поле і оператор...  
  }
return (  

    )   } 

Ця функція також вимагає два параметри: field і operator.
Щоб зберегти компонент чистим та організованим, я захотів визначити свою кастомну функцію в окремому файлі, але було дві проблеми, які треба було вирішити:

  • Мені потрібен був доступ до списку полів всередині цієї функції, щоб визначити відповідний тип вводу.
  • Я не міг імпортувати поля з компонента в цей файл, оскільки вони зберігалися в стані.

Ось тут Function Currying став у пригоді.

Рішення

Розпочнемо з полів, формат кожного параметра виглядатиме наступним чином:

const fields = [  
  {  
    name: "reservation_date",  
    label: "Reservation",  
    operators: [  
      {  
        name: "eq",  
        label: "=",  
        valueType: "date",  
      },  
      {  
        name: "days_from_now",  
        label: "Days from now",  
        valueType: "number", // якщо дата відносна, ми хочемо, щоб це було число  
      }]  
  }  
]

Як ви бачите, поле reservation_date має масив operators, і кожен оператор має свій різний valueType. Тепер давайте подивимось на функцію getInputType, яку ми створимо для обробки цих операторів:

 // функція для встановлення типу пропса для вводу (date, text...)  
const getInputType = (field, operator) => {  
  const value = fields.find((f) => f.name === field)
if (!value) return null const valueType = value.operators.find((op) => op.name === operator)?.valueType return valueType ?? null  
}

Це виглядає чудово! Але, як я згадував раніше, є маленька проблема — я не можу отримати fields, не порушивши логіку оригінальної функції. Ми можемо вирішити цю проблему, обгорнувши її в іншу функцію, яка братиме параметр, який мені потрібен, і повертатиме саме цю функцію.

export const defineGetInputType = (fields) => {  
  // функція для встановлення типу пропса для вводу (date, text...)  
  const getInputType = (field, operator) => {  
    const value = fields.find((f) => f.name === field)
if (!value) return null const valueType = value.operators.find((op) => op.name === operator)?.valueType return valueType ?? null  
 }  
 return getInputType  
}

І ось і все, тепер я можу експортувати це і використовувати де завгодно. Ось код в дії:

Висновок

Function currying — це чудове рішення, коли потрібно обробляти параметри, які знаходяться поза лексичним контекстом. У моєму випадку, я зміг перемістити всі свої функції в файл utils.js, щоб зберегти компонент більш організованим, це не тільки покращило читабельність, але й підвищило зрозумілість коду.

Тепер ваша черга спробувати!

Перекладено з: Function Currying: A real-life use example

Leave a Reply

Your email address will not be published. Required fields are marked *