是否存在同步承诺这样的概念?使用promises的语法编写同步代码会有什么好处吗?
try {
foo();
bar(a, b);
bam();
} catch(e) {
handleError(e);
}
...可以写成(但使用then
的同步版本);
foo()
.then(bar.bind(a, b))
.then(bam)
.fail(handleError)
答案 0 :(得分:17)
是否存在同步承诺这样的概念?
本杰明是绝对正确的。 Promises are a type of monad。但是,它们不是唯一的类型。
如果您还没有意识到这一点,那么您可能想知道monad是什么。网上有很多关于monad的解释。但是,他们中的大多数都患有monad tutorial fallacy。
简而言之,谬论是大多数了解monad的人并不真正知道如何向他人解释这个概念。简单来说,monad是一个抽象概念,人类很难掌握抽象概念。然而,人类很容易理解具体的概念。
因此,让我们开始征服,从一个具体的概念开始理解monad。正如我所说,monad是一个抽象的概念。这意味着monad是interface而没有implementation(即它定义了某些操作并指定了这些操作应该做什么,而没有指定必须如何完成)。
现在,有不同类型的monad。每种类型的monad都是具体的(即它定义了monad implementation的interface)。承诺是一种单子行为。因此,承诺是monad的具体例子。因此,如果我们研究承诺,那么我们就可以开始理解单子。
那么我们从哪里开始呢?幸运的是,用户spike在comment问题中为我们提供了一个很好的起点:
我能想到的一个实例是将promises与同步代码链接在一起。在找到这个问题的答案时:Generating AJAX Request Dynamically Based on Scenario我在一个承诺中包含了一个同步调用,以便能够将它们与其他承诺链接起来。
让我们看看他的代码:
var run = function() {
getScenario()
.then(mapToInstruction)
.then(waitForTimeout)
.then(callApi)
.then(handleResults)
.then(run);
};
此处run
函数返回一个承诺,该承诺由getScenario
,mapToInstruction
,waitForTimeout
,callApi
,{{1}返回的承诺组成和handleResults
本身链接在一起。
现在,在我们继续之前,我想向您介绍一种新的符号,以便可视化这些功能正在做什么:
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
符号表示“属于”,::
符号表示“至”。因此,例如,->
读取“run :: Unit -> Deferred a
类型为run
至Unit
”。Deferred a
是一个函数,它接受run
值(即没有参数)并返回Unit
类型的值。Deferred a
表示任何类型。我们不知道a
是什么类型,我们不关心a
类型。因此,它可以是任何类型。a
是一种承诺数据类型(名称不同),Deferred
表示在解析承诺时会产生Deferred a
类型的值。我们可以从上述可视化中学到一些东西:
每个promise返回的已解析值将成为下一个函数的输入:
a
在解决上一个承诺之前,下一个函数无法执行,因为它必须使用先前承诺的已解析值。
现在,正如我之前提到的,monad是interface,它定义了某些操作。 monad接口提供的操作之一是链接monad的操作。在承诺的情况下,这是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
方法。例如:
then
我们知道:
getScenario().then(mapToInstruction)
因此:
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
我们也知道:
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
所以我们得到了第一个monad操作:
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的抽象操作。因此,我们推广该函数,使其适用于任何monad:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
请注意,此bind :: Monad m => m a -> (a -> m b) -> m b
功能与Function.prototype.bind
无关。此bind
函数是bind
函数的推广。然后then
函数特定于promise。但是,then
函数是通用的。它适用于任何monad bind
。
胖箭m
表示bounded quantification。如果=>
和a
可以是任何类型,那么b
可以是任何类型实现monad接口。只要它实现了monad接口,我们就不在乎m
类型。
这是我们在JavaScript中实现和使用m
函数的方法:
bind
这是如何通用的?好吧,我可以创建一个实现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)
数据类型与promises一样,也是一种monad。事实上,它是所有monad中最简单的。它被称为Identity
因为它没有对其输入类型做任何事情。它保持原样。
不同的monad具有不同的效果,使它们有用。例如,承诺具有管理异步性的效果。然而Identity
monad没有效果。它是 vanilla 数据类型。
无论如何,继续......我们发现了monad的一个操作,即Identity
函数。还有一项操作还有待发现。事实上,用户spike在上述评论中暗示了这一点:
我在一个承诺中包含了一个同步调用,以便能够将它们与其他承诺链接起来。
你知道,问题是bind
函数的第二个参数必须是一个返回一个promise的函数:
then
这意味着第二个参数必须是异步的(因为它返回一个promise)。但是,有时我们可能希望使用then :: Deferred a -> (a -> Deferred b) -> Deferred b
|_______________|
|
-- second argument is a function
-- that returns a promise
链接同步函数。为此,我们将promise中的同步函数的返回值包装起来。例如,这是spike所做的:
then
如您所见,// 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
。但是,我们需要将它包装在promise对象中,这就是我们这样做的原因:
instruction
事实上,他在// 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
,如下所示:
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();
};
函数是monad接口的第二个缺失操作。一般化时,可以如下显示:
unit
它只是在monad数据类型中包装一个值。这允许您将常规值和函数提升到monadic上下文中。例如,promises提供异步上下文,unit :: Monad m => a -> m a
允许您将同步函数提升到此异步上下文中。同样,其他monad提供其他效果。
使用函数编写unit
可以将函数提升到monadic上下文中。例如,考虑我们之前定义的unit
函数:
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
我之前提到monad是一个没有interface的implementation(即它定义了某些操作并指定了这些操作应该做什么,而没有说明必须如何完成)。因此,monad是一个接口:
我们现在知道monad的两个操作是:
// 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的法律):
bind :: Monad m => m a -> (a -> m b) -> m b
unit :: Monad m => a -> m a
根据数据类型,我们可以为其定义违反这些法律的// 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
函数。在这种情况下,unit
和then
的特定实现是不正确的。
例如,数组是一种表示非确定性计算的monad。让我们为数组定义一个不正确的unit
函数(数组的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
实现的。但是,数组并不是唯一拥有此属性的monad。每个monad map
函数都可以使用bind
和concat
的广义monadic版本来实现:
map
如果您对functor的内容感到困惑,请不要担心。仿函数只是实现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
函数的数据类型。根据定义,每个monad也是一个仿函数。
我不会详细了解monad法律以及fmap
和fmap
如何相等于join
。您可以在Wikipedia page上了解它们。
在旁注中,根据JavaScript Fantasy Land Specification,bind
函数称为unit
,of
函数称为bind
。这将允许您编写如下代码:
chain
无论如何,回到你的主要问题:
使用promises语法编写同步代码会有什么好处吗?
是的,使用promises语法(即monadic代码)编写同步代码可以获得很大的好处。许多数据类型都是monad,使用monad接口,你可以建模不同类型的顺序计算,如异步计算,非确定性计算,失败计算,状态计算,记录计算等。我最喜欢使用monads的一个例子是使用free monads to create language interpreters。
Monads是函数式编程语言的一个特性。使用monads可以促进代码重用。从这个意义上来说它绝对是好的。但是,它会受到惩罚。功能代码比程序代码慢几个数量级。如果这对您来说不是问题,那么您一定要考虑编写monadic代码。
一些比较流行的monad是数组(用于非确定性计算),Identity.of(1).chain(isOdd);
monad(用于可能失败的计算,类似于浮点数中的Maybe
)和{{3} }。
NaN
...可以写成(但使用
try { foo(); bar(a, b); bam(); } catch(e) { handleError(e); }
的同步版本);then
是的,你绝对可以编写这样的代码。请注意,我没有提及有关foo()
.then(bar.bind(a, b))
.then(bam)
.fail(handleError)
方法的任何内容。原因是您根本不需要特殊的fail
方法。
例如,让我们为可能失败的计算创建一个monad:
fail
然后我们定义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
:
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);
monad实际上是函数式编程语言中的CanFail
monad。希望有所帮助。
答案 1 :(得分:1)
以下答案是我对@AaditMShah给出的丰厚回报的回应。虽然它的长度和彻底性给我留下了深刻的印象,但它无法回答这个问题的能力却没有。所以,在我不是唯一一个机会的情况下,这里就是......
Promises API是函数式编程的monad pattern实例,它允许扩展函数组合以与函数返回值的某些方面配合。它的语法反映了这种模式;异步执行只在其实现中处理。因此,对模式的认识本身就是一个答案。
要扩展一点,您可能希望在需要进入功能组合过程时使用该模式。你问题中的代码不是一个很好的例子,因为除了异常处理之外,函数之间没有明显的联系。 (当然,如果这是您最初关注的问题,您可以使用该模式进行自定义故障处理。)
请考虑以下代码。
var a = 'initial value',
b = foo(a, 'more', 'arguments'),
// ...
result = bar(z);
我们可以通过将其重写为
来挖掘其功能构成on('initial value')
.do(_.partialRight(foo, 'more', 'arguments'))
// ...
.do(bar)
.do(function (result) {
// ...
});
(_.partialRight只是参数绑定。)
在这种情况下,新语法可能非常有用,因为它拦截了函数之间的数据流。根据您的需要,您可以实施on
/ do
来执行任何操作,例如并行处理复杂的数据结构节点或在阻塞一段时间后产生。
与任何其他模式一样,这个模式也会引入开销(在效率和代码维护方面),并且只应在有原因的情况下使用。