Что такое функтор? Функциональное программирование
2 года назад·5 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.
Это серия статей о функциональном программировании:
Функтор
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Имплементация функтора
Как использовать функтор
С валидными данными:
Функтор Решение задачи с функтором
Комментарии к решению с функтором
Мы смогли улучшить традиционное решение при помощи функтора
- Парадигмы программирования
- Композиция
- Функторы (рассматривается в этой статье)
- Каррирование
- Чистые функции
- Функции первого класса
Что такое функтор?
Функтор (functor) это:- обертка над значением,
- предоставляет интерфейс для преобразование (map),
- подчиняется законам функтора (поговорим о них позже).
Примеры функторов
- Массив (
Array
), - Промис (
Promise
).
Почему массив - функтор?
Вспомним определение функтора:- обертка над списком значений,
- предоставляет интерфейс для преобразования - метод
map
, - подчиняется законам функтора.
[1, 2, 3] // обернутое значение .map( // интерфейс для преобразования значения x => x * 2 )
Почему промис - функтор?
Промис это:- обертка над любым значением из JavaSctipt типов,
- предоставляет интерфейс для преобразования - метод
then
, - подчиняется законам функтора.
const promise = new Promise((resolve, reject) => { resolve( { data: "value" } // обернутое значение, в данном случае объект ) }); promise .then( // интерфейс для преобразования значения response => console.log(response) );
Что объединяет массив (или промис) и функтор?
Функтор - это паттерн проектирования, аArray
и Promise
- типы данных, которые основаны на этом паттерне.
Почему мы говорим, что массив и промис - функторы?
Чтобы понять, что функторы ближе чем кажутся. Массив и промис легко понять, при это они являются мощной концепцией. Мы используем их ежедневно, даже не подозревая об их сущности.Где использовать функторы?
Немного поговорив о функторах и связав их с нашим повседневным использованием, было бы разумно рассмотреть их подробнее. Чтобы лучше понять идею функтора, создадим свои собственные функторы. Для начала рассмотрим такую задачу. Предположим, есть следующий кусочек данных.{ products: [ { name: "All about JavaScript", type: "book", price: 22, discount: 20 } ] }
Постановка задачи
Найти финальную цену первого товара с учетом скидки. Если по какой-либо причине будут переданы неправильные данные, вывести строку "No data".Шаги алгоритма
- Найти первый продукт со скидкой,
- Применить скидку,
- Продолжать проверку данных на валидность. Если данные не валидны - вернуть "No data".
Традиционное решение
const isProductWithDiscount = product => { return !isNaN(product.discount) } const findFirstDiscounted = products => { products.find(isProductWithDiscount) } const calcPriceAfterDiscount = product => { return product.price - product.discount } const findFinalPrice = (data, fallbackValue) => { if(!data || !data.products) return fallbackValue const discountedProduct = findFirstDiscounted(data.products) if(!discountedProduct) return fallbackValue return calcPriceAfterDiscount(discountedProduct) } findFinalPrice(data, "No data")
Комментарии к традиционному решению
Достоинства:- Атомарные логические единицы (
isProductWithDiscount
,findFirstDiscounted
иcalcPriceAfterDiscount
), - Логику защищена от невалидных данных.
- Cлишком много защитных проверок. (Защитное программирование (Defensive programming) является обязательным в любом отказоустойчивом программном обеспечении. Однако, в нашем коде 50% тела функции
findFinalPrice
— проверка на валидность данных. Это слишком много). fallbackValue
почти везде.
Почему нас волнуют эти улучшения?
Потому что данный код заставляет слишком в него вникать. Это негативно влияет на DX (Developer Experience) - уровень удовлетворенности разработчика от работы с кодом. Проанализируем код, чтобы прийти к лучшему решению. Части, которые мы стремимся улучшить, формируют паттерны (защита (defence) и откат (fallback)). Хорошо то, что эти части на самом деле цельные и атомарные. Мы должны иметь возможность абстрагировать этот паттерн в оболочку, которая могла бы обрабатывать эти крайние случаи вместо нас. Обертка позаботится о крайних случаях, а нам останется позаботиться только о бизнес-логике.Функтор Maybe
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Maybe
состоит в том, чтобы обернуть наши данные (потенциально невалидные данные) и обработать для нас крайние случаи.
Имплементация функтора Maybe
function Maybe(value) { const isNothing = () => { return value === null || value === undefined } const map = (fn) => { return isNothing() ? Maybe() : Maybe(fn(value)) } const getValueOrFallback = { return (fallbackValue) => isNothing() ? fallbackValue : value; } return { map, getValueOrFallback, }; }
Пояснения к имплементации
isNothing
проверяет валидно ли обернутое в функторMaybe
значениеmap
- интерфейс для преобразования обернутого значения, с помощью которого мы применяем функции с бизнес логикой к обернутому значению.map
возвращает новое значение в другом экземпляреMaybe
. Таким образом, мы можем сделать цепочку вызововmap
-.map().map().map...
.getValueOrFallback
возвращает обернутое значение или запасное значениеfallbackValue
.
Как использовать функтор Maybe
?
С валидными данными:
С невалидными данными:Maybe('Hello') .map(x => x.substring(1)) .getValueOrFallback('fallback') // 'ello'
Maybe(null) .map(x => x.substring(1)) // функция не будет запущена .getValueOrFallback('fallback') // 'fallback'
Maybe
обработал крайние случаи вместо нас и не запустил функцию с невалидными данными. Нам нужно лишь позаботиться о бизнес логике. Таким образом, мы внедрили улучшение, о котором говорили в традиционном решении. Внедрим это решение в задачу.
Решение задачи с функтором Maybe
const isProductWithDiscount = product => { return !isNaN(product.discount) } const findFirstDiscounted = products => { return products.find(isProductWithDiscount) } const calcPriceAfterDiscount = product => { return product.price - product.discount } Maybe(data) .map((x) => x.products) .map(findFirstDiscounted) .map(calcPriceAfterDiscount) .getValueOrFallback("No data")
Комментарии к решению с функтором Maybe
Мы смогли улучшить традиционное решение при помощи функтора Maybe
:
- мы не защищаем код сами, вместо нас это делает функтор
Maybe
, - мы указали
fallbackValue
только один раз.
Maybe
соответствует определению функтора? Функтор Maybe
это:
- обертка над любым значением из JavaScript типов,
- предоставляет интерфейс для преобразования - метод
map
, - подчиняется законам функтора.
Законы функторов
Закон идентичности (Identity law)
Если при выполнении операции преобразования, значения в функторе преобразовываются сами на себя, результатом будет немодифицированный функтор.const m1 = Maybe(value) const m2 = Maybe(value).map(v => v) // m1 и m2 эквивалентны
Закон композиции (Composition law)
Если две последовательные операции преобразования выполняются одна за другой с использованием двух функций, результат должен быть таким же, как и при одной операции отображения с одной функцией, что эквивалентно применению первой функции к результату второй.const m1 = Maybe(value).map(v => f(g(v))) const m2 = Maybe(value).map(v => g(v)).map(v => f(v)) // m1 и m2 эквивалентны
Зачем использовать функторы?
- Абстракция над применением функции,
- Усиление композиции функций,
- Уменьшение количества защитного кода (как в функторе
Maybe
), - Более чистая структура кода,
- Переменные более явно указывают на то, что мы ожидаем (что
Maybe
моделирует значение, которое может присутствовать, а может и не присутствовать).
Что означает Абстракция над применением функции?
То, что мы передаем функцию (т.е.x => x.products
) в интерфейс преобразования (т.е. map
) обертки (т.е. Maybe
), и она знает, как позаботиться о себе (посредством своей внутренней реализации).
Нас не интересуют детали реализации оболочки, которые она содержит (детали реализации скрыты), и тем не менее мы знаем, как использовать обертку (Array
или Promise
), используя их интерфейсы преобразования (map
).
И это на самом деле крайне важно в мире программирования. Это снижает планку того, как много мы, как программисты, должны понимать, чтобы иметь возможность что-то сделать. Функторы могут быть реализованы на любом языке, поддерживающем функции высшего порядка (а таких в наши дни большинство).
Почему функторы не используются повсеместно?
Просто потому, что мы к ним не привыкли. До.map
(и .then
) мы мутировали массивы или перебирали их значения вручную. Но как только мы обнаружили .map
, мы начали адаптировать его в качестве нового инструмента преобразования. Я надеюсь, что, поняв ценность функторов, мы начнем чаще внедрять их в наши ежедневные задачи как привычный инструмент.
Функтор Maybe
- лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.
Итоги
Функтор как паттерн проектирования - это простой, но очень мощный паттерн. Мы используем его ежедневно в различных типах данных, не догадываясь об этом. Было бы здорово, если мы сможем распознавать и ценить функторы немного больше и выделять им больше места в кодовой базе, потому что они делают код чище и дают нам больше возможностей.Хуки useTransition и useDeferredValue в ReactJS 18
2 года назад·3 мин. на чтение
В React 18, релиз которого произошел в марте 2022, появилось много новых инструментов для написания производительных и отзывчивых приложений. Одним из заметных изменений является механизм рендеринга с новой ключевой концепцией: конкурентный рендеринг (concurrent rendering).
В этой статье повнимательнее рассмотрим два новых хука:
Какое обновление можно считать срочным, а какое обычным?
Хук
До React 18 все обновления состояния помечались как "срочные". Это означает, что все обновления состояния обрабатывались одинаково с одинаковым приоритетом.
С помощью
Когда использовать
Одним из примеров может быть список товаров с параметрами фильтрации.
Когда вы переключаете чекбоксы, чтобы выбрать размер или цвет одежды, вы ожидаете, что чекбоксы сразу же отобразят отмеченное или снятое состояние.
А сам список товаров, которые необходимо обновить согласно фильтрам, может быть отдельным и менее срочным обновлением.
Как использовать
Хук
Когда использовать
С помощью
Как использовать
useTransition()
и useDeferredValue()
.
Эти два хука дают возможность определять приоритет обновления состояния, или, скорее, указывать, является ли обновление менее важным, чем другие, и откладывать его в пользу более срочных.
- Срочные обновления: отражают прямое взаимодействие, такое как набор текста, клики, нажатия и т. д., т.е. то с чем взаимодействует пользователь. Когда вы вводите текст в поле ввода, вы хотите сразу увидеть введенный вами текст. В противном случае UI будет казаться медленным и подлагивать. Поэтому мы хотим сделать такие обновления приоритетным.
- Обычные обновления: переход пользовательского интерфейса из одного вида в другой. Пользователи знают, что представление должно измениться или обновиться (например, когда ожидается ответ на запрос данных). Даже если есть небольшая задержка, это можно рассматривать как ожидаемое поведение, и это не будет восприниматься как медлительность приложения.
Хук useTransition()
и функция startTransition()
До React 18 все обновления состояния помечались как "срочные". Это означает, что все обновления состояния обрабатывались одинаково с одинаковым приоритетом.
С помощью useTransition()
теперь можно пометить некоторые обновления состояния как несрочные.
Когда использовать useTransition()
?
Одним из примеров может быть список товаров с параметрами фильтрации.
Когда вы переключаете чекбоксы, чтобы выбрать размер или цвет одежды, вы ожидаете, что чекбоксы сразу же отобразят отмеченное или снятое состояние.
А сам список товаров, которые необходимо обновить согласно фильтрам, может быть отдельным и менее срочным обновлением.
Как использовать useTransition()
?
function App() { const [isPending, startTransition] = useTransition(); const [searchQuery, setSearchQuery] = useState(''); // запрос данных, который занимает некоторое время const filteredResults = getProducts(searchQuery); function handleQueryChange(event) { startTransition(() => { // оборачивая setSearchQuery() в startTransition(), // мы помечаем эти обновления как менее важные setSearchQuery(event.target.value); }); } return ( <div> <input type="text" onChange={handleQueryChange} /> {isPending && <span>Loading...</span>} <ProductsList results={filteredResults} /> </div> ); }
Хук useDeferredValue()
useDeferredValue()
очень похож на useTransition()
в том, что он позволяет отложить несрочное обновление состояния, но применяется его к части дерева.
Это похоже методы debounce и throttle, которые мы часто используем для отложенных обновлений. React будет работать с такими обновлениями, как только завершатся срочные обновления.
Когда использовать useDeferredValue()
?
С помощью useTransition()
вы сами решаете, когда конкретное обновление состояния может быть помечено как менее срочное. Но иногда такой возможности может и не быть, например, если фрагмент кода находится в сторонней библиотеке.
В таких случаях можно воспользоваться хуком useDeferredValue()
. С помощью useDeferredValue()
вы можете обернуть значение и пометить его изменения как менее важные и, следовательно, отложить повторный рендеринг.
useDeferredValue()
будет возвращать предыдущее значение до тех пор, пока есть более срочные обновления для завершения и отображения дерева с обновленным значением.
Как использовать useDeferredValue()
?
function ProductsList({ results }) { // deferredResults получат обновленные данные // когда завершатся срочные обновления const deferredResults = useDeferredValue(results); return ( <ul> {deferredResults.map((product) => ( <li key={product.id}>{product.title}</li> ))} </ul> ); }