JavaScript 中的基本 monad

2025-06-10

JavaScript 中的基本 monad

我将解释一些常见的 monad,你现在可以在 JavaScript 中使用它们。monad 可以帮助你的代码更易于阅读、更易于维护,最重要的是——更安全。

或许

Maybe monad 用于处理可空数据。我们经常在 JavaScript 中处理数据,例如格式化、计算、过滤和排序。但我们通常需要在执行任何操作之前确保数据存在。这时 Maybe 就能派上用场了。

我将使用一个名为Pratica的小型友好辅助库来提供本文中 monad 的实现。

让我们看一看可以从 Maybe monad 中受益的片段。

const data = 'Hello my name is Jason'

if (data) {
  console.log(data.toUpperCase()) // HELLO MY NAME IS JASON
}
Enter fullscreen mode Exit fullscreen mode

现在让我们看看如何使用 Maybe 来重构它。

import { Maybe } from 'pratica'

Maybe('Hello my name is Jason')
  .map(data => data.toUpperCase())
  .cata({
    Just: data => console.log(data), // HELLO MY NAME IS JASON
    Nothing: () => console.log('No data available')
  })
Enter fullscreen mode Exit fullscreen mode

瞧,我们不需要检查数据是否存在,因为如果数据为空,Maybe 会自动停止执行任何函数。避免类似这样的错误Uncaught TypeError: Cannot read property 'toUpperCase' of undefined

现在你可能不会立刻看到它的优势,但这并不是 Maybe 的亮点所在。让我们来看另一个步骤更多的例子。

// Step 1: Filter cool people
// Step 2: Find the first cool person
// Step 3: Log their uppercased name if there is one

const data = [
  { name: 'Jason', level: 7, cool: true },
  { name: 'Blanche', level: 8, cool: false }
]

if (data) {
  const coolPeople = data.filter(person => person.cool)
  if (coolPeople) {
    const firstCoolPerson = coolPeople[0]

    if (firstCoolPerson && firstCoolPerson.name) {
      console.log(firstCoolPerson.name.toUpperCase()) // JASON
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

现在让我们看看 Maybe 的替代方案。

import { Maybe } from 'pratica'

Maybe(data)
  .map(people => people.filter(person => person.cool))
  .map(people => people[0])
  .map(person => person.name)
  .map(name => name.toUpperCase())
  .cata({
    Just: data => console.log(data), // JASON
    Nothing: () => console.log('No data available')
  })
Enter fullscreen mode Exit fullscreen mode

如果数据实际上为空或未定义,则所有 .map 函数都不会运行,并且会在 cata 中执行 Nothing 函数。

但是,如果数据为空,我们还想返回一个默认值。那么我们可以使用此.default()方法。

import { Maybe } from 'pratica'

Maybe(null)
  .map(people => people.filter(person => person.cool))
  .map(people => people[0])
  .map(person => person.name)
  .map(name => name.toUpperCase())
  .default(() => 'No cool people yo')
  .cata({
    Just: data => console.log(data), // No cool people yo
    Nothing: () => console.log('No data available')
  })
Enter fullscreen mode Exit fullscreen mode

哇,这么干净,这么平坦。

结果

因此,我们了解到 Maybe monad 非常适合处理可空数据,但是如果我们想检查数据的值并根据值执行不同的操作,该怎么办?

输入结果 monad(或有时称为 Either monad)。

Result 用于“分支”你的逻辑。我们先来看一个没有 Result 的例子。

const person = { name: 'Jason', level: 7, cool: true }

if (person.level === 7) {
  console.log('This person is level 7, ew')
} else {
  console.error('This person is some other level, but not 7')
}
Enter fullscreen mode Exit fullscreen mode

好的,现在有结果了。

import { Ok, Err } from 'pratica'

const person = { name: 'Jason', level: 7, cool: true }

const lvl = person.level === 7
  ? Ok('This person is level 7, ew')
  : Err('This person is some other level, but not 7')

lvl.cata({
  Ok: msg => console.log(msg), // This person is level 7, ew
  Err: err => console.log(err) // This person is some other level, but not 7
})
Enter fullscreen mode Exit fullscreen mode

嗯,我不明白这有什么意义。Ok 和 Err 是什么?这有什么好处吗?

在解释之前我们先再举一个例子。

在这个例子中,我们需要在继续之前验证一些数据。

const data = {
  first: 'Jason',
  level: 85,
  cool: true,
  shirt: {
    size: 'm',
    color: 'blue',
    length: 90,
    logo: {
      color1: '#abc123',
      color2: '#somehexcolor'
    }
  }
}

if (data) {
  if (data.shirt) {
    if (data.shirt.logo) {
      if (data.shirt.logo.color1 !== 'black') {

        // Color1 is valid, now lets continue
        console.log(data.shirt.logo.color1)

      } else {
        console.error ('Color1 is black')
      }
    } else {
      console.error ('No logo')
    }
  } else {
    console.error ('No shirt')
  }
} else {
  console.error ('No data')
}
Enter fullscreen mode Exit fullscreen mode

看起来有点乱。我们看看如何使用 Result 来改进。

import { Ok, Err } from 'pratica'

const hasData = data => data
  ? Ok (data.shirt)
  : Err ('No data')

const hasShirt = shirt => shirt
  ? Ok (shirt.logo)
  : Err ('No shirt')

const hasLogo = logo => logo
  ? Ok (logo.color1)
  : Err ('No logo')

const isNotBlack = color => color !== 'black'
  ? Ok (color)
  : Err ('Color is black')

hasData (data2)
  .chain (hasShirt)
  .chain (hasLogo)
  .chain (isNotBlack)
  .cata ({
    Ok: color => console.log(color), // #abc123
    Err: msg => console.log(msg)
  })
Enter fullscreen mode Exit fullscreen mode

有趣的是,它平坦了很多,但我仍然不明白发生了什么。

好的,事情是这样的。

我们从 hasData 函数开始。它接受需要验证的初始数据,并返回下一个需要验证的数据,但返回的数据被包装在 Result monad 中,更具体地说,是 Ok 或 Err 类型。这两个类型构成了 Result monad,也是我们的应用程序分支逻辑的方式。

.chain()为什么每一行都有这个?

每个函数都会返回 Ok 或 Err 数据类型。但每个函数也期望其输入仅仅是数据,而不是包装在 monad 中的数据。因此,在每个函数上调用 chain 会将数据从 monad 中解包出来,以便函数能够读取其中的内容。

为什么这样更好?

好吧,更好是主观的,但在函数式编程中,这被认为是更好的,因为它将IO(IO是控制台日志语句)推到了程序的边缘。这意味着有更多纯函数可以进行单元测试,并且内部不会混入IO。纯函数中包含IO并不再使它们变得纯粹,这意味着它们会更难进行单元测试,并且会成为bug的根源。控制台日志在JavaScript中并不是什么大问题,但如果IO是发出网络请求,那么这种编程方式就会有很大的不同,因为所有逻辑/验证都将独立于IO,并且更易于测试和维护。

因此,您今天就可以开始使用这两个流行的 monad。

这是我的第一篇 dev.to 文章,请在评论中告诉我您的想法!

如果您想了解有关 monad 的更多信息,请查看这些很酷的文章和库。

鏂囩珷鏉ユ簮锛�https://dev.to/rametta/basic-monads-in-javascript-3el3
PREV
面向 JS 开发人员的 F#
NEXT
项目:COVID 19。😷 免责声明:📝项目目标:👩🏻‍💻使用的技术和语言:📄数据集:📊训练数据集:📈保存模型:🌸创建 UI:👾将它们放在一起:📟现场演示和源代码: