有同步promise这样的概念吗?
本杰明是绝对正确的。 Promises are a type of monad。但是,它们不是唯一的类型。
如果您还没有意识到它,那么您可能想知道什么是 monad。网上有很多关于 monad 的解释。但是,他们中的大多数人都遭受monad tutorial fallacy。
简而言之,谬误在于大多数了解 monad 的人并不真正知道如何向他人解释这个概念。简单来说,单子是一个抽象概念,人类很难掌握抽象概念。然而,人类很容易理解具体的概念。
那么,让我们从一个具体的概念开始理解 monad。正如我所说,单子是一个抽象概念。这意味着 monad 是一个没有 implementation 的 interface(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成)。
现在,有不同类型的 monad。每种类型的 monad 都是具体的(即它定义了 monad interface 的 implementation)。 Promise 是一种单子。因此,promise 是 monad 的一个具体例子。因此,如果我们研究 promise,那么我们就可以开始理解 monad。
那么我们从哪里开始呢?幸运的是,spike 用户在他的comment 中为我们提供了一个很好的起点:
我能想到的一个例子是将 Promise 与同步代码链接在一起。在寻找这个问题的答案时:Generating AJAX Request Dynamically Based on Scenario 我将同步调用包装在一个 Promise 中,以便能够将它们与其他 Promise 链接起来。
那么让我们看看他的代码:
var run = function() {
getScenario()
.then(mapToInstruction)
.then(waitForTimeout)
.then(callApi)
.then(handleResults)
.then(run);
};
这里run函数返回一个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 类型的值。
我们可以从上面的可视化中学到几件事:
- 每个函数都接受一些值并返回一个承诺。
-
每个 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 的解析值。
现在,正如我之前提到的,monad 是一个 interface,它定义了某些操作。 monad 接口提供的操作之一是链接 monad 的操作。在承诺的情况下,这是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
但是,这个操作是具体的。它特定于承诺。我们想要一个适用于任何 monad 的抽象操作。因此,我们对函数进行泛化,使其适用于任何 monad:
bind :: Monad m => m a -> (a -> m b) -> m b
请注意,这个bind 函数与Function.prototype.bind 无关。这个bind 函数是then 函数的泛化。然后 then 函数是特定于承诺的。但是,bind 函数是通用的。它适用于任何 monad m。
粗箭头=> 表示bounded quantification。如果a 和b 可以是任何类型,那么m 可以是任何类型实现monad 接口。我们不关心 m 是什么类型,只要它实现了 monad 接口即可。
这就是我们在 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);
}
我可以很容易地写成one.then(isOdd),而不是bind(one, isOdd)(实际上更容易阅读)。
Identity 数据类型和 promises 一样,也是一种 monad。事实上,它是所有单子中最简单的。它被称为Identity,因为它不对其输入类型做任何事情。它保持原样。
不同的 monad 有不同的效果,这使得它们很有用。例如,promise 具有管理异步性的效果。但是 Identity monad 没有效果。它是一种 vanilla 数据类型。
不管怎样,继续……我们发现了 monad 的一个操作,bind 函数。还有一项操作有待发现。事实上,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 就是这样做的:
// 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 接口的第二个缺失操作。泛化后,可视化如下:
unit :: Monad m => a -> m a
它所做的一切都是将一个值包装在一个 monad 数据类型中。这允许您将常规值和函数提升到一元上下文中。例如,promise 提供了一个异步上下文,unit 允许您将同步函数提升到这个异步上下文中。同样,其他 monad 也提供其他效果。
将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 函数会更好看:
// 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 是一个没有implementation 的interface(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成)。因此,monad 是一个接口:
- 定义某些操作。
- 指定这些操作应该做什么。
我们现在知道 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);
不管怎样,回到你的主要问题:
使用 Promise 的语法编写同步代码有什么好处吗?
是的,使用 Promise 的语法(即一元代码)编写同步代码会带来很多好处。许多数据类型是 monad,使用 monad 接口,您可以对不同类型的顺序计算进行建模,例如异步计算、非确定性计算、失败计算、状态计算、日志计算等。我最喜欢使用 monad 的示例之一是使用free monads to create language interpreters。
Monad 是函数式编程语言的一个特性。使用 monads 可以促进代码重用。从这个意义上说,它绝对是好的。但是,这是要付出代价的。功能代码比过程代码慢几个数量级。如果这对您来说不是问题,那么您绝对应该考虑编写一元代码。
一些更流行的 monad 是数组(用于非确定性计算)、Maybe monad(用于可能失败的计算,类似于浮点数中的NaN)和monadic parser combinators。
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和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。希望对您有所帮助。