J

JavaScript 中的函子和单子 函子和单子

2025-06-04

JavaScript 中的函子和单子

函子和单子

函子和单子

本次演讲的目的是阐明我们在这里和那里看到的一些函数式编程术语,主要是FunctorMonad

这到底是什么鬼?先从我在网上看到的两个短语说起吧。

“Afunctor是你可以做到的事情map。”

“Amonadfunctor你可以 的flatMap。”

让我们深入研究一下。

函子

在数学中,特别是范畴论中,函子是范畴之间的映射。

在数学中,范畴(有时称为抽象范畴,以区别于具体范畴)是通过“箭头”链接的“对象”的集合。

感到困惑?很好。

简单地说,类别就是collection of objects我们所说的某个东西,而函子是之间的映射collections of objects

因此,这引出了我们的第一条声明:

“Afunctor是你可以做到的事情map。”

让我们看一些代码:

const collection1 = [1, 2, 3] // [1,2,3]
const collection2 = collection.map(x => x + 1) // [2,3,4]

这里我们有一个数组(一个 Int 类型的集合)。由于我们可以通过以下方式将 collection1 映射到 collection2,所以x => x + 1我们可以说 JS 中的数组是Functors

假设我们想创建自己的函子。这个函子将表示一个 Person 对象。

const p1 = {
  firstName: 'matthew',
  lastName: 'staniscia',
  hairColor: 'brown',
  age: 37,
}

const Person = value => ({
  value,
})

Person(p1)

/*
Result
{ 
  value:{ 
    firstName: 'matthew',
    lastName: 'staniscia',
    hairColor: 'brown',
    age: 37 
  }
}
*/

这还不是一个函子,因为我们还不能映射它。所以让我们给它添加一个映射函数。

const p1 = {
  firstName: 'matthew',
  lastName: 'staniscia',
  hairColor: 'brown',
  age: 37,
}

const Person = value => ({
  map: fn => Person(fn(value)),
  value,
})

Person(p1)

/*
Result
{ 
  map: [Function: map],
  value:{ 
    firstName: 'matthew',
    lastName: 'staniscia',
    hairColor: 'brown',
    age: 37 
  }
}
*/

我们现在可以将一些功能映射到它。

const objectMapper = fn => value =>
  Object.keys(value).reduce((acc, cur, idx, arr) => ({ ...acc, [cur]: fn(value[cur]) }), value)

const makeUpper = s => (typeof s === 'string' ? s.toUpperCase() : s)

Person(p1).map(x => objectMapper(y => makeUpper(y))(x))
Person(p1).map(x => objectMapper(makeUpper)(x))
Person(p1).map(objectMapper(makeUpper))

/*
Result for all 3 calls
{ 
  map: [Function: map],
  value:{ 
    firstName: 'MATTHEW',
    lastName: 'STANISCIA',
    hairColor: 'BROWN',
    age: 37 
  }
}
*/

让我们尝试将几个函数映射在一起。

const objectMapper = fn => value =>
  Object.keys(value).reduce((acc, cur, idx, arr) => ({ ...acc, [cur]: fn(value[cur]) }), value)

const makeUpper = s => (typeof s === 'string' ? s.toUpperCase() : s)

const checkAge = n => (typeof n === 'number' ? (n <= 35 ? [n, 'You is good.'] : [n, 'You is old.']) : n)

Person(p1)
  .map(objectMapper(makeUpper))
  .map(objectMapper(checkAge))

/*
Result
{ 
  map: [Function: map],
  value:{ 
    firstName: 'MATTHEW',
    lastName: 'STANISCIA',
    hairColor: 'BROWN',
    age: [ 37, 'You is old.' ] 
  }
}
*/

这个对象现在是一个函子,因为它是可以映射的对象。现在是时候把它变成 Monad 了。

单子

让我们回到之前的 Monad 的定义。

“Amonadfunctor你可以 的flatMap。”

什么是flatMap?

简而言之,当您将某个东西平铺到平面图时,您将运行一个映射函数,然后将其展平。

就我们的 Person 对象而言,我们的输出看起来不会像,Person({...stuff...})而是{...stuff...}

我们使用 flatMap 从其上下文中取出 map 的结果。flatMap 的其他名称包括chainbind

回到代码。

const Person = value => ({
  map: fn => Person(fn(value)),
  chain: fn => fn(value),
  value,
})

