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

Ben*_*Ben 8 javascript promise

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

try {
  foo();
  bar(a, b);
  bam();
} catch(e) {
  handleError(e);
}
Run Code Online (Sandbox Code Playgroud)

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

foo()
  .then(bar.bind(a, b))
  .then(bam)
  .fail(handleError)
Run Code Online (Sandbox Code Playgroud)

Aad*_*hah 17

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

本杰明是绝对正确的.承诺是一种单子行为.但是,它们不是唯一的类型.

如果您还没有意识到这一点,那么您可能想知道monad是什么.网上有很多关于monad的解释.但是,他们中的大多数都受到monad教程谬误的影响.

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

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

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

那么我们从哪里开始呢?幸运的是,用户峰值在他对你的问题的评论中给了我们一个很好的起点:

我能想到的一个例子是将promises与同步代码链接在一起.在找到这个问题的答案时:基于场景动态生成AJAX请求我在promise中包含了一个同步调用,以便能够将它们与其他promises链接起来.

那么让我们来看看他的代码:

var run = function() {
    getScenario()
    .then(mapToInstruction)
    .then(waitForTimeout)
    .then(callApi)
    .then(handleResults)
    .then(run);
};
Run Code Online (Sandbox Code Playgroud)

在这里,run函数返回一个它是由返回的承诺的承诺getScenario,mapToInstruction,waitForTimeout,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
Run Code Online (Sandbox Code Playgroud)

所以这是细分:

  1. ::符号是指"是该类型"->符号表示"到".因此,例如,run :: Unit -> Deferred a读取为" run类型UnitDeferred a".
  2. 这意味着这run是一个获取Unit值(即没有参数)并返回类型值的函数Deferred a.
  3. 这里,a意味着任何类型.我们不知道什么类型a,我们不关心什么类型a.因此,它可以是任何类型.
  4. 这里Deferred是一个promise数据类型(具有不同的名称),Deferred a意味着当promise被解析时,它会产生一个type类型的值a.

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

  1. Each function takes some value and returns a promise.
  2. The resolved value returned by each promise becomes the input to the next function:

    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
    
    Run Code Online (Sandbox Code Playgroud)
  3. The next function cannot execute until the previous promise is resolved because it has to make use of the resolved value of the previous promise.

Now, as I mentioned earlier a monad is an interface which defines certain operations. One of the operations that the monad interface provides is the operation of chaining monads. In case of promises this is the then method. For example:

getScenario().then(mapToInstruction)
Run Code Online (Sandbox Code Playgroud)

We know that:

getScenario      :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
Run Code Online (Sandbox Code Playgroud)

Hence:

getScenario()    :: Deferred Data -- because when called, getScenario
                                  -- returns a Deferred Data value
Run Code Online (Sandbox Code Playgroud)

We also know that:

getScenario().then(mapToInstruction) :: Deferred Instruction
Run Code Online (Sandbox Code Playgroud)

Thus, we can deduce:

then :: Deferred a -> (a -> Deferred b) -> Deferred b
Run Code Online (Sandbox Code Playgroud)

In words, "then is a function which takes two arguments (a value of the type Deferred a and a function of the type a -> Deferred b) and returns a value of type Deferred b." Hence:

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
Run Code Online (Sandbox Code Playgroud)

So we got our first monad operation:

then :: Deferred a -> (a -> Deferred b) -> Deferred b
Run Code Online (Sandbox Code Playgroud)

However, this operation is concrete. It is specific to promises. We want an abstract operation that can work for any monad. Hence, we generalize the function so that it can work for any monad:

bind :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

Note that this bind function has nothing to do with Function.prototype.bind. This bind function is a generalization of the then function. Then then function is specific to promises. However, the bind function is generic. It can work for any monad m.

The fat arrow => means bounded quantification. If a and b can be of any type whatsoever then m can be of any type whatsoever which implements the monad interface. We don't care what type m is as long as it implements the monad interface.

This is how we would implement and use the bind function in JavaScript:

function bind(m, f) {
    return m.then(f);
}

bind(getScenario(), mapToInstruction);
Run Code Online (Sandbox Code Playgroud)

How is this generic? Well, I could create a new data type which implements the then function:

// 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);
}
Run Code Online (Sandbox Code Playgroud)

Instead of bind(one, isOdd) I could just have easily written one.then(isOdd) (which is actually much easier to read).

The Identity data type, like promises, is also a type of monad. In fact, it is the simplest of all monads. It's called Identity because it doesn't do anything to its input type. It keeps it as it is.

Different monads have different effects which make them useful. For example, promises have the effect of managing asynchronicity. The Identity monad however has no effect. It is a vanilla data type.

Anyway, continuing... we discovered one operation of monads, the bind function. There is one more operation that is left to be discovered. In fact, the user spike alluded to it in his aforementioned comment:

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

You see, the problem is that the second argument of the then function must be a function which returns a promise:

then :: Deferred a -> (a -> Deferred b) -> Deferred b
                      |_______________|
                              |
                    -- second argument is a function
                    -- that returns a promise
Run Code Online (Sandbox Code Playgroud)

This implies that the second argument must be asynchronous (since it returns a promise). However, sometimes we may wish to chain a synchronous function with then. To do so, we wrap the return value of the synchronous function in a promise. For example, this is what spike did:

// 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();
};
Run Code Online (Sandbox Code Playgroud)

As you can see, the return value of the mapToInstruction function is instruction. However, we need to wrap it in a promise object which is why we do this:

// And now we create a promise from this
// instruction so we can chain it
var deferred = $.Deferred();
deferred.resolve(instruction);
return deferred.promise();
Run Code Online (Sandbox Code Playgroud)

In fact, he does the same thing in the handleResults function as well:

// handleResults :: Data -> Deferred Unit

