23

所以我想了解 JavaScript 中的 monad 有用的实际案例。

我阅读了一堆关于 JavaScript 中 Monads 的文章,并了解 jQuery 是其使用的一个例子。但是除了“链式”模式,在前端工程中使用 Monads 还能有效解决哪些问题呢?

参考:

http://importantshock.wordpress.com/2009/01/18/jquery-is-a-monad/

http://igstan.ro/posts/2011-05-02-understanding-monads-with-javascript.html

4

4 回答 4

14

这是我为您可能从未在其他任何地方找到的 monads 初学者做出贡献的尝试。

monad 是函数式编程中高度可组合的单元(一种编程的构建块)。

(IMO,在没有任何上下文和合理化的情况下引入“Monad 定律”只是一种无用的分类和理解这个概念的危险。不用担心,我会在本文后面完成这项工作。)

在大多数情况下,我们有多种编程构建块,例如对象、函数、列表等。

尽管拥有多种编程块似乎是自然规律,并且对于实际用途的灵活编程来说是不可避免的,但事实上,拥有多种块是编程环境污染的主要来源之一。

使用各种块构建块是一项复杂的任务。程序员需要在各种情况下从各种块中非常明智地选择一个块,并且在很长一段时间内,他会失败。

因此,不鼓励根据情况选择各种块,相反,始终使用某个普遍标准化的预选块是一个很好的原则。

事实上,这种智慧在当今的 PC 世界中很常见。

USB 是通用串行总线的缩写,是一种行业标准,旨在定义用于个人计算机及其外围设备之间的连接、通信和电源的电缆、连接器和协议。

获得精心设计的通用标准化构建块可以消除许多问题。

  1. 对象是(曾经是)那个。
  2. 功能是唯一的。
  3. 单子就是一个。
  4. 规格
  5. 执行
  6. 确认

1.OOP

面向对象编程(OOP)是一种基于“对象”概念的编程范式,它可能包含数据,以字段的形式,通常称为属性;和代码,以过程的形式,通常称为方法。对象的一个​​特征是对象的过程可以访问并经常修改与其相关联的对象的数据字段(对象具有“this”或“self”的概念)。在 OOP 中,计算机程序是通过将它们制作成相互交互的对象来设计的。OOP 语言有很大的多样性,但最流行的语言是基于类的,这意味着对象是类的实例,这通常也决定了它们的类型。

选择对象作为通用标准化的构建块,程序员准备一个包含成员值和函数的基类,并且为了获得块的变体,使用继承

面向对象的思想通常是用现实世界的物理对象来解释的,而范式本身在数学抽象方面很薄弱。

例如,函数(或方法)从属于对象,函数不需要是一等对象,这是理所当然的,因为范式最初选择对象作为其精心设计的通用标准化构建块。

功能是作为标准化构建块的对象的从属实体并且两者的角色完全不同的观点来自物理世界中的工程意义。不是编程实际所在的数学抽象。

OOP 的基本问题只是对象不是精心设计的通用标准化构建块。具有强大数学背景的函数式编程或 monad 是更好的选择。

2.函数式编程

函数式编程就是组合函数。

说起来容易,但这已经是编程史上的一大成就了。

与其研究编程的悠久历史,不如分享我的个人历史。

我从1.0版本开始就是C#(OOP)程序员,总体上还算满意,但感觉很不对劲,但不知道是什么问题。

后来我成为了一名 JavaScript 程序员,在早期,我曾经这样写:

function add1(a) {
    return a + 1;
}

有一天,我读到一篇网络文章说“在 JavaScript 中,一个函数也是一个值”。

这个事实让我很惊讶,也突破了我的编程技巧。

在那之前,对我来说,价值就是价值,函数就是函数。两者都是不同领域的完全不同的实体。