嗯,看起来很简单。由于我们只是映射并从上下文中取出值,所以我们只需要返回解包后的值。让我们看看实际效果。

Person(p1).chain(objectMapper(makeUpper))

/*
Result
{ 
  firstName: 'MATTHEW',
  lastName: 'STANISCIA',
  hairColor: 'BROWN',
  age: 37 
}
*/

Person(p1)
  .chain(objectMapper(makeUpper))
  .chain(objectMapper(checkAge))

/* 
Result

TypeError: Person(...).chain(...).chain is not a function
*/

Huston,我们遇到问题了。这是怎么回事?为什么会出错?
很简单。第一个链的返回值不再是一个 Person Monad,而是一个 JSON 字符串,所以尝试再次链接它是行不通的,如果我们想在链上链接,我们需要维护上下文。

Person(p1)
  .chain(x => Person(objectMapper(makeUpper)(x)))
  .chain(objectMapper(checkAge))

/*
Result
{
  firstName: 'MATTHEW',
  lastName: 'STANISCIA',
  hairColor: 'BROWN',
  age: [ 37, 'You is old.' ]
}
*/

但这不就和这个一样吗?

Person(p1)
  .map(objectMapper(makeUpper))
  .chain(objectMapper(checkAge))

是的。由于 map 保存了上下文,我们可以在该上下文上进行映射或链接。

我想我们以前见过类似的事情……

单子定律

一个对象要成为单子,必须满足 3 条单子定律。

  • 左翼身份
  • 正确的身份
  • 结合性
// testing monad rules
const x = 'Matt'
const f = x => Person(x)
const g = x => Person(x + ' is kool')

const LI1 = Person(x).chain(f)
const LI2 = f(x)

const RI1 = Person(x).chain(Person)
const RI2 = Person(x)

const AC1 = Person(x)
  .chain(f)
  .chain(g)
const AC2 = Person(x).chain(x => f(x).chain(g))

// Left Identity
// Monad(x).chain(f) === f(x)
// f being a function returning a monad
Object.entries(LI1).toString() === Object.entries(LI2).toString()

// Right Identity
// Monad(x).chain(Monad) === Monad(x)
Object.entries(RI1).toString() === Object.entries(RI2).toString()

// Associativity
// Monad(x).chain(f).chain(g) == Monad(x).chain(x => f(x).chain(g));
// f and g being functions returning a monad
Object.entries(AC1).toString() === Object.entries(AC2).toString()

/*
Result
true
true
true
*/

就我们的 Person monad 而言,它满足这些规则。

为什么要使用 Monad?

你不必使用 Monad。如果你使用 Monad,并且所有 Monad 都以相同的方式编写,那么你将拥有一个可以随意链接和混合的结构。Monad 本质上是一种设计结构,可以用来帮助你追踪上下文,从而使你的代码清晰一致。

我们来看一个不同 Monad 一起使用的基本示例。这些 Monad 虽然很基础,但它们能很好地表达我们的意思。

我们将创建另外 3 个 monad Child,、TeenAdult。这些 monad 会有一些属性,如果你想知道它是ChildTeen还是,可以访问这些属性Adult

const Person = value => ({
  map: fn => Person(fn(value)),
  chain: fn => fn(value),
  value,
})

const Adult = value => ({
  map: fn => Adult(fn(value)),
  chain: fn => fn(value),
  isChild: false,
  isTeen: false,
  isAdult: true,
  value,
})

const Teen = value => ({
  map: fn => Teen(fn(value)),
  chain: fn => fn(value),
  isChild: false,
  isTeen: true,
  isAdult: false,
  value,
})

const Child = value => ({
  map: fn => Child(fn(value)),
  chain: fn => fn(value),
  isChild: true,
  isTeen: false,
  isAdult: false,
  value,
})

我们还将添加用于映射和/或链接的函数。

const objectMapper = fn => value =>
  Object.keys(value).reduce((acc, cur, idx, arr) => ({ ...acc, [cur]: fn(value[cur]) }), value)

const makeUpper = s => (typeof s === 'string' ? s.toUpperCase() : s)

const makeLower = s => (typeof s === 'string' ? s.toLowerCase() : s)

const makeCapitalize = s => (typeof s === 'string' ? s.replace(/(?:^|\s)\S/g, a => a.toUpperCase()) : s)

const addAge = curr => add => curr + add

const setContext = obj => (obj.age < 13 ? Child(obj) : obj.age < 18 ? Teen(obj) : Adult(obj))