var handleResults = function(data) {
    console.log("Handling data ...");
    var deferred = $.Deferred();
    deferred.resolve();
    return deferred.promise();
};
Run Code Online (Sandbox Code Playgroud)

It would be nice to put these three lines into a separate function so that we don't have to repeat ourselves:

// unit :: a -> Deferred a

function unit(value) {
    var deferred = $.Deferred();
    deferred.resolve(value);
    return deferred.promise();
}
Run Code Online (Sandbox Code Playgroud)

Using this unit function we can rewrite mapToInstruction and handleResults as follows:

// 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();
};
Run Code Online (Sandbox Code Playgroud)

In fact, as it turns out the unit function is the second missing operation of the monad interface. When generalized, it can be visualized as follows:

unit :: Monad m => a -> m a
Run Code Online (Sandbox Code Playgroud)

All it does it wrap a value in a monad data type. This allows you to lift regular values and functions into a monadic context. For example, promises provide an asynchronous context and unit allows you to lift synchronous functions into this asynchronous context. Similarly, other monads provide other effects.

Composing unit with a function allows you to lift the function into a monadic context. For example, consider the isOdd function we defined before:

// isOdd :: Number -> Identity Boolean

function isOdd(n) {
    return new Identity(n % 2 === 1);
}
Run Code Online (Sandbox Code Playgroud)

It would be nicer (albeit slower) to define it as follows instead:

// 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));
}
Run Code Online (Sandbox Code Playgroud)

It would look even nicer if we used a compose function:

// 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);
Run Code Online (Sandbox Code Playgroud)

I mentioned earlier that a monad is an interface without an implementation (i.e. it defines certain operations and specifies what those operations should do, without specifying how it must be done). Hence, a monad is an interface that:

  1. Defines certain operations.
  2. Specifies what those operations should do.

We now know that the two operations of a monad are:

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

unit :: Monad m => a -> m a
Run Code Online (Sandbox Code Playgroud)

Now, we'll look at what these operations should do or how they should behave (i.e. we will look at the laws that govern 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); })
Run Code Online (Sandbox Code Playgroud)

Given a data type we can define then and unit functions for it that violate these laws. In that case those particular implementations of then and unit are incorrect.

For example, arrays are a type of monad that represent non-deterministic computation. Let's define an incorrect unit function for arrays (the bind function for arrays is correct):

// 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));
}
Run Code Online (Sandbox Code Playgroud)

This incorrect definition of unit for arrays disobeys the second law (right identity):

// 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;
Run Code Online (Sandbox Code Playgroud)

The correct definition of unit for arrays would be:

// unit :: a -> Array a

function unit(x) {
    return [x];
}
Run Code Online (Sandbox Code Playgroud)

An interesting property to note is that the array bind function was implemented in terms of concat and map. However, arrays are not the only monad that possess this property. Every monad bind function can be implemented in terms of generalized monadic versions of concat and map:

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
Run Code Online (Sandbox Code Playgroud)

If you're confused about what a functor is then don't worry. A functor is just a data type that implements the fmap function. By definition, every monad is also a functor.

I won't get into the details of the monad laws and how fmap and join together are equivalent to bind. You can read about them on the Wikipedia page.

On a side note, according to the JavaScript Fantasy Land Specification the unit function is called of and the bind function is called chain. This would allow you to write code like:

Identity.of(1).chain(isOdd);
Run Code Online (Sandbox Code Playgroud)

Anyway, back to your main question:

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

Yes, there are great benefits to be gained when writing synchronous code using the syntax of promises (i.e. monadic code). Many data types are monads and using the monad interface you can model different types of sequential computations like asynchronous computations, non-deterministic computations, computations with failure, computations with state, computations with logging, etc. One of my favourite examples of using monads is to use free monads to create language interpreters.

Monads are a feature of functional programming languages. Using monads promotes code reuse. In that sense it is definitely good. However, it comes at a penalty. Functional code is orders of magnitude slower than procedural code. If that's not an issue for you then you should definitely consider writing monadic code.

Some of the more popular monads are arrays (for non-deterministic computation), the Maybe monad (for computations that can fail, similar to NaN in floating point numbers) and monadic parser combinators.

try {
  foo();
  bar(a, b);
  bam();
} catch(e) {
  handleError(e);
}
Run Code Online (Sandbox Code Playgroud)

...could be written something like (but using a synchronous version of then);

foo()
  .then(bar.bind(a, b))
  .then(bam)
  .fail(handleError)
Run Code Online (Sandbox Code Playgroud)

Yes, you can definitely write code like that. Notice that I didn't mention anything about the fail method. The reason is that you don't need a special fail method at all.

For example, let's create a monad for computations that can 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;
};
Run Code Online (Sandbox Code Playgroud)

Then we define foo, bar, bam and 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Finally, we can use it as follows:

// result :: CanFail Number String

var result = foo()
            .then(bar("Hello", "World"))
            .then(bam);

if (result instanceof Okay)
    alert(result.value);
else handleError(result.error);
Run Code Online (Sandbox Code Playgroud)

The CanFail monad that I described is actually the Either monad in functional programming languages. Hope that helps.

  • 正如这个答案所详述的那样,我很难找到原始问题的一段摘要和解释:"使用promises语法编写同步代码会有什么好处吗?" 然后,自然跟进就是为什么?当然,顺序编程语句对于同步代码来说很容易死,所以我仍然不知道为什么会使用promises来实现纯同步代码?并且,我有点希望我不必阅读并尝试理解整本书的章节以获得原始问题的答案. (7认同)
  • 我期待你提到[JavaScript monad接口](https://github.com/fantasyland/fantasy-land)(其中`unit`被命名为`of`和`bind`被命名为`chain`)? (3认同)