javascript - 使用promise的语法编写同步代码会不会有任何好处

标签 javascript promise

有同步 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是没有interfaceimplementation(即,它定义了某些操作并指定了这些操作应执行的操作,而未指定必须完成的操作)。

现在,有不同类型的单子(monad)。每种monad都是具体的(即,它定义monad implementationinterface)。 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 由链在一起的getScenariomapToInstructionwaitForTimeoutcallApihandleResultsrun本身返回的 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的类型为UnitDeferred a”。
  • 这意味着run是一个函数,它接受Unit值(即没有参数)并返回Deferred a类型的值。
  • 在这里,a表示任何类型。我们不知道a是什么类型,也不在乎a是什么类型。因此,它可以是任何类型。
  • 在这里,Deferred是一个promise数据类型(具有不同的名称),而Deferred a意味着当promise解析后,它会产生a类型的值。

  • 我们可以从上面的可视化中学到一些东西:
  • 每个函数取一些值并返回一个Promise。
  • 每个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接口(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。如果ab可以是任何类型,则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函数,我们可以按以下方式重写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接口(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是没有interfaceimplementation(即它定义了某些操作并指定了这些操作应执行的操作,而未指定必须如何执行)。因此,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); })
    

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

    例如,数组是一种表示非确定性计算的单子(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函数是根据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函数称为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;
    };
    

    然后我们定义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。希望能有所帮助。

    关于javascript - 使用promise的语法编写同步代码会不会有任何好处,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28937788/

    相关文章:

    javascript - 更改使用 <object> 嵌入的 SVG 的颜色

    javascript - 使用 AngularJS 在客户端初始化 Braintree 支付?

    javascript - 混合使用 ES6 Promise 和 JQuery Promise

    javascript - 当列表长度未知时依次调用http.get

    javascript - Bluebird 中 Promise.all() 的动态数组

    javascript - 如何调试 Gulp 任务?

    javascript - document.getElementsByClassName 上的 onclick 事件同时应用于所有元素

    javascript - onFocus 没有通过 selectionStart 获取光标位置

    javascript - 未处理拒绝的 promise 的意外 unhandledRejection 事件

    javascript - 执行一个 promise 循环并捕获过程中的错误