const agePerson = age => obj => setContext({ ...obj, age: addAge(obj.age)(age) })

让我们开始玩我们的 monad 吧。

const p1 = {
  firstName: 'matthew',
  lastName: 'staniscia',
  hairColor: 'brown',
  age: 10,
}

Person(p1).map(objectMapper(makeUpper))

/*
Result: This is a Person Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  value:
   { 
     firstName: 'MATTHEW',
     lastName: 'STANISCIA',
     hairColor: 'BROWN',
     age: 10 
   }
}
*/


Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)

/*
Result: This is a Child Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  isChild: true,
  isTeen: false,
  isAdult: false,
  value:
   { 
     firstName: 'MATTHEW',
     lastName: 'STANISCIA',
     hairColor: 'BROWN',
     age: 10 
   }
}
*/


Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))

/*
Result: This is a Teen Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  isChild: false,
  isTeen: true,
  isAdult: false,
  value:
   { 
     firstName: 'matthew',
     lastName: 'staniscia',
     hairColor: 'brown',
     age: 14 
   }
}
*/


Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))
  .chain(agePerson(4))
  .map(objectMapper(makeCapitalize))

/*
Result: This is an Adult Monad
{ 
  map: [Function: map],
  chain: [Function: chain],
  isChild: false,
  isTeen: false,
  isAdult: true,
  value:
   { 
     firstName: 'Matthew',
     lastName: 'Staniscia',
     hairColor: 'Brown',
     age: 18 
   }
}
*/

为了好玩,我们再添加一个 Monad。我们将使用 Pratica 库中的 Maybe Monad,并添加一个函数来判断这个人在美国是否可以喝酒。

import { Maybe } from 'pratica'

const maybeDrinkInUS = obj => (obj.age && obj.age >= 21 ? Maybe(obj) : Maybe())

运行完管道后,我们将返回数据结构或消息。

Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))
  .chain(agePerson(4))
  .map(objectMapper(makeCapitalize))
  .chain(maybeDrinkInUS) // This returns a Maybe Monad
  .cata({
    Just: v => v,
    Nothing: () => 'This Person is too young to drink in the US',
  })

/*
Result
'This Person is too young to drink in the US'
*/

Person(p1)
  .map(objectMapper(makeUpper))
  .chain(setContext)
  .chain(agePerson(4))
  .map(objectMapper(makeLower))
  .chain(agePerson(7)) // Changed this line to now be 21
  .map(objectMapper(makeCapitalize))
  .chain(maybeDrinkInUS) // This returns a Maybe Monad
  .cata({
    Just: v => v,
    Nothing: () => 'This Person is too young to drink in the US',
  })

/*
Result
{ 
  firstName: 'Matthew',
  lastName: 'Staniscia',
  hairColor: 'Brown',
  age: 21 
}
*/

结论

总而言之,Monad 只不过是一个具有以下能力的包装器/上下文/类:

  • 在其自身上下文内映射数据。
  • 通过映射其数据并从其上下文中提取数据来链接。
  • 满足三元律。
  • 它可能有与之相关的额外属性或方法。

来源

以下链接帮助我理解了 Monads 并能够用语言表达出来。

  1. https://dev.to/rametta/basic-monads-in-javascript-3el3
  2. https://www.youtube.com/watch?v=2jp8N6Ha7tY
  3. https://medium.com/front-end-weekly/implementing-javascript-functors-and-monads-a87b6a4b4d9a
  4. https://medium.com/javascript-scene/javascript-monads-made-simple-7856be57bfe8
  5. https://www.codingame.com/playgrounds/2980/practical-introduction-to- functional-programming-with-js/functors-and-monads
  6. https://medium.com/better-programming/tuples-in-javascript-57ede9b1c9d2
  7. https://hackernoon.com/ functional-javascript-functors-monads-and-promises-679ce2ab8abe
  8. https://blog.klipse.tech/javascript/2016/08/31/monads-javascript.html
  9. https://github.com/getify/Functional-Light-JS
  10. https://www.youtube.com/watch?v=ZhuHCtR3xq8
  11. https://marmelab.com/blog/2018/09/26/ functional-programming-3-functor-redone.html
  12. https://www.npmjs.com/package/pratica
文章来源:https://dev.to/bonesmcginty/functors-and-monads-in-javascript-4j29
PREV
仅使用 CSS 属性即可提高页面速度!
NEXT
凌晨4点起床的力量