Иллюстрация версии машинописного текста Functor, Applicative и Monad

JavaScript
Иллюстрация версии машинописного текста Functor, Applicative и Monad

Эта статья является классическойFunctors, Applicatives, And Monads In PicturesМашинописная переведенная версия.

Functor/Applicative/Monad — одни из самых «базовых» понятий в функциональном программировании. В любом случае, я не согласен с термином «базовый». Я также читал много подобных статей о Monad, но, в конце концов, они Эти понятия более трудны для понимания, и с этими вещами трудно соприкоснуться в обычной практике программирования.

Я прочитал это позжеFunctors, Applicatives, And Monads In Pictures, Да вроде понял. Так что я хочу через перевод, а затем глубокое переваривание и переваривание этой статьи, вот использоватьTypescriptКак язык описания, он будет лучше понят интерфейсом.

Если что-то не так с вашим пониманием, пожалуйста, поправьте меня.


Вот простое значение:

такие как эти

1        // number
'string' // string

Все знают, как применить функцию к этому значению:

// So easy
const add3 = (val: number) => val + 3
console.log(add3(2)) // 5

Это просто. Давайте расширим его так, чтобы любое значение можно было обернуть в **контекст**. Теперь вы можете представить блок, в который можно поместить значение:

теперь ваша очередьПри применении функции к обернутому значению вы получите разные результаты в зависимости от типа контекста.. ЭтоFunctor, Applicative, Monad, Arrowоснову таких понятий.

Maybeявляется типичным типом данных, который определяет два связанных 'контекст', Может быть, сам по себе является "контекстом" (что угодно, кроме значения, может быть контекстом?):

Исходный текст основан на Haskell, и его тип Maybe имеет два контекста Just (синее поле) и None (красное пустое поле). Чтобы имитировать Haskell в Typescript, мы можем использовать可选类型(Maybe)Представлять:

type Maybe<T> = Just<T> | Nothing // Just 表示值‘存在’,Nothing表示空值,类似于null、undefined的概念

Базовая структура Just and Nothing:

// 我们只用None来取代null, 这里我们将None作为一个值,而不是一个类
export class None {}
// 对应None的类型
export type Nothing = typeof None

// 判断是否是Nothing,这里使用Typescript的 `Type Guards`
export const isNothing = (val: any): val is Nothing => {
  return val === None
}

// Just类型
export class Just<T> {
  static of<T>(val: T) {
    return new Just(val)
  }
  value: T
  constructor(value: T) {
    this.value = value
  }
}

Пример использования:

let a: Maybe<number>;
a = None;
a = Just.of(3);

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

  type Optional<T> = NonNullable<T> | nul
  let a: Optional<number> = 1;
  a = null;

Скоро мы увидим паруJust<a>Чем приложение-функция отличается от Nothing Сначала давайте посмотрим на Functor!



Functors

Когда значение заключено в контекст, вы не можете использовать обычные функции:

declare let a: Just<number>;

const add3 = (v: number) => v + 3
add3(a) // ❌ 类型“Just<number>”的参数不能赋给类型“number”的参

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

Например, представьте, что вы хотите применить add3 к Just 2. Используйте fmap:

Just.of(2).fmap(add3) // Just 5

💥 Бум!fmap показывает нам свои результаты. Но откуда fmap знает, как применить функцию?


Что такое функтор?

в ХаскелеFunctorЯвляетсятипкласс. Он определяется следующим образом:

В Typescript функтор думает какЛюбой тип, определяющий fmap. посмотриfmapКак это работает:

  1. Фа типа Functor, например Just 2
  2. fa определяет fmap, а fmap принимает функцию fn, такую ​​как add3
  3. fmap возвращает функтор типа fb до тех пор, пока не будет применено fa к fn.fa и fb имеют одинаковый тип контекста упаковки, например, fa — Справедливо, тогда fb — тоже Справедливо, иначе fa — Ничто, и fb — тоже Ничто;

Опишите его сигнатурой функции Typescript:

<Functor T>.fmap<U>(fn: (val: T) => U): <Functor U>

Итак, мы можем сделать это:

Just.of(2).fmap(add3) // Just 5

И fmap волшебным образом применяет эту функцию, потому что Maybe — это Functor, который указывает, как fmap применяется к Just and Nothing:

class Just<T> {
  // ...
  // 实现fmap
  fmap<U>(fn: (val: T) => U) {
    return Just.of(fn(this.value))
  }
}

class None {
  // None 接受任何函数都返回None
  static fmap(fn: any) {
    return None
  }
}

когда мы пишемJust.of(2).fmap(add3), вот что происходит за кулисами:

Итак, вот так, fmap, как насчет того, чтобы применить add3 к Nothing?

None.fmap(add3) // Nothing

Подобно Морфеусу в «Матрице», fmap знает, что делать: если вы начнете с Ничего, то и закончите Ни с чем! фмап это дзен.

Теперь он говорит нам, почему существует тип данных Maybe. Например, вот как обработать запись базы данных на языке без Maybe, таком как Javascript:

let post = Post.findByID(1)
if (post != null) {
  return post.title
} else {
  return null
}

С фмап:

// 假设findPost返回Maybe<Article>
findPost(1).fmap(getPostTitle)

Если findPost возвращает пост, мы получаем его заголовок через getPostTitle. Если он ничего не возвращает, мы ничего не возвращаем! Гораздо проще, чем раньше, верно?

После того, как в Typescript появилась опциональная цепочка, обработка null также может быть очень лаконичной:

findPost(1)?.title // 异曲同工

Исходный текст также определяет версию fmap с перегруженным оператором, поскольку JavaScript не поддерживает перегрузку операторов, поэтому он просто взят здесь.

getPostTitle <$> findPost(1) // 使用操作符重载<$> 来简化fmap. 等价于上面的代码

Другой пример: что произойдет, если вы примените функцию к массиву (списку в Haksell)?

Массивы тоже функторы!

[1, 2, 3].map(add3) // [4, 5, 6]. fa是Array,输出fb也是Array,符合Functor的定义吧,所以Javascript的map就是fmap,Array就是Functor

Хорошо, хорошо, один последний пример: что произойдет, если вы применяете одну функцию в другую?

const multiply3 = (v: number) => v * 3
const add3 = (v: number) => v + 3

add3.fmap(multiply3) // ❓

Вот функция:

Вот функция, примененная к другой функции:

В результате получается еще одна функция!

// 仅作示例,不要模仿
interface Function {
  fmap<V, T, U>(this: (val: V) => T, fn: (val: T) => U): (val: V) => U
}
Function.prototype.fmap = function(fn) {
  return v => fn(this(v))
}

Так что функции тоже функторы! Использование fmap для функции на самом деле является композицией функции (compose)! То есть:f.fmap(g)Эквивалентноcompose(f, g)


FunctorСводка

Из приведенного выше примера вы можете понять, что Functor не так уж сложен для понимания.

<Functor T>.fmap(fn: (v: T) => U): <Functor U>

Functor определит операцию «fmap», этот fmap принимает функцию fn, fn получает определенное значение и возвращает другое определенное значение, такое как add3 выше.fmap решает, как применить fn к исходному Functor(a), который возвращает новый Functor(b). То есть исходное и выходное значения fmap имеют один и тот же 'контекстный' тип. Например

  • Just -> fmap -> Just
  • Nothing -> fmap -> Nothing
  • Maybe -> fmap -> Maybe
  • Array -> fmap -> Array


Applicative

Теперь практика ко второму дню. Applicative выводит его на новый уровень.

Для Applicative наше значение по-прежнему заключено в контекст, такой как Functor.

Разница в том, что мыОберните функции в Functor (например, add3) в контекст, а также!

Эм. Давайте углубимся. Applicative не шутит. В отличие от Haskell, Typescript не имеет встроенного способа обработки аппликативов. Мы можем определить функцию применения для типов, которые должны поддерживать Applicative.Функция применения знает, как применять包装在上下文的函数применить к包装在上下文的值:

class None {
  static apply(fn: any) {
    return None;
  }
}

class Just<T> {
  // 使用方法重载,让Typescript更好推断
  // 如果值和函数都是Just类型,结果也是Just类型
  apply<U>(fn: Just<(a: T) => U>): Just<U>;
  // 如果函数是Nothing类型,结果是Nothing.
  // 严格上apply只应该接收同一个上下文类型的函数,即Just,
  // 因为Maybe是Typescript的Union类型,没办法给它扩展方法,这里将Maybe和Just混在一起了
  apply<U>(fn: Nothing): Nothing;
  // 如果值和函数都是Maybe类型, 返回一个Maybe类型
  apply<U>(fn: Maybe<(a: T) => U>): Maybe<U> {
    if (!isNothing(fn)) {
      return Just.of(fn.value(this.value));
    }
    return None.apply(fn);
  }
}

