有同步 promise 的概念吗?使用promise语法编写同步代码是否有好处?
try {
foo();
bar(a, b);
bam();
} catch(e) {
handleError(e);
}
...可以写成类似(但使用
then
的同步版本);foo()
.then(bar.bind(a, b))
.then(bam)
.fail(handleError)
最佳答案
Is there such a concept as a synchronous promise?
本杰明是绝对正确的。 Promises are a type of monad。但是,它们不是唯一的类型。
如果您还不知道它,那您可能想知道什么是单子(monad)。网上有很多有关单子(monad)的解释。但是,大多数人患有monad tutorial fallacy。
简而言之,谬误是大多数了解单子(monad)的人并不真正知道如何向他人解释这一概念。简单来说,单子(monad)是一个抽象概念,人类很难掌握抽象概念。但是,人类容易轻视具体概念。
因此,让我们开始从一个具体的概念入手,了解单子(monad)。如我所说,单子(monad)是一个抽象的概念。这意味着monad是没有interface的implementation(即,它定义了某些操作并指定了这些操作应执行的操作,而未指定必须完成的操作)。
现在,有不同类型的单子(monad)。每种monad都是具体的(即,它定义monad implementation的interface)。 promise 是一种单子(monad)。因此,promise是monad的具体示例。因此,如果我们研究 promise ,那么我们就可以开始理解单子(monad)。
那么我们从哪里开始呢?幸运的是,用户spike在我们的comment中为您的问题提供了一个很好的起点:
One instance I can think of is chaining promises together with sync code. While finding an answer for this question: Generating AJAX Request Dynamically Based on Scenario I wrapped a synchronous call in a promise in order to be able to chain them with other promises.
因此,让我们看一下他的代码:
var run = function() {
getScenario()
.then(mapToInstruction)
.then(waitForTimeout)
.then(callApi)
.then(handleResults)
.then(run);
};
此处
run
函数返回一个 promise ,该 promise 由链在一起的getScenario
,mapToInstruction
,waitForTimeout
,callApi
,handleResults
和run
本身返回的 promise 组成。现在,在我们继续之前,我想向您介绍一个新的表示法,以可视化这些功能的作用:
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
是一个promise数据类型(具有不同的名称),而Deferred a
意味着当promise解析后,它会产生a
类型的值。 我们可以从上面的可视化中学到一些东西:
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
现在,正如我前面提到的,monad是一个interface,它定义了某些操作。 monad接口(interface)提供的操作之一是链接monad的操作。在 promise 的情况下,这是
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
类型的值的函数。”因此: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
但是,此操作是具体的。它特定于 promise 。我们想要一个可以对任何monad起作用的抽象操作。因此,我们对函数进行了概括,使其可以适用于任何monad:
bind :: Monad m => m a -> (a -> m b) -> m b
注意,此
bind
函数与 Function.prototype.bind
没有关系。此bind
函数是then
函数的概括。然后then
函数特定于promise。但是,bind
函数是通用的。它可以用于任何monad m
。粗箭头
=>
表示bounded quantification。如果a
和b
可以是任何类型,则m
可以是实现monad接口(interface)的任何类型。我们不关心m
是什么类型,只要它实现monad接口(interface)即可。这就是我们在JavaScript中实现和使用
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一样,
Identity
数据类型也是monad的一种类型。实际上,它是所有单子(monad)中最简单的。之所以称为Identity
,是因为它对输入类型没有任何作用。它保持原样。不同的monad具有不同的作用,这使它们有用。例如,promise具有管理异步性的作用。
Identity
monad无效。这是原始数据类型。无论如何,继续...我们发现了monad的一个操作,即
bind
函数。还有一项操作尚待发现。实际上,用户spike在他前面提到的评论中提到了它:I wrapped a synchronous call in a promise in order to be able to chain them with other promises.
您会发现,问题在于
then
函数的第二个参数必须是返回promise的函数:then :: Deferred a -> (a -> Deferred b) -> Deferred b
|_______________|
|
-- second argument is a function
-- that returns a promise
这意味着第二个参数必须是异步的(因为它返回了一个Promise)。但是,有时我们可能希望将同步函数与
then
链接在一起。为此,我们将同步函数的返回值包装在promise中。例如,这是spike所做的:// 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
和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
函数是monad接口(interface)的第二个缺失操作。概括后,可以将其可视化如下:unit :: Monad m => a -> m a
它所做的全部工作将值包装为monad数据类型。这使您可以将常规值和函数提升到单子(monad)上下文中。例如,promise提供一个异步上下文,并且
unit
允许您将同步函数提升到该异步上下文中。同样,其他monad提供其他效果。通过将
unit
与函数组合在一起,可以将函数提升到Monadic上下文中。例如,考虑我们之前定义的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是没有interface的implementation(即它定义了某些操作并指定了这些操作应执行的操作,而未指定必须如何执行)。因此,monad是一个接口(interface),它可以:
现在我们知道单子(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
和unit
函数,这些函数违反了这些法律。在那种情况下,then
和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
和map
实现的。但是,数组不是唯一拥有此属性的monad。每个monad bind
函数都可以按照concat
和map
的广义monadic版本来实现: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感到困惑,请不要担心。函子只是实现
fmap
函数的数据类型。根据定义,每个monad也是一个函子。我将不介绍单子(monad)法则的细节以及
fmap
和join
怎么等同于bind
。您可以在Wikipedia page上阅读有关它们的信息。另外,根据JavaScript Fantasy Land Specification,
unit
函数称为of
,而bind
函数称为chain
。这将允许您编写如下代码:Identity.of(1).chain(isOdd);
无论如何,回到您的主要问题:
Would there be any benefit to writing synchronous code using the syntax of promises?
是的,当使用promise的语法编写同步代码(即monadic代码)时,将会获得很大的好处。许多数据类型都是monad,使用monad接口(interface),您可以对不同类型的顺序计算建模,例如异步计算,非确定性计算,带故障的计算,带状态的计算,带日志的计算等。我最喜欢的使用monad的示例之一是使用free monads to create language interpreters。
Monad是功能编程语言的功能。使用monad可以促进代码重用。从这个意义上说,这绝对是件好事。但是,这是要付出代价的。功能代码比程序代码慢几个数量级。如果这对您来说不是问题,那么您绝对应该考虑编写monadic代码。
一些比较流行的monad是数组(用于非确定性计算),
Maybe
monad(用于可能失败的计算,类似于浮点数中的NaN
)和monadic parser combinators。try { foo(); bar(a, b); bam(); } catch(e) { handleError(e); }
...could be written something like (but using a synchronous version of
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
和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);
我描述的
CanFail
monad实际上是功能编程语言中的Either
monad。希望能有所帮助。
关于javascript - 使用promise的语法编写同步代码会不会有任何好处,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28937788/