当然,C#1.0 已经实现了委托,我稍微理解它是关于事件的内部机制的东西。毕竟,C# 一直是主要的 OOP 语言,而且对于函数式编程来说相当丑陋,至少在 version1.0 中是这样。

在 JavaScript 中,函数也是一个值。由于 JavaScript 的函数是一等对象,因此我可以定义一个函数,该函数既可以将其他函数作为参数,也可以将它们作为结果返回。

所以,现在,我写这个:

const add1 = x => x + 1;
const add2 = x => x + 2;
[1, 2, 3].map(add1); //[2,3,4]
[1, 2, 3].map(add2); //[3,4,5]

或者

const plus = (x) => (y => x + y);
plus(1)(5); //6

事实上,这是我在 C# 编程中非常需要的,我一直觉得这是非常错误的。

这就是所谓的函数组合,这是释放编程约束的真正秘诀。

所以,JavaScript 的一个函数是一个一流的对象,它似乎是一个精心设计的通用标准化的构建块,从现在开始,我们称之为“高度可组合的单元”。

一个函数是BEFORE => AFTER

基本思想是组合函数。

当您专注于功能组合时,您只关心BEFORE => AFTER.

当您专注于功能组合时,您应该忘记从代码顶部流向底部或有时循环的流程图。

流程图编码被称为命令式编程,一般来说,它有缺陷且过于复杂。OOP 倾向于成为这种风格。

另一方面,函数式编程会自动将编程风格引入Declarative_programming,而且一般来说,它没有错误或易于调试。

流更难追踪和控制,但组合更容易追踪和控制。程序员不应控制流程,而应编写函数。

3.单子

顺便说一句,我不会在这里使用 Haskell 代码。

对于大多数人来说,理解 monad 事物的一个主要障碍是

  1. 为了学习 monad,初学者需要熟悉 Haskell 代码和术语。
  2. 为了熟悉 Haskell 代码和术语,初学者需要学习 Monad。

这是“先有鸡还是先有蛋?” 问题。确保避免。

话虽如此,正如我在本文开头所说的那样,为了分享 Monad 的知识,首先引用“Monad 法则”似乎也很荒谬。

人们只能在他们已经知道的基础上学习。

那么,让我们回到 JavaScript 代码。

函数似乎是高度可组合的单元,但是这个呢?

console.log("Hello world!");

这是最简单的 JS 代码之一,当然它是一个函数。

在 ChromeBrowser 上按 F12 键,然后将代码复制粘贴到开发人员控制台上。

Hello world!
undefined

好的,代码已经完成了显示“Hello world!”的任务。然而,在控制台上,console.log函数的返回值为undefined.

编写函数,情况不舒服;一个不舒服的功能。

另一方面,有一个舒适的功能。让我们研究以下代码:

const add1 = x => x + 1;
[1, 2, 3].map(add1); //[2,3,4]

JavaScript 中的数组在函数式编程世界中表现得非常好。

[1, 2, 3].map(add1)   //[2,3,4]

表示:
Array Function=> Array

函数的输入和输出是同一类型:Array.

数学结构自始至终是相同的BEFORE => AFTER

一致性和同一性的本质是美丽的。

与 USB 接口的耐人寻味的相似性自然引出了一个想法:
Array Function=> Array Function=> Array Function=> Array...

在 JavaScript 代码中:

[1, 2, 3]
  .map(add1) //[2,3,4]
  .map(add1) //[3,4,5]
  .map(add1);//[4,5,6]

代码提示一旦进入 Array 领域,出口永远是 Array 领域,所以在某种意义上没有出口。

由于数组领域是一个自包含的世界,因此可以在函数式编程中做类似代数的事情。

当我们有:

Array.map(F).map(F).map(F)...

考虑到.map(F)JavaScript Array 特定的语法,将其替换为更简洁的语法是可能的,例如,通过利用诸如Babel之类的转译器。

所以替换.map(F)*F

Array*F*F*F...

这看起来像代数

