Рубрики
Без рубрики

Обращение с нулевыми значениями с монадом и функторами в JS

Краткое введение в монадные функторы с реальным использованием.

Автор оригинала: 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 Option extends 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