Давайте снова посмотрим на массив:

// 仅作示例
interface Array<T> {
  apply<U>(fns: Array<(e: T) => U>): U[]
}

// 接收一个函数‘数组(上下文)’,返回一个应用了‘函数’的新的数组
Array.prototype.apply = function<T, U>(fns: Array<(e: T) => U>) {
  const res: U[] = []
  for (const fn of fns) {
    this.forEach(el => res.push(fn(el)))
  }
  return res
}

В Haskell используйте<*>для представления операции применения:Just (+3) <*> Just 2 == Just 5, Typescript не поддерживает перегрузку операторов, поэтому игнорируйте .

Просто тип прикладной схемы приложения:

Прикладная схема применения массивного типа:

const num: number[] = [1, 2, 3]
console.log(num.apply([multiply2, add3]))
// [2, 4, 6, 4, 5, 6]

Вот что Applicative может сделать, чего не может Functor. Как применить функцию, которая принимает два аргумента, к двум обернутым значениям?

// 一个支持两个参数的Curry型加法函数
const curriedAddition = (a: number) => (b: number) => a + b

Just.of(5).fmap(curriedAddition) // 返回 `Just.of((b: number) => 5 + b)`
// Ok 继续
Just.of(4).fmap(Just.of((b: number) => 5 + b))  // ❌不行了,报错了,Functor没办法处理包装在上下文的fn

Но Applicative может:

Just.of(5).fmap(curriedAddition) // 返回 `Just.of((b: number) => 5 + b)`
// ✅当当当
Just.of(3).apply(Just.of((b: number) => 5 + b)) // Just.of(8)

В этот момент Applicative отталкивает Functor в сторону. «Большие шишки могут использовать функции с любым количеством аргументов», — говорится в сообщении. «С помощью (fmap) и (apply) я могу принять любую функцию с любым количеством аргументов развернутого значения. Затем я передаю ей все обернутые значения и получаю обернутое значение Значение упаковки выходит!

Just.of(3).apply(Just.of(5).fmap(curriedAddition)) // 返回 `Just.of(8)`

ApplicativeСводка

Мы повторяем аппликативное определение,Если Functor требует реализации fmap, Applicative требует реализации apply, apply соответствует следующим определениям:

// 这是Functor的fmap定义
<Functor T>.fmap(fn: (v: T) => U): <Functor U>

// 这是Applicative的apply定义,和上面对比,fn变成了一个包装在上下文的函数
<Applicative T>.apply(fn: <Applicative (v: T) => U>): <Applicative U>


Monad

Наконец добрался до тройного неба! Продолжить ⛽ Давай ️

Как изучать монады:

  1. Вы собираетесь получить докторскую степень в области компьютерных наук.
  2. Тогда выбросьте его, потому что он вам не нужен для этой статьи!

Монада добавляет новое преобразование.

Functorположить函数применить к已包装的值начальство:

Applicativeположить已包装的函数применить к已包装的值начальство:

Монада ставит返回已包装值的函数применить к已包装的值начальство. Монада определяет функциюflatMap(В Haskell можно использовать операторы>>=Чтобы применить монад, произносится «связывание»), чтобы сделать это.

Давайте посмотрим на пример. Старый партнер Maybe — это монада:

Предположениеhalfэто функция, которая работает только с четными числами:

// 这就是一个典型的: "返回已包装值"的函数
function half(value: number): Maybe<number> {
  if (value % 2 === 0) {
    return Just.of(value / 2)
  }
  return None
}

если мы будем кормить его已包装的值Что случится?

Нам нужно использовать flatMap (>>= в Haskell), чтобы втиснуть наше обернутое значение в функцию. Вот фото >>= :

Вот как это работает:

Just.of(3).flatMap(half) // => Nothing, Haskell中使用操作符这样操作: Just 3 >>= half
Just.of(4).flatMap(half) // => Just 2
None.flatMap(half)       // => Nothing

Что происходит внутри? Давайте еще раз посмотрим на сигнатуру метода flatMap:

// Maybe
Maybe<T>.flatMap<U>(fn: (val: T) => Maybe<U>): Maybe<U>