获得高度可组合的单元,程序员可以写出像代数一样的代码,意义重大,值得非常认真地研究。

在代数中,

a
= 0+a
= 0+0+a
= 0+0+0+a

或者

a
= 1*a
= 1*1*a
= 1*1*1*a

0在+(加法)运算中,

a + 0 = a  //right identity
0 + a = a  //left identity

1在*(乘法)运算中,

a ∗ 1 = a  //right identity
1 ∗ a = a  //left identity

称为恒等元

在代数中,

1 + 2 + 3 = 1 + 2 + 3
(1+2) + 3 = 1 + (2+3)
    3 + 3 = 1 + 5
        6 = 6

称为关联属性

number + number = number

number * number = number

string + string = string

"Hello" + " " + "world" + "!" 
= "Hello world" + "!" 
= "Hello "+ "world!"

也是关联的,标识元素是""

那么,函数式编程中的标识元素是什么?

就像是:

identityF * f = f = f * identityF

函数式编程中的关联属性是什么?

const add1 = x => x + 1;
const add2 = x => x + 2;
const add3 = x => x + 2;

就像是:

add1 * add2 * add3
= (add1 * add2) * add3
= add1 * (add2 * add3)

或者

  (add1)(add2)(add3) = (add1)(add2)(add3)
 ((add1)(add2))(add3) = (add1)((add2)(add3))
         (add3)(add3) = (add1)(add5)
              (add6) = (add6)

函数式编程是关于函数组合的。

我们在函数式编程中需要的是

function * function = function

当然,在 JavaScript(或其他语言)中,由于每种语言的语法限制,我们不能写出上面的确切形式。

其实我们可以有《Algebraic JavaScript Specification》(JavaScript中常见代数结构的互操作性规范)

代数 JavaScript 规范

那么 JavaScript 数组就是所谓的 Monad 吗?

不,但很接近。JavaScript 数组可以归类为Functor

Monad是 Functor 的一种特殊形式,具有一些额外的性质(应用了更多规则)。

Functor 仍然是高度可组合的单元之一。

所以我们正在接近 Monad 是什么。让我们更进一步。

现在,我们知道 JavaScript 数组是可以做一些代数的高度可组合的单元之一,至少在一定程度上是这样。

那么除了数组之外的 JavaScript 值呢?功能呢?

学习并遵循Algebraic JavaScript Specification,很容易尝试实现各种可组合单元,包括 Functor 或 Monad,有什么意义?

毕竟,它们只是数学结构的分类表,一味地遵循规范是没有意义的。

4.规格

关键是要获得一个高度可组合的单元,该单元是独立的。这是唯一要满足的规范。

所以,这里是问题的建立:
实现一个生成自包含领域的数学结构,看看它是如何进行的。

什么都好,我会从头开始,但我已经有一个好的模型可以参考。

JavaScript 数组

Array.map(F).map(F).map(F)...

代替 Array 领域,让我们将我的原始M领域变成这样:

M.map(F).map(F).map(F)...

我认为Array.map不是简洁的语法,M它本身就是一个函数:

M(F)(F)(F)...

嗯,总是使用某个预先选择的、普遍标准化的块是一门很好的纪律。这就是开始的想法,所以可能F也应该是M

M(M)(M)(M)...

嗯,这是什么意思??

所以,这是我的疯狂想法。

在函数式编程中,任何函数也是一等对象,这就是突破。所以当我将任何值/对象/函数解释为M时,就会有另一个突破。

这就像说“任何值都是数组!”一样疯狂。

确切地说,如果它在 JavaScript 领域是疯狂的,但如果它在 Array 的自包含领域是合法的。

因此,我将设计原始M领域将任何裸值/对象/函数视为M

例如,在M领域中,当找到裸值:时5,解释为M(5).

换句话说,只要在M领域中,程序员就不必编写M(5),因为5被隐式解释为M(5).

因此,在M领域:

