【问题标题】:Monads in JavaScriptJavaScript 中的 Monad
【发布时间】:2012-08-05 22:47:19
【问题描述】:

所以我想了解 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

【问题讨论】:

标签: javascript monads


【解决方案1】:

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

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

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

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

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

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

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

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

USB, short for Universal Serial Bus, is an industry standard that was developed to define cables, connectors and protocols for connection, communication, and power supply between personal computers and their peripheral devices.

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

  1. Object 是(曾经是)那个。
  2. Function 就是那个。
  3. Monad 就是这个。
  4. 规格
  5. 实施
  6. 验证

1.OOP

Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which may contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods. A feature of objects is that an object's procedures can access and often modify the data fields of the object with which they are associated (objects have a notion of "this" or "self"). In OOP, computer programs are designed by making them out of objects that interact with one another. There is significant diversity of OOP languages, but the most popular ones are class-based, meaning that objects are instances of classes, which typically also determine their type.

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

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

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

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

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# 编程中非常需要的,我一直觉得这是非常错误的。

这叫function composition,这才是解除编程束缚的真正秘诀。

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

一个函数是BEFORE => AFTER

基本思想是组合函数。

当你关注功能组合时,你只关心BEFORE => AFTER的各种组合。

当你专注于函数式组合时,你应该忘记flowchart,从代码的顶部流到底部或者有时会循环。

流程图编码被称为Imperative programming,一般来说,它有bug,太复杂了。 OOP 倾向于成为这种风格。

另一方面,函数式编程会自动将编程风格引向Declarative_programming,一般来说,它没有bug,也不容易调试。

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

3.Monad

顺便说一下,这里我不会使用 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]

表示:
ArrayFunction=>Array

函数的输入和输出类型相同:Array

整个BEFORE => AFTER 的数学结构是相同的。

一致性和身份的本质是美丽的。

与USB接口有趣的相似性自然引出了一个想法:
ArrayFunction=>ArrayFunction=>ArrayFunction=>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 数组特定的语法,可以将其替换为更简洁的语法,例如,通过利用诸如 Babel 之类的一些转译器。

所以将.map(F) 替换为*F

Array*F*F*F...

这看起来像algebra

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

在代数中,

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

被称为identity element

在代数中,

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

被称为associative property

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"(Specification for interoperability of common algebraic structures in 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)...

让我们把我原来的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(身份) 2.A.of(x).ap(A.of(f))等价于A.of(f(x))(同态) 3.A.of(y).ap(u)等价于u.ap(A.of(f => f(y)))(互换)

身份
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

实现链规范的值还必须 实施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

【讨论】:

【解决方案2】:

嗯,我认为第一篇文章很棒而且很详细。它描述了 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 函数。

【讨论】:

  • Monad 模式如何导致“更丰富的事件(“mouseenter”、“mouseleave”、“hashchnged”等)”?
  • Monad 不会导致更丰富的事件。没有它你可以有一个 Monad,没有一个 monad 你可以有更丰富的事件。然而,包装 DOM 是一种 monad 品质。通过提供包装器而不是直接 DOM 访问,您可以增强行为。
【解决方案3】:

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

【讨论】:

  • 还有Bacon.js,它似乎更广泛并且正在积极开发中。文档也很好。
【解决方案4】:

将参数传递给特定范围内的给定回调函数可以使用 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);

参考文献

【讨论】:

  • 这个例子如何是一个 Monad 不是很清楚。您能否扩展a)它的单子性质(可能与三个单子定律有关)和b)一个使用示例以及它如何提高对典型代码的理解?谢谢。
  • @Sukima 我已经添加了 cmets 和一个使用示例。
猜你喜欢
  • 1970-01-01
  • 2014-01-10
  • 1970-01-01
  • 2017-08-04
  • 2011-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多