【问题标题】:Would there be any benefit to writing synchronous code using the syntax of promises使用 Promise 的语法编写同步代码有什么好处吗
【发布时间】:2015-03-18 19:39:54
【问题描述】:

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

try {
  foo();
  bar(a, b);
  bam();
} catch(e) {
  handleError(e);
}

...可以写成类似(但使用then的同步版本);

foo()
  .then(bar.bind(a, b))
  .then(bam)
  .fail(handleError)

【问题讨论】:

  • 我能想到的一个例子是将 Promise 与同步代码链接在一起。在找到这个问题的答案时:stackoverflow.com/questions/28418601/… 我将同步调用包装在一个 Promise 中,以便能够将它们与其他 Promise 链接起来。
  • 或者,如果您计划稍后使实现异步,例如您认为可以迁移到 WebWorker 的大量同步代码。但是这个问题应该是关于程序员的,而不是 SO。
  • 是的,使用与 promises 类似的 synchronous 来编写东西有很多好处 - promises 是 monadic :) 看看像 @ 这样的其他 monad 987654325@ Writer 等等 - 询问您友好的邻居 Haskell 潮人。我会ping我的:)
  • @Bergi 作为一名学生,我手头有很多时间。
  • @AaditMShah 我没有写答案,我做的是 ping 你,所以你可以写一个,因为这个问题正好在“Haskell,函数式 JavaScript,JS 中的 Monads”的交集上,而我认为自己在所有事情上都很擅长 - 我认为你更好:)

标签: javascript promise


【解决方案1】:

有同步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

所以这里是细分:

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

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

  1. 每个函数都接受一些值并返回一个承诺。
  2. 每个 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
    
  3. 在前一个 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 是一个接口:

  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); })

给定一种数据类型,我们可以为它定义违反这些法律的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。希望对您有所帮助。

【讨论】:

  • 我希望你会提到JavaScript monad interface(其中unit 被命名为of 而bind 被命名为chain)?
  • 哦,是的。我完全忘记了。
  • 很棒的答案。非常感谢。
  • 尽管这个答案很详细,但我很难找到对原始问题的一段摘要和解释:“使用承诺的语法编写同步代码有什么好处”?而且,那么自然的跟进是为什么呢?当然,顺序编程语句对于同步代码来说很容易脑死,所以我仍然不知道为什么要对纯同步代码使用 Promise?而且,我有点希望我不必阅读并尝试理解整本书的章节来获得原始问题的答案。
  • @jfriend00 是的,我可能应该向上移动它。
【解决方案2】:

以下答案是我看到@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 来做任何事情,例如复杂数据结构节点的并行处理或阻塞一段时间后的让步。

与任何其他模式一样,这种模式也引入了开销(在效率和代码维护方面),并且只应在有原因时使用。

【讨论】:

    猜你喜欢
    • 2012-08-18
    • 2020-04-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多