5
= M(5)
= M(M(5))
= M(M(M(5)))
...

结果,我发现M有些透明,M应该是领域中的一个身份元素。

正如我一直在强调的,函数式编程就是组合函数。

函数的组合对于函数式编程是关联的。

M应该灵活地编写来组合函数 :

const add1 = x => x + 1;
M(10)(add1);             //11
M(10)(add1)(add1);       //12
M(10)(add1)(add1)(add1); //13
const add2 = M(add1)(add1);
M(10)(add2);             //12
const add3 = M(add2)(add1);   
M(10)(add3);             //13

此外,高阶函数的组合:

const plus = (x) => (y => x + y);
M(plus(1)(5));    //6
M(5)(M(1)(plus)); //6
const plus1 = M(1)(plus);
M(5)(plus1)(;     //6

5.实施

这是一个实现M

const compose = (f, g) => (x => g(f(x)));
const isMonad = (m) => !(typeof m.val === "undefined");

const M = (m = []) => {
  const f = m1 => {
    try { //check type error
      return M(M(m1).val(m));
    } catch (e) {
      return M(compose(m, M(m1).val)); // f-f compose
    };
  };
  f.val = m;
  return isMonad(m)
    ? m
    : f;
};
M.val = m => m;

记录功能:

const log = (m) => (typeof m !== 'function')
  ? (() => {
    console.log(m);
    return m;
  })()
  : err();

测试代码:

const err = () => {
  throw new TypeError();
};

const log = (m) => (typeof m !== 'function')
  ? (() => {
    console.log(m);
    return m;
  })()
  : err();

const loglog = M(log)(log);
M("test")(loglog);

M("------")(log);
M([1])(log);
M(M(M(5)))(log)
M(99)(M)(log)

M("------")(log);
M([1, 2, 3])(([a, b, c]) => [a + 1, b + 1, c + 1])(log)

M("------")(log);

const add1 = a => (typeof a == 'number')
  ? a + 1
  : err();

M(10)(add1)(log); //11
M(10)(add1)(add1)(log); //12
M(10)(add1)(add1)(add1)(log); //13
const add2 = M(add1)(add1);
M(10)(add2)(log); //12
const add3 = M(add2)(add1);
M(10)(add3)(log); //13

M("------")(log);
const plus = (x) => (y => x + y);
M(plus(1)(5))(log); //6
M(5)(M(1)(plus))(log); //6
const plus1 = M(1)(plus);
M(5)(plus1)(log); //6

M("------")(log);
const map = (f) => (array => array.map(f));
const map1 = M(add1)(map);
M([1, 2, 3])(log)(map1)(log);

//===

M("left identity   M(a)(f) = f(a)")(log);
M(7)(add1)(log) //8

M("right identity  M = M(M)")(log);
console.log(M) //{ [Function: M] val: [Function] }
console.log(M(M)) //{ [Function: M] val: [Function] }

M("identity")(log);
M(9)(M(x => x))(log); //9
M(9)(x => x)(log); //9

M("homomorphism")(log);
M(100)(M(add1))(log); //101
M(add1(100))(log); //101

M("interchange")(log);
M(3)(add1)(log); //4
M(add1)(f => f(3))(log); //4

M("associativity")(log);
M(10)(add1)(add1)(log); //12
M(10)(M(add1)(add1))(log); //12

输出:

test
test
------
[ 1 ]
5
99
------
[ 2, 3, 4 ]
------
11
12
13
12
13
------
6
6
6
------
[ 1, 2, 3 ]
[ 2, 3, 4 ]
left identity   M(a)(f) = f(a)
8
right identity  M = M(M)
{ [Function: M] val: [Function] }
{ [Function: M] val: [Function] }
identity
9
9
homomorphism
101
101
interchange
4
4
associativity
12
12

好的,工作。

M是函数式编程中高度可组合的单元。

6.验证

那么,这就是所谓的Monad吗?

是的。

https://github.com/fantasyland/fantasy-land#monad

单子

实现 Monad 规范的值也必须实现ApplicativeChain规范。1.M.of(a).chain(f)等价于f(a)(左身份) 2.m.chain(M.of)等价于m(右身份)

左恒等式 M(a)(f) = f(a)
M(7)(add1) //8
M(add1(7)) //8
正确的身份 M = M(M)
console.log(M) //{ [Function: M] val: [Function] }
console.log(M(M)) //{ [Function: M] val: [Function] }

适用的

实现 Applicative 规范的值也必须实现Apply规范。1.v.ap(A.of(x => x))等价于v(identity) 2.A.of(x).ap(A.of(f))等价于A.of(f(x))(homomorphism) 3.A.of(y).ap(u)等价于u.ap(A.of(f => f(y)))(interchange)

身份
M(9)(M(x => x)) //9
同态
M(100)(M(add1)) //101
M(add1(100)) //101
互换
M(3)(add1)    //4
M(add1)(f => f(3))  //4

实现 Chain 规范的值也必须实现Apply规范。1.m.chain(f).chain(g)等价于m.chain(x => f(x).chain(g))(关联性)

联想性
M(10)(add1)(add1) //12
M(10)(M(add1)(add1)) //12
于 2018-05-23T00:20:26.510 回答
9

好吧,我认为第一篇文章很棒而且很详细。它描述了 JQuery 解决的许多问题及其 monad 特性。

  1. JQuery 包装了 DOM 元素并提供了更丰富的界面。解决的问题很多:更丰富的事件(“mouseenter”、“mouseleave”、“hashchnged”等)。事件绑定添加处理程序而不是覆盖。CSS 处理的接口类似于 JQuery 公开的其他接口。

这也是 JQuery 对许多开发人员如此直观的原因,因为它只是简单地包装了我们所知道的内容,而不是试图重新发明 HTML。

更不用说它在引用空值时节省了很多错误。如果我没有带有 id 的元素guy,则运行$("#guy").text("I am not here")不会导致 JQuery 出错。

  1. JQuery 很容易将自己包裹在 DOM 元素周围,允许在原始 JS 和 JQuery 的接口之间来回遍历。这允许开发人员按照自己的节奏学习 JQuery,而不是一次性重写整个代码。

  2. 当 JQuery 使用参数提供回调时,它使用 DOM 对象而不是 JQuery 的包装器。这允许第 3 方轻松与 JQuery 集成,因为他们不需要依赖 JQuery。例如,假设我编写了一个使用原始 JavaScript 将文本绘制为红色的函数。function paintRed(element){element.style.color="red"}- 我可以轻松地将此函数作为回调传递给 JQuery 函数。

于 2012-08-21T21:13:29.450 回答
3

使用 monad 时可以避免使用全局变量和状态(例如“纯”代码)。您可能还想看看https://github.com/brownplt/flapjax/。Flapjax 是一个函数响应式编程库,它也使用单子方法。

于 2012-08-15T09:16:41.910 回答
0

将参数传递给特定范围内的给定回调函数可以使用 monad 进行泛化:

/* Unit function */
function Monad(value)
  {
  // Construct monad and set value to given argument or undefined
  this.value = value || undefined;
  }

/* Constructor function */
Monad.prototype.pass = function(value, cb, scope)
  {
  // return constructor result if no default value is passed
  if (/undefined/.test(this.value) )
    {
    return new this.constructor();
    }
  // return callback result for given value in given context if scope is passed
  if(scope)
    {
    /* Bind function */
    return cb.call(scope, value);
    }
  // return callback result for given value otherwise
  return cb(value);
  }

 /* Separate arguments from function, and function from global scope */
 var foo = new Monad(RegExp);
 var bar = foo.pass(2, Function("count","return ++count"), Math);

参考

于 2013-01-22T20:30:11.527 回答