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

时间:2015-03-09 08:31:38

标签: javascript promise

是否存在同步承诺这样的概念?使用promises的语法编写同步代码会有什么好处吗?

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

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

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

2 个答案:

答案 0 :(得分:17)

  

是否存在同步承诺这样的概念?

本杰明是绝对正确的。 Promises are a type of monad。但是,它们不是唯一的类型。

如果您还没有意识到这一点,那么您可能想知道monad是什么。网上有很多关于monad的解释。但是,他们中的大多数都患有monad tutorial fallacy

简而言之,谬论是大多数了解monad的人并不真正知道如何向他人解释这个概念。简单来说,monad是一个抽象概念,人类很难掌握抽象概念。然而,人类很容易理解具体的概念。

因此,让我们开始征服,从一个具体的概念开始理解monad。正如我所说,monad是一个抽象的概念。这意味着monad是interface而没有implementation(即它定义了某些操作并指定了这些操作应该做什么,而没有指定必须如何完成)。

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

那么我们从哪里开始呢?幸运的是,用户spikecomment问题中为我们提供了一个很好的起点:

  

我能想到的一个实例是将promises与同步代码链接在一起。在找到这个问题的答案时:Generating AJAX Request Dynamically Based on Scenario我在一个承诺中包含了一个同步调用,以便能够将它们与其他承诺链接起来。

让我们看看他的代码:

var run = function() {
    getScenario()
    .then(mapToInstruction)
    .then(waitForTimeout)
    .then(callApi)
    .then(handleResults)
    .then(run);
};

此处run函数返回一个承诺,该承诺由getScenariomapToInstructionwaitForTimeoutcallApi,{{1}返回的承诺组成和handleResults本身链接在一起。

现在,在我们继续之前,我想向您介绍一种新的符号,以便可视化这些功能正在做什么:

run

所以这里有细分:

  1. 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类型为runUnit
  2. 这意味着Deferred a是一个函数,它接受run值(即没有参数)并返回Unit类型的值。
  3. 此处,Deferred a表示任何类型。我们不知道a是什么类型,我们不关心a类型。因此,它可以是任何类型。
  4. 此处,a是一种承诺数据类型(名称不同),Deferred表示在解析承诺时会产生Deferred a类型的值。
  5. 我们可以从上述可视化中学到一些东西:

    1. 每个函数都会获取一些值并返回一个promise。
    2. 每个promise返回的已解析值将成为下一个函数的输入:

      a
    3. 在解决上一个承诺之前,下一个函数无法执行,因为它必须使用先前承诺的已解析值。

    4. 现在,正如我之前提到的,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(); } 功能,我们可以重写unitmapToInstruction,如下所示:

      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是一个没有interfaceimplementation(即它定义了某些操作并指定了这些操作应该做什么,而没有说明必须如何完成)。因此,monad是一个接口:

      1. 定义某些操作。
      2. 指定这些操作应该做什么。
      3. 我们现在知道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函数。在这种情况下,unitthen的特定实现是不正确的。

        例如,数组是一种表示非确定性计算的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]; } 函数是根据bindconcat实现的。但是,数组并不是唯一拥有此属性的monad。每个monad map函数都可以使用bindconcat的广义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法律以及fmapfmap如何相等于join。您可以在Wikipedia page上了解它们。

        在旁注中,根据JavaScript Fantasy Land Specificationbind函数称为unitof函数称为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; }; foobarbam

        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来执行任何操作,例如并行处理复杂的数据结构节点或在阻塞一段时间后产生。

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