Автор оригинала: Agustin Chiappe Berrini.
Не так давно мы говорили о ленивой оценке. Я упомянул там, что структура данных, которую мы создавали, была фактически монадой. Я не упомянул об этом, но это также функтор. Я также сказал, что в том, что такое монад, почему они интересны и как их использовать, было предметом для другой статьи. И это сказано статья! Я постараюсь дать краткое объяснение монадам и функторам практичным способом решения очень типичной проблемы: ненужные значения.
Проблема
Значения, которые нуляют, могут быть головной болью на каждом языке. Если вы работали с JVM, вы уверены, что увидели морюки Nullpointerexception
Отказ Если вы работали с JavaScript, вы уверены, что увидели ошибки, как undefined не функция
Отказ
Эта добрая ошибка появляется, когда у вас есть значение, которое может не установить и использовать его без проверки, если оно или нет. Это очень раздражающая проблема и может легко привести к тонны Если (значение)
для того, чтобы предотвратить такое поведение.
В функциональном программировании (специально, если у нас есть статические типы), мы используем что-то под названием Monads и функторов для создания структуры, которая обеспечивает простой способ обрабатывать эти значения. В Haskell указанная структура называется Может быть
Отказ В Scala, Опция
Отказ
Какие?
Сначала я хотел бы представить какой-нибудь словарный запас: концепция контейнера.
Контейнер (или обертка) – это структура данных, которая хранит значение (или группу значений) и содержит интерфейс для доступа к нему. Например, Массив
, а Карта
, Объект
… вы получаете идею.
Функторы
Функтор представляет собой контейнер с одной конкретной функциональностью: учитывая пользовательскую функцию, которая принимает тип содержимого контейнера и возвращает что-то еще, он применит его ко всем элементам в контейнере и завернут результат в (новый) функтор и вернуть его.
Обычно на практике мы можем подумать о функторе в качестве интерфейса, который реализует карта
метод. Давайте посмотрим, как выглядит этот интерфейс:
interface Functor{ content: T map > (modifier: (a: T) => T1): F }
Массивы – хороший пример функторов.
Монад
Монад также является контейнером, который применяет функцию к его содержанию. Тем не менее, эта функция немного отличается: вместо того, чтобы принимать развернутый элемент, возвращая модифицированное значение и оставьте упаковочные работы в структуру данных, она примет развернутое значение, обернуть его в (новый) монаде и вернуть это обратно Отказ
Вы когда-либо использовали Обещание
? Вы когда-нибудь были привязаны Обещание
? Тогда вы использовали монады.
return Promise.resolve(1) .then(v => Promise.resolve(2)) .then(v => Promise.resolve(3)) .catch(e => console.log(e));
Это монад в действии! В этом случае мы передаем функцию, используя метод Тогда
Отказ Другими языками называют этот метод FlatMap
или привязывать
Отказ Для согласованности мы будем использовать первую альтернативу и сказать, что монад – это любой объект, который реализует этот интерфейс:
interface Monad{ content: T then > (modifier: (a: T) => M): M }
Как?
Давайте начнем кодировать!
interface Functor{ content: T map > (modifier: (a: T) => T1): F } interface Monad { content: T then > (modifier: (a: T) => M): M } class Option extends Monad , Functor { content: T constructor (content?: V) { this.content = content } isDefined(): boolean { return this.content !== null && this.content !== undefined } nonDefined(): boolean { return !this.isDefined() } then (modifier: (a: V) => Option ): Option { if (this.nonDefined()) { return Option.none() } return modifier(this.content) } map (modifier: (a: V) => V1): Option { return this.then(a => Option.unit(modifier(a))) } public static unit (value?: V): Option { return new Option(value) } public static none(): Option { return new Option(null) } }
Как видите, я представлял определенное значение как то, что отличается от null
и undefined
Отказ В то же время, если монад не определен, оба Тогда
и карта
всегда будет возвращать не определенные Опция
Отказ
Реализация, как вы видите, очень проста и легко понять. Однако … нам может понадобиться немного больше, чтобы увидеть, как это полезно.
Почему?
Я не буду пытаться спорить, зачем его использовать, потому что я потерю неудачу. Я позволю вам разговору кода.
Во-первых, позвольте мне добавить немного нашей структуре, чтобы сделать его более практичным менее теоретичным.
class Optionextends Monad , Functor { // ... get(): V { return this.content } getOrElse(defaultValue: V): V { return this.isDefined() ? this.content : defaultValue } filter (check: (a: V) => boolean): Option { if (this.nonDefined() || !check(this.content)) { return Option.none() } return new Option(this.content) } foreach (action: (a: V) => void): Option { if (this.isDefined()) { action(this.content) } return this } orElseDo (action: () => void): Option { if (this.nonDefined()) { action() } return this } // ... }
И теперь, предположим, что следующий случай:
Мы строим веб-сервер. У нас есть функция, которая получит объект с заголовками запроса, ключом ожидаемого заголовка и возврат Опция
со значением заголовка или ничего, если он не присутствовал.
interface Headers { [key: string]: string } const getHeader = (h: Headers, key: string): Option=> { if (h[key]) return Option.some(h[key]) return Option.none() }
Теперь, давайте предположим, что мы создаем промежуточное программное обеспечение, которое будет обрабатывать аутентификацию. Мы ожидаем, что каждый запрос, который проходит через это промежуточное программное обеспечение, будет Авторизация
Заголовок со значением TOKEN $ {СЛЕДУЮЩИЙ}
где Пояснулся
подписан это токен JWT.
Как это сделать с вариантами?
interface JWTHelper { validate: (t: string): boolean extract: (t: string): Option<{user_id: number, username: string}> } const JWT =require('some-jwt-library') interface Request { headers: Headers } interface Response { setStatus: (s: number) => void send: (t: string) => void } interface Handler { (req: Request, res: Response, next: Handler) => void } const TokenHeaderRegex = /^Token (.+)/g const auth = (req: Request, res: Response, next: Handler): void => { getHeader(req.headers) // Let's get the header! .filter(h => h.match(TokenHeaderRegex)) // Let's accept only headers in the expected format .map(h => h.replace(TokenHeaderRegex, '$1')) // And now we extract the token .filter(t => JWT.validate(t)) // Is it a valid token? .then(t => JWT.extract(t)) // And now extract the information in the token .foreach(u => { // If all went right, authenticate and go to the next step req.user = u next(req, res, next) }) .orElseDo(() => { // If something went wrong, send 403 res.sendStatus(403) res.send('you shall not pass') }) }
(Обратите внимание, что jwt.extract
Возвращение на Опция
С помощью объекта пользователя ожидается или ничего, если токен не имел указанных пользователей значений).
Разве это не просто? Примерно в десяти строках кода мы обрабатывали все случаи, которые нам нужны без необходимости беспокоиться о нулях или неопределенных. Хотя логика в этой функции очень условие («является пустым заголовком?», «Направлен в правильном формате?», «Является ли токен?»), Нет Если
И в то же время мы можем быть уверены, что мы получаем доступ только к значению только тогда, когда они действительны. Поскольку мы используем небольшие функции для фильтров и карт, их легко проверить, и они корректности.
Как мы можем попросить больше?
Вы экспериментируете с этим в этом Github Repository Или через это пакет NPM