// Array
Array<T>.flatMap<U>(fn: (val: T) => U[]): U[]

Массив — это монада, плоская карта Javascript Array официально стала стандартом, см. пример ее использования:

const arr1 = [1, 2, 3, 4];
arr1.map(x => [x * 2]); 
// [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]

// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]

Maybe также является Монадой:

class None {
  static flatMap(fn: any): Nothing {
    return None;
  }
}

class Just<T> {
  // 和上面的apply差不多
  // 使用方法重载,让Typescript更好推断
  // 如果函数返回Just类型,结果也是Just类型
  flatMap<U>(fn: (a: T) => Just<U>): Just<U>;
  // 如果函数返回值是Nothing类型,结果是Nothing.
  flatMap<U>(fn: (a: T) => Nothing): Nothing;
  // 如果函数返回值是Maybe类型, 返回一个Maybe类型
  flatMap<U>(fn: (a: T) => Maybe<U>): Maybe<U> {
    return fn(this.value)
  }
}

// 示例
Just.of(3).flatMap(half) // Nothing
Just.of(4).flatMap(half) // Just.of(4)

Вот как это работает с Just 3!

Еще проще, если вы передаете Nothing :

Вы также можете связать эти вызовы вместе:

Just.of(20).flatMap(half).flatMap(half).flatMap(falf) // => Nothing


Очень круто ха! Итак, теперь мы знаем, что Maybe является и функтором, и аппликативом, и монадой.

Исходный текст также демонстрирует другой пример:IOМонада, давайте кратко рассмотрим это здесь

Подпись IO примерно такая:

class IO<T> {
  val: T
  // 具体实现我们暂不关心
  flatMap(fn: (val: T) => IO<U>): IO<U>
}

Рассмотрим подробнее три функции. getLine не имеет параметров и используется для получения пользовательского ввода:

function getLine(): IO<string>

readFile принимает строку (имя файла) и возвращает содержимое этого файла:

function readFile(filename: string): IO<string>

putStrLn выводит строку в консоль:

function putStrLn(str: string): IO<void>

Все три функции принимают простое значение (или не принимают никакого значения) и возвращают обернутое значение, то есть ввод-вывод. Мы можем использовать flatMap, чтобы соединить их!

getLine().flatMap(readFile).flatMap(putStrLn)

чудесный! Займите место в первом ряду, чтобы посмотреть шоу монад! Нам не нужно тратить время на распаковку и переупаковку значений монады IO — за нас это делает flatMap!

Haskell также предоставляет синтаксический сахар для монад, называемый выражениями do:

foo = do
    filename <- getLine
    contents <- readFile filename
    putStrLn contents

Суммировать

  1. функтор реализованfmapтип данных.
  2. аппликативный реализованapplyтип данных.
  3. монада реализованаflatMapтип данных.
  4. Может быть, реализует все три, так что это функтор, аппликативная и монада.

В чем разница между этими тремя?

  1. functor: может быть преобразован в函数применить к已包装的值начальство.
  2. applicative: Вы можете применить已包装的函数применимый к已包装的值начальство.
  3. monad: flatMap можно использовать для преобразования返回已包装值的函数применимый к已包装的值начальство.

Взгляните на их подписи вместе:

// 这是Functor的fmap定义
<Functor T>.fmap(fn: (v: T) => U): <Functor U>

// 这是Applicative的apply定义,和上面对比,fn变成了一个包装在上下文的函数
<Applicative T>.apply(fn: <Applicative (v: T) => U>): <Applicative U>

// Monad的定义, 而接受一个函数, 这个函数返回一个包装在上下文的值
<Monad T>.flatmap(fn: (v: T) => <Monad U>): <Monad U>

Итак, дорогие друзья (я думаю, что теперь мы друзья), я думаю, мы все можем согласиться с тем, что монады — это простая и блестящая идея (УМНАЯ ИДЕЯ (tm)). Теперь, когда вы намочили свой свисток этим руководством, почему бы не надеть Mel Gibson и не взять всю бутылку. См. Давайте посмотрим на некоторые монады в Haskell Fun Guide. Многие вещи я на самом деле замалчивал, потому что Миран проделал большую работу, углубившись в это.


расширять

Эта статья находится воригинальныйНа основании следующих переводов, еще раз спасибо этим авторам: