使用 promises 语法编写同步代码会有什么好处吗?

Would there be any benefit to writing synchronous code using the syntax of promises

有同步promise这样的概念吗?使用 promises 语法编写同步代码会有什么好处吗?

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 是一个 interface without an implementation(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成)。

现在,有不同类型的 monad。每种类型的 monad 都是具体的(即它定义了一个 implementation of the monad interface)。承诺是一种单子。因此,promises 是 monad 的一个具体例子。因此,如果我们研究 promise,那么我们就可以开始理解 monad。

那么我们从哪里开始呢?幸运的是,用户spike gave us a good starting point in his 对你的问题:

One instance I can think of is chaining promises together with sync code. While finding an answer for this question: 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 函数 return 是一个承诺,由 getScenariomapToInstructionwaitForTimeout 编辑的承诺 return 组成, callApi, handleResultsrun 本身链接在一起。

现在,在我们继续之前,我想向您介绍一个新的符号来可视化这些函数在做什么:

run              :: Unit        -> Deferred a
getScenario      :: Unit        -> Deferred Data
mapToInstruction :: Data        -> Deferred Instruction
waitForTimeout   :: Instruction -> Deferred Instruction
callApi          :: Instruction -> Deferred Data
handleResults    :: Data        -> Deferred Unit

所以这是细分:

  1. ::符号表示“是类型”->符号表示“到”.因此,例如,run :: Unit -> Deferred a 读作 runUnitDeferred a 的类型”.
  2. 这意味着 run 是一个函数,它接受一个 Unit 值(即没有参数)并且 return 是一个 Deferred a 类型的值。
  3. 这里,a表示任意类型。我们不知道 a 是什么类型,也不关心 a 是什么类型。因此,它可以是任何类型。
  4. 这里,Deferred 是一个 promise 数据类型(具有不同的名称),Deferred a 意味着当 promise 被 resolve 时,它​​会产生一个 a 类型的值。

我们可以从上面的可视化中了解到几件事:

  1. 每个函数都有一些值并且return是一个承诺。
  2. 每个承诺的解析值return成为下一个函数的输入:

    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
    
  3. 下一个函数在上一个承诺被解析之前无法执行,因为它必须使用上一个承诺的已解析值。

现在,正如我之前提到的,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类型的函数)和return 是 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 的抽象操作。因此,我们推广该函数,使其适用于任何单子:

bind :: Monad m => m a -> (a -> m b) -> m b

注意这个bind函数与Function.prototype.bind没有任何关系。此 bind 函数是 then 函数的推广。然后 then 功能特定于承诺。但是,bind 函数是通用的。它适用于任何 monad m.

粗箭头=>表示bounded quantification。如果 ab 可以是任何类型,那么 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);
}

而不是bind(one, isOdd)我可以很容易地写成one.then(isOdd)(实际上更容易阅读)。

Identity 数据类型与 promises 一样,也是一种 monad。事实上,它是所有 monad 中最简单的。它被称为 Identity 是因为它不对其输入类型做任何事情。它保持原样。

不同的 monad 有不同的效果,这使它们很有用。例如,promises 具有管理异步性的作用。然而 Identity monad 没有任何效果。它是 vanilla 数据类型。

无论如何,继续...我们发现了 monad 的一种操作,即 bind 函数。还有一种操作有待发现。事实上,用户 spike 在他的上述评论中提到了它:

I wrapped a synchronous call in a promise in order to be able to chain them with other promises.

你看,问题是 then 函数的第二个参数必须是 return 承诺的函数:

then :: Deferred a -> (a -> Deferred b) -> Deferred b
                      |_______________|
                              |
                    -- second argument is a function
                    -- that returns a promise

这意味着第二个参数必须是异步的(因为它 return 是一个承诺)。然而,有时我们可能希望用 then 链接一个同步函数。为此,我们将同步函数的 return 值包装在一个 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函数的return值为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函数我们可以重写mapToInstructionhandleResults如下:

// 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 数据类型包装一个值。这允许您将常规值和函数提升到单子上下文中。例如,promises 提供了一个异步上下文,而 unit 允许您将同步函数提升到这个异步上下文中。同样,其他单子提供其他效果。

与函数组合 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 是一个 interface without an implementation(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成)。因此,monad 是一个接口:

  1. 定义某些操作。
  2. 指定这些操作应该做什么。

我们现在知道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); })

给定一个数据类型,我们可以为它定义违反这些定律的 thenunit 函数。在那种情况下,thenunit 的那些特定实现是不正确的。

例如,数组是一种代表非确定性计算的单子。让我们为数组定义一个不正确的 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 函数是根据 concatmap 实现的。然而,数组并不是唯一拥有这个 属性 的 monad。每个 monad bind 函数都可以根据 concatmap:

的通用 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 法则以及 fmapjoin 如何等价于 bind。您可以在 Wikipedia page.

上阅读有关它们的信息

附带说明,根据 JavaScript Fantasy Land Specificationunit 函数称为 ofbind 函数称为 chain。这将允许您编写如下代码:

Identity.of(1).chain(isOdd);

无论如何,回到你的主要问题:

Would there be any benefit to writing synchronous code using the syntax of promises?

是的,使用承诺语法(即单子代码)编写同步代码时可以获得很大的好处。许多数据类型都是 monad,使用 monad 接口你可以模拟不同类型的顺序计算,比如异步计算、非确定性计算、失败计算、状态计算、日志计算等。我最喜欢使用 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;
};

然后我们定义foobarbamhandleError:

// 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。希望对您有所帮助。

以下答案是我看到@AaditMShah 的巨大答案获得丰厚赏金后的反应。虽然它的篇幅和详尽给我留下了深刻印象,但它简洁回答问题的能力却没有。所以,如果我不是唯一的机会,这里是...


Promises API 是函数式编程中 monad pattern 的一个实例,它允许扩展函数组合以与函数的 return 值的某些方面协作。它的语法反映了这种模式;异步执行仅在其实现中处理。所以,认清规律本身就是一个答案。


为了扩展一点,您可能希望在需要进入功能组合过程时使​​用该模式。您问题中的代码不是一个很好的例子,因为除了异常处理之外,函数之间没有明显的联系。 (当然,如果您最初关心的是自定义故障处理模式,您可以使用该模式。)

改为考虑以下代码。

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 来做任何事情,例如并行处理复杂数据结构节点或阻塞一段时间后yield

与任何其他模式一样,这个模式也引入了开销(在效率和代码维护方面)并且应该只在有理由的情况下使用。