Would there be any benefit to writing synchronous

2019-04-24 18:16发布

问题:

Is there such a concept as a synchronous promise? Would there be any benefit to writing synchronous code using the syntax of promises?

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)

回答1:

Is there such a concept as a synchronous promise?

Benjamin is absolutely right. Promises are a type of monad. However, they are not the only type.

If you're not already aware of it then you're probably wondering what a monad is. There are lots of explanations of monads available online. However, most of them suffer from the monad tutorial fallacy.

In brief, the fallacy is that most people who understand monads don't really know how to explain the concept to others. In simple terms, monads are an abstract concept and human beings find it difficult to grasp abstract concepts. However, it's easy for humans to grok concrete concepts.

So let's begin our conquest to understand monads starting with a concrete concept. As I said, monads are an abstract concept. This means 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).

Now, there are different types of monads. Each type of monad is concrete (i.e. it defines an implementation of the monad interface). Promises are a type of monad. Hence, promises are a concrete example of a monad. Thus, if we study promises then we can begin to understand monads.

So where do we begin? Fortunately, the user spike gave us a good starting point in his comment to your question:

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.

So let's look at his code:

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

Here the run function returns a promise which is composed of the promises returned by getScenario, mapToInstruction, waitForTimeout, callApi, handleResults and run itself chained together.

Now, before we proceed I want to introduce to you a new notation to visualize what these functions are doing:

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

So here's the breakdown:

  1. The :: symbol means “is of the type” and the -> symbol means “to”. Hence for example, run :: Unit -> Deferred a reads as run is of the type Unit to Deferred a.
  2. That means that run is a function which takes a Unit value (i.e. no arguments) and returns a value of type Deferred a.
  3. Here, a means any type. We don't know what type a is and we don't care what type a is. Hence, it can be any type whatsoever.
  4. Here, Deferred is a promise data type (with a different name) and Deferred a means that when the promise is resolved it yields a value of type a.

There are several things we can learn from the above visualization:

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

We know that:

getScenario      :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction

Hence:

getScenario()    :: Deferred Data -- because when called, getScenario
                                  -- returns a Deferred Data value

We also know that:

getScenario().then(mapToInstruction) :: Deferred Instruction

Thus, we can deduce:

then :: Deferred a -> (a -> Deferred b) -> Deferred b

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

So we got our first monad operation:

then :: Deferred a -> (a -> Deferred b) -> Deferred b

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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;

The correct definition of unit for arrays would be:

// unit :: a -> Array a

function unit(x) {
    return [x];
}

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

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

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

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

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

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

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

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

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



回答2:

The following answer is my reaction to seeing the humongous answer by @AaditMShah awarded a fat bounty. While its length and thoroughness impressed me, its ability to succintly answer the question did not. So, on the off-chance that I'm not the only one, here goes...


Promises API is an instance of the monad pattern from functional programming, which allows extending functional composition to cooperate with some aspect of the functions' return values. Its syntax reflects this pattern; asynchronous execution is only being dealt with in its implementation. Therefore, recognition of the pattern is an answer in itself.


To expand a bit, you might want to use the pattern whenever you need to tap into the process of functional composition. The code in your question is not a good example, because there is no apparent connection between the functions apart from exception handling. (Of course, you can use the pattern for custom failure handling, if that was your original concern.)

Consider the following code instead.

var a = 'initial value',
    b = foo(a, 'more', 'arguments'),
    // ...
    result = bar(z);

We could tap into its functional composition by re-writing it as

on('initial value')
    .do(_.partialRight(foo, 'more', 'arguments'))
    // ...
    .do(bar)
    .do(function (result) {
        // ...
    });

(_.partialRight is just argument binding.)

In this case, the new syntax could be immensely useful, because it intercepts the data flow between the functions. Depending on your needs, you could implement on/do to do anything, e.g. parallel processing of complex data structure nodes or yielding after blocking for some time.

Like any other pattern, this one too introduces an overhead (in both efficiency and code maintenance) and should only be used when there's a reason for it.