有同步承诺这样的概念吗?
本杰明是绝对正确的。Promise 是一种 monad https://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/。然而,它们并不是唯一的类型。
如果您还没有意识到这一点,那么您可能想知道 monad 是什么。网上有很多关于 monad 的解释。然而,他们中的大多数都患有Monad 教程谬误 https://byorgey.wordpress.com/2009/01/12/abstraction-intuition-and-the-monad-tutorial-fallacy/.
简而言之,这个谬论是大多数理解单子的人并不真正知道如何向其他人解释这个概念。简单来说,单子是一个抽象的概念,人类很难掌握抽象的概念。然而,人类很容易理解具体的概念。
因此,让我们从一个具体的概念开始,开始理解单子。正如我所说,单子是一个抽象概念。这意味着 monad 是一个界面 http://en.wikipedia.org/wiki/Interface_%28computing%29没有执行 http://en.wikipedia.org/wiki/Implementation(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成)。
现在,有不同类型的单子。每种类型的 monad 都是具体的(即它定义了一个执行 http://en.wikipedia.org/wiki/Implementation单子的界面 http://en.wikipedia.org/wiki/Interface_%28computing%29)。 Promise 是一种 monad。因此,promise 是 monad 的一个具体例子。因此,如果我们研究 Promise,那么我们就可以开始理解 monad。
那么我们从哪里开始呢?幸运的是,用户spike https://stackoverflow.com/users/896697/spike给了我们一个很好的起点comment https://stackoverflow.com/questions/28937788/would-there-be-any-benefit-to-writing-synchronous-code-using-the-syntax-of-promi#comment46130667_28937788对于你的问题:
我能想到的一个例子是将 Promise 与同步代码链接在一起。在寻找这个问题的答案时:根据场景动态生成AJAX请求 https://stackoverflow.com/q/28418601/783743我将同步调用包装在 Promise 中,以便能够将它们与其他 Promise 链接起来。
那么我们来看看他的代码:
var run = function() {
getScenario()
.then(mapToInstruction)
.then(waitForTimeout)
.then(callApi)
.then(handleResults)
.then(run);
};
这里的run
函数返回一个承诺,它由返回的承诺组成getScenario
, mapToInstruction
, waitForTimeout
, callApi
, handleResults
and run
本身被锁链在一起。
现在,在我们继续之前,我想向您介绍一种新的符号来可视化这些函数正在做什么:
run :: Unit -> Deferred a
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
waitForTimeout :: Instruction -> Deferred Instruction
callApi :: Instruction -> Deferred Data
handleResults :: Data -> Deferred Unit
所以这是细分:
- The
::
符号的意思“属于这种类型”和->
符号的意思“to”。因此,例如,run :: Unit -> Deferred a
读作“run
属于类型Unit
to Deferred a
”.
- 这意味着
run
是一个函数,它需要一个Unit
value(即无参数)并返回 type 的值Deferred a
.
- Here,
a
表示任何类型。我们不知道什么类型a
是并且我们不关心什么类型a
是。因此,它可以是任何类型。
- Here,
Deferred
是一个承诺数据类型(具有不同的名称)并且Deferred a
意味着当承诺得到解决时,它会产生一个类型的值a
.
从上面的可视化中我们可以学到一些东西:
- 每个函数都接受一些值并返回一个承诺。
-
每个 Promise 返回的解析值将成为下一个函数的输入:
run :: Unit -> Deferred a
getScenario :: Unit -> Deferred Data
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
mapToInstruction :: Data -> Deferred Instruction
waitForTimeout :: Instruction -> Deferred Instruction
waitForTimeout :: Instruction -> Deferred Instruction
callApi :: Instruction -> Deferred Data
callApi :: Instruction -> Deferred Data
handleResults :: Data -> Deferred Unit
handleResults :: Data -> Deferred Unit
run :: Unit -> Deferred a
在前一个 Promise 得到解析之前,下一个函数无法执行,因为它必须使用前一个 Promise 的解析值。
现在,正如我之前提到的,单子是界面 http://en.wikipedia.org/wiki/Interface_%28computing%29它定义了某些操作。 monad 接口提供的操作之一是链接 monad 的操作。如果是承诺的话,这是then
方法。例如:
getScenario().then(mapToInstruction)
我们知道:
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
Hence:
getScenario() :: Deferred Data -- because when called, getScenario
-- returns a Deferred Data value
我们还知道:
getScenario().then(mapToInstruction) :: Deferred Instruction
由此,我们可以推论:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
用言语来说,“then
是一个带有两个参数的函数(类型的值Deferred a
和类型的函数a -> Deferred b
) 并返回类型的值Deferred b
.” Hence:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
getScenario() :: Deferred Data
-- Therefore, since a = Data
getScenario().then :: (Data -> Deferred b) -> Deferred b
mapToInstruction :: Data -> Deferred Instruction
-- Therefor, since b = Instruction
getScenario().then(mapInstruction) :: Deferred Instruction
所以我们得到了第一个 monad 操作:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
然而,这个操作是具体的。它特定于承诺。我们想要一个可以适用于任何 monad 的抽象操作。因此,我们概括该函数,使其可以适用于任何 monad:
bind :: Monad m => m a -> (a -> m b) -> m b
请注意,这bind
函数与 无关Function.prototype.bind https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind. This bind
函数是then
功能。然后then
函数特定于 Promise。但是,那bind
功能是通用的。它可以适用于任何单子m
.
胖箭头=>
means 有界量化 https://en.wikipedia.org/wiki/Bounded_quantification. If a
and b
可以是任何类型m
可以是任何类型它实现了 monad 接口。我们不在乎什么类型m
只要它实现了 monad 接口即可。
这就是我们实施和使用的方式bind
JavaScript 中的函数:
function bind(m, f) {
return m.then(f);
}
bind(getScenario(), mapToInstruction);
这个怎么通用呢?好吧,我可以创建一个新的数据类型来实现then
功能:
// Identity :: a -> Identity a
function Identity(value) {
this.value = value;
}
// then :: Identity a -> (a -> Identity b) -> Identity b
Identity.prototype.then = function (f) {
return f(this.value);
};
// one :: Identity Number
var one = new Identity(1);
// yes :: Identity Boolean
var yes = bind(one, isOdd);
// isOdd :: Number -> Identity Boolean
function isOdd(n) {
return new Identity(n % 2 === 1);
}
代替bind(one, isOdd)
我本可以轻松地写出one.then(isOdd)
(这实际上更容易阅读)。
The Identity
数据类型,就像 Promise 一样,也是 monad 的一种类型。事实上,它是所有单子中最简单的。它被称为Identity
因为它不会对其输入类型执行任何操作。它保持原样。
不同的单子有不同的作用,这使得它们很有用。例如,Promise 具有管理异步性的效果。这Identity
然而 monad 没有任何效果。它是一个vanilla数据类型。
不管怎样,继续......我们发现了 monad 的一种操作,bind
功能。还有一项操作有待发现。事实上,用户spike https://stackoverflow.com/users/896697/spike他在上述评论中提到了这一点:
我将同步调用包装在 Promise 中,以便能够将它们与其他 Promise 链接起来。
你看,问题是第二个参数then
函数必须是一个返回 Promise 的函数:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
|_______________|
|
-- second argument is a function
-- that returns a promise
这意味着第二个参数必须是异步的(因为它返回一个承诺)。然而,有时我们可能希望将同步函数与then
。为此,我们将同步函数的返回值包装在 Promise 中。例如,这就是spike https://stackoverflow.com/users/896697/spike did:
// mapToInstruction :: Data -> Deferred Instruction
// The result of the previous promise is passed into the
// next as we're chaining. So the data will contain the
// result of getScenario
var mapToInstruction = function (data) {
// We map it onto a new instruction object
var instruction = {
method: data.endpoints[0].method,
type: data.endpoints[0].type,
endpoint: data.endpoints[0].endPoint,
frequency: data.base.frequency
};
console.log('Instructions recieved:');
console.log(instruction);
// And now we create a promise from this
// instruction so we can chain it
var deferred = $.Deferred();
deferred.resolve(instruction);
return deferred.promise();
};
正如你所看到的,返回值mapToInstruction
函数是instruction
。然而,我们需要将它包装在一个 Promise 对象中,这就是我们这样做的原因:
// And now we create a promise from this
// instruction so we can chain it
var deferred = $.Deferred();
deferred.resolve(instruction);
return deferred.promise();
事实上,他也在做同样的事情handleResults
函数还有:
// handleResults :: Data -> Deferred Unit
var handleResults = function(data) {
console.log("Handling data ...");
var deferred = $.Deferred();
deferred.resolve();
return deferred.promise();
};
最好将这三行放入一个单独的函数中,这样我们就不必重复自己:
// unit :: a -> Deferred a
function unit(value) {
var deferred = $.Deferred();
deferred.resolve(value);
return deferred.promise();
}
使用这个unit
我们可以重写函数mapToInstruction
and handleResults
如下:
// mapToInstruction :: Data -> Deferred Instruction
// The result of the previous promise is passed into the
// next as we're chaining. So the data will contain the
// result of getScenario
var mapToInstruction = function (data) {
// We map it onto a new instruction object
var instruction = {
method: data.endpoints[0].method,
type: data.endpoints[0].type,
endpoint: data.endpoints[0].endPoint,
frequency: data.base.frequency
};
console.log('Instructions recieved:');
console.log(instruction);
return unit(instruction);
};
// handleResults :: Data -> Deferred Unit
var handleResults = function(data) {
console.log("Handling data ...");
return unit();
};
事实上,事实证明unit
function 是 monad 接口中第二个缺失的操作。概括后,可以将其可视化如下:
unit :: Monad m => a -> m a
它所做的只是将一个值包装在 monad 数据类型中。这允许您将常规值和函数提升到一元上下文中。例如,Promise 提供异步上下文并且unit
允许您将同步函数提升到这个异步上下文中。类似地,其他单子也提供其他效果。
构成unit
with a function 允许您将函数提升到一元上下文中。例如,考虑isOdd
我们之前定义的函数:
// isOdd :: Number -> Identity Boolean
function isOdd(n) {
return new Identity(n % 2 === 1);
}
如果按如下方式定义它会更好(尽管速度较慢):
// odd :: Number -> Boolean
function odd(n) {
return n % 2 === 1;
}
// unit :: a -> Identity a
function unit(value) {
return new Identity(value);
}
// isOdd :: Number -> Identity Boolean
function idOdd(n) {
return unit(odd(n));
}
如果我们使用一个,它看起来会更好compose
功能:
// compose :: (b -> c) -> (a -> b) -> a -> c
// |______| |______|
// | |
function compose( f, g) {
// compose(f, g) :: a -> c
// |
return function ( x) {
return f(g(x));
};
}
var isOdd = compose(unit, odd);
我之前提到过 monad 是界面 http://en.wikipedia.org/wiki/Interface_%28computing%29没有执行 http://en.wikipedia.org/wiki/Implementation(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成)。因此,单子是一个接口:
- 定义某些操作。
- 指定这些操作应该做什么。
我们现在知道 monad 的两个操作是:
bind :: Monad m => m a -> (a -> m b) -> m b
unit :: Monad m => a -> m a
现在,我们将看看这些操作应该做什么或者它们应该如何表现(即我们将看看管理 monad 的法则):
// Given:
// x :: a
// f :: Monad m => a -> m b
// h :: Monad m => m a
// g :: Monad m => b -> m c
// we have the following three laws:
// 1. Left identity
bind(unit(x), f) === f(x)
unit(x).then(f) === f(x)
// 2. Right identity
bind(h, unit) === h
h.then(unit) === h
// 3. Associativity
bind(bind(h, f), g) === bind(h, function (x) { return bind(f(x), g); })
h.then(f).then(g) === h.then(function (x) { return f(x).then(g); })
给定一个我们可以定义的数据类型then
and unit
违反这些法律的职能。在这种情况下,那些特定的实现then
and unit
是不正确的。
例如,数组是一种表示非确定性计算的 monad。让我们定义一个不正确的unit
数组的函数(bind
数组的函数是正确的):
// unit :: a -> Array a
function unit(x) {
return [x, x];
}
// concat :: Array (Array a) -> Array a
function concat(h) {
return h.concat.apply([], h);
}
// bind :: Array a -> (a -> Array b) -> Array b
function bind(h, f) {
return concat(h.map(f));
}
这个错误的定义unit
对于数组违反第二定律(右恒等式):
// 2. Right identity
bind(h, unit) === h
// proof
var h = [1,2,3];
var lhs = bind(h, unit) = [1,1,2,2,3,3];
var rhs = h = [1,2,3];
lhs !== rhs;
正确的定义是unit
对于数组来说是:
// unit :: a -> Array a
function unit(x) {
return [x];
}
需要注意的一个有趣的属性是数组bind
功能是按照以下方式实现的concat
and map
。然而,数组并不是唯一拥有此属性的 monad。每个单子bind
函数可以用通用的一元版本来实现concat
and map
:
concat :: Array (Array a) -> Array a
join :: Monad m => m (m a) -> m a
map :: (a -> b) -> Array a -> Array b
fmap :: Functor f => (a -> b) -> f a -> f b
如果您对什么感到困惑functor https://en.wikipedia.org/wiki/Functor那就别担心了。函子只是一种实现以下功能的数据类型fmap
功能。根据定义,每个单子也是一个函子。
我不会详细介绍单子定律以及如何实现fmap
and join
加在一起就相当于bind
。您可以在以下位置阅读有关它们的信息维基百科页面 https://en.wikipedia.org/wiki/Monad_(functional_programming)#Monad_laws.
顺便说一句,根据JavaScript 幻想世界规范 https://github.com/fantasyland/fantasy-land the unit
函数被调用of
和bind
函数被调用chain
。这将允许您编写如下代码:
Identity.of(1).chain(isOdd);
无论如何,回到你的主要问题:
使用 Promise 语法编写同步代码有什么好处吗?
是的,使用 Promise 语法(即一元代码)编写同步代码可以获得很大的好处。许多数据类型都是 monad,使用 monad 接口,您可以对不同类型的顺序计算进行建模,例如异步计算、非确定性计算、失败计算、状态计算、日志记录计算等。我最喜欢的使用 monad 的示例之一是使用免费 monad 来创建语言解释器 http://debasishg.blogspot.in/2013/01/a-language-and-its-interpretation.html.
Monad 是函数式编程语言的一个特性。使用 monad 可以促进代码重用。从这个意义上来说,它绝对是好的。然而,这是有代价的。函数代码比过程代码慢几个数量级。如果这对您来说不是问题,那么您绝对应该考虑编写单子代码。
一些更流行的 monad 是数组(用于非确定性计算),Maybe
monad(对于可能失败的计算,类似于NaN
以浮点数表示)和单子解析器组合器 https://hackage.haskell.org/package/parsec.
try {
foo();
bar(a, b);
bam();
} catch(e) {
handleError(e);
}
...可以写成类似的东西(但使用同步版本then
);
foo()
.then(bar.bind(a, b))
.then(bam)
.fail(handleError)
是的,你绝对可以编写这样的代码。请注意,我没有提到任何有关fail
方法。原因是你不需要特殊的fail
根本没有方法。
例如,让我们为可能失败的计算创建一个 monad:
function CanFail() {}
// Fail :: f -> CanFail f a
function Fail(error) {
this.error = error
}
Fail.prototype = new CanFail;
// Okay :: a -> CanFail f a
function Okay(value) {
this.value = value;
}
Okay.prototype = new CanFail;
// then :: CanFail f a -> (a -> CanFail f b) -> CanFail f b
CanFail.prototype.then = function (f) {
return this instanceof Okay ? f(this.value) : this;
};
然后我们定义foo
, bar
, bam
and handleError
:
// foo :: Unit -> CanFail Number Boolean
function foo() {
if (someError) return new Fail(1);
else return new Okay(true);
}
// bar :: String -> String -> Boolean -> CanFail Number String
function bar(a, b) {
return function (c) {
if (typeof c !== "boolean") return new Fail(2);
else return new Okay(c ? a : b);
};
}
// bam :: String -> CanFail Number String
function bam(s) {
if (typeof s !== "string") return new Fail(3);
else return new Okay(s + "!");
}
// handleError :: Number -> Unit
function handleError(n) {
switch (n) {
case 1: alert("unknown error"); break;
case 2: alert("expected boolean"); break;
case 3: alert("expected string"); break;
}
}
最后,我们可以如下使用它:
// result :: CanFail Number String
var result = foo()
.then(bar("Hello", "World"))
.then(bam);
if (result instanceof Okay)
alert(result.value);
else handleError(result.error);
The CanFail
我描述的 monad 实际上是Either
函数式编程语言中的 monad。希望